Added product admin features

This commit is contained in:
Niklas Fondberg
2021-07-26 10:35:52 +02:00
committed by GitHub
parent 32adee1bf4
commit d17df41d86
38 changed files with 1613 additions and 381 deletions
+2 -1
View File
@@ -10,6 +10,7 @@
"extends": ["prettier"], "extends": ["prettier"],
"plugins": ["@typescript-eslint", "prettier"], "plugins": ["@typescript-eslint", "prettier"],
"rules": { "rules": {
"prettier/prettier": ["error"] "prettier/prettier": ["error"],
"no-return-await": "error"
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
# Uses the node base image with the latest LTS version # Uses the node base image with the latest LTS version
FROM node:14.16.0 FROM node:14.17.0
# Informs Docker that the container listens on the # Informs Docker that the container listens on the
# specified network ports at runtime # specified network ports at runtime
EXPOSE 4000 EXPOSE 4000
+12
View File
@@ -0,0 +1,12 @@
# Uses the node base image with the latest LTS version
FROM node:14.17.0
# Informs Docker that the container listens on the
# specified network ports at runtime
EXPOSE 4000
COPY . app/
# Changes working directory to the new directory just created
WORKDIR /app
# Installs npm dependencies on container
RUN npm ci
# Command container will actually run when called
CMD ["npx", "nodemon", "-w", "src", "--ext", "ts", "--exec", "ts-node", "src/index.ts"]
+2 -1
View File
@@ -17,6 +17,7 @@ Then head over to [The playground GUI](https://localhost:4000) in the browser
## Run tests ## Run tests
Scripts will create the insert sql files for a randomized fake data postgres database. This database is based of the actual db in photowall project. Scripts will create the insert sql files for a randomized fake data postgres database. This database is based of the actual db in photowall project.
``` ```
npm test npm test
``` ```
@@ -27,6 +28,6 @@ The tests are held in testcontainers [testcontainers-node](https://github.com/te
Each test is build around inserting fake data in the empty db, fetching that data through the GraphQL and with raw Sql queries and then comparing the results. Each test is build around inserting fake data in the empty db, fetching that data through the GraphQL and with raw Sql queries and then comparing the results.
To write new inserts into the DB generate sql files with js in the src/__test__/db_generator tool. Then load the new sql file in the containerSetup.ts file. To write new inserts into the DB generate sql files with js in the src/**test**/db_generator tool. Then load the new sql file in the containerSetup.ts file.
When you have the data you need in the db, write tests in all.test.ts file following the pattern already set. When you have the data you need in the db, write tests in all.test.ts file following the pattern already set.
+120
View File
@@ -0,0 +1,120 @@
##
# Example:
# make create-stack STACK_ENVIRONMENT_NAME=staging
# make create-stack STACK_ENVIRONMENT_NAME=production
#
# # make update-stack STACK_ENVIRONMENT_NAME=staging
##
STACK_NAME=graphql
GITHUB_REPO := graphql-api-js
GITHUB_USER := Photowall
$(eval GITHUB_TOKEN=$(shell aws ssm get-parameter --with-decryption --name /github/CODE_PIPELINE_TOKEN --output text --query Parameter.Value))
AWS_REGION=$(shell aws configure get region)
AWS_ACCOUNT_ID=$(shell aws sts get-caller-identity --query Account --output text)
# HOSTED_ZONE=photowall.com default in the template
# Cert ARN for the hosted zone
CERTIFICATE_ARN='arn:aws:acm:eu-west-1:954747537408:certificate/1278df32-006d-48a4-abd6-2bfccabb11fd'
DB_SECURITY_GROUP_ID_PRODUCTION=sg-0b5bafc1e0a97e848
DB_SECURITY_GROUP_ID_STAGING=sg-001b3667edf19e1ce
checkdef = $(if $(value $(1)),,$(error $(1) not set))
DOCKER_IMAGE=$(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/$(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)
ifeq ($(STACK_ENVIRONMENT_NAME),production)
SUBDOMAIN=$(STACK_NAME)
GITHUB_BRANCH=master
DB_SECURITY_GROUP_ID=$(DB_SECURITY_GROUP_ID_PRODUCTION)
CPU_SIZE=1024
MEMORY_SIZE=2GB
else
SUBDOMAIN=$(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)
GITHUB_BRANCH=$(STACK_ENVIRONMENT_NAME)
DB_SECURITY_GROUP_ID=$(DB_SECURITY_GROUP_ID_STAGING)
CPU_SIZE=512
MEMORY_SIZE=1GB
endif
check-create-initial-ecr-image:
@echo Checking for inital image $(DOCKER_IMAGE)
@aws ecr list-images --repository-name $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME) 2>/dev/null | grep imageTag | grep -q latest; \
status=$$?; \
if [ $$status != 0 ]; \
then \
echo ""; \
echo "ECR Repo for initial image does not exist, creating"; \
echo ""; \
aws ecr create-repository --repository $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME); \
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com; \
docker pull nginx; \
docker tag nginx $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/$(STACK_NAME)-$(STACK_ENVIRONMENT_NAME):latest; \
docker push $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/$(STACK_NAME)-$(STACK_ENVIRONMENT_NAME):latest; \
echo ""; \
fi
set-repository-lifecycle-policy:
@echo Setting lifecycle policy for repo $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)
@aws ecr put-lifecycle-policy \
--repository-name $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME) \
--lifecycle-policy-text "file://ecr-lifecycle-policy.json"
check-create-variables-set:
$(call checkdef,STACK_NAME)
$(call checkdef,STACK_ENVIRONMENT_NAME)
$(call checkdef,GITHUB_TOKEN)
$(call checkdef,GITHUB_USER)
$(call checkdef,GITHUB_REPO)
check-update-variables-set:
$(call checkdef,STACK_ENVIRONMENT_NAME)
delete-stack:
aws cloudformation delete-stack --stack-name $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)
aws ecr delete-repository --force --repository-name $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)
@echo ""
@echo "################################################################"
@echo "# Do not forget to delete the articast bucket and loggroup:"
@aws s3 ls | grep $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)
@echo "# aws s3 rb --force s3://<bucket name>"
@echo ""
@echo "aws logs delete-log-group --log-group-name /aws/codebuild/$(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)"
@echo "aws logs delete-log-group --log-group-name $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)"
@echo "################################################################"
create-stack: check-create-variables-set check-create-initial-ecr-image
aws cloudformation create-stack --stack-name $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME) --template-body file://ecs-service.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--tags Key=EnvironmentName,Value=$(STACK_ENVIRONMENT_NAME) \
--parameters ParameterKey="EnvironmentName",ParameterValue="$(STACK_ENVIRONMENT_NAME)" \
ParameterKey="Image",ParameterValue="$(DOCKER_IMAGE)" \
ParameterKey="Subdomain",ParameterValue="$(SUBDOMAIN)" \
ParameterKey="Certificate",ParameterValue="$(CERTIFICATE_ARN)" \
ParameterKey="DatabaseSecurityGroup",ParameterValue="$(DB_SECURITY_GROUP_ID)" \
ParameterKey="GitHubRepo",ParameterValue="$(GITHUB_REPO)" \
ParameterKey="GitHubUser",ParameterValue="$(GITHUB_USER)" \
ParameterKey="GitHubBranch",ParameterValue="$(GITHUB_BRANCH)" \
ParameterKey="GitHubToken",ParameterValue="$(GITHUB_TOKEN)" \
ParameterKey="CpuSize",ParameterValue="$(CPU_SIZE)" \
ParameterKey="MemorySize",ParameterValue="$(MEMORY_SIZE)"
update-stack: set-repository-lifecycle-policy
$(call checkdef,STACK_NAME)
$(call checkdef,ENVIRONMENT_NAME)
aws cloudformation update-stack --stack-name $(STACK_NAME)-$(STACK_ENVIRONMENT_NAME) --template-body file://ecs-service.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--tags Key=EnvironmentName,Value=$(STACK_ENVIRONMENT_NAME) \
--parameters ParameterKey="EnvironmentName",UsePreviousValue=true \
ParameterKey="Image",UsePreviousValue=true \
ParameterKey="Subdomain",UsePreviousValue=true \
ParameterKey="Certificate",UsePreviousValue=true \
ParameterKey="DatabaseSecurityGroup",,UsePreviousValue=true \
ParameterKey="GitHubRepo",UsePreviousValue=true \
ParameterKey="GitHubUser",UsePreviousValue=true \
ParameterKey="GitHubBranch",UsePreviousValue=true \
ParameterKey="GitHubToken",UsePreviousValue=true \
ParameterKey="CpuSize",ParameterValue="$(CPU_SIZE)" \
ParameterKey="MemorySize",ParameterValue="$(MEMORY_SIZE)"
+13
View File
@@ -0,0 +1,13 @@
{
"rules": [
{
"rulePriority": 1,
"description": "Only keep 8 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 8
},
"action": { "type": "expire" }
}]
}
+527
View File
@@ -0,0 +1,527 @@
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation template for a full E2E ECS service with CI/CD on Fargate.
Parameters:
VPC:
Type: AWS::EC2::VPC::Id
Default: vpc-6f596904
Description: The id of the VPC
SubnetA:
Type: AWS::EC2::Subnet::Id
Default: subnet-6e596905
Description: The id of the first subnet
SubnetB:
Type: AWS::EC2::Subnet::Id
Default: subnet-6d596906
Description: The id of the second subnet
DatabaseSecurityGroup:
Type: String
Description: Security group id to access the database
Image:
Type: String
# Docker image for this service. It must have a latest tag
# Example 260851616142.dkr.ecr.eu-west-1.amazonaws.com/nginx
EnvironmentName:
Type: String
Description: Name of env such as staging or production
# CPU
# 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB
# 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB
# 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB
# 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments
# 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments
CpuSize:
Type: Number
# Memory
# 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU)
# 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU)
# 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU)
# Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU)
# Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU)
MemorySize:
Type: String
ContainerPort:
Type: Number
Default: 4000
LoadBalancerPort:
Type: Number
Default: 443
HealthCheckPath:
Type: String
Default: /.well-known/apollo/server-health
Certificate:
Type: String
# Update with the certificate ARN from Certificate Manager, which must exist in the same region.
HostedZoneName:
Type: String
Default: photowall.com
Subdomain:
Type: String
# for autoscaling
MinContainers:
Type: Number
Default: 1
# for autoscaling
MaxContainers:
Type: Number
Default: 4
# target CPU utilization (%)
AutoScalingTargetValue:
Type: Number
Default: 50
# Github info for code pipeline
GitHubRepo:
Type: String
GitHubBranch:
Type: String
GitHubToken:
Type: String
NoEcho: true
GitHubUser:
Type: String
Resources:
Cluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Join ["", [!Ref "AWS::StackName", Cluster]]
TaskDefinition:
Type: AWS::ECS::TaskDefinition
# Makes sure the log group is created before it is used.
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]]
# awsvpc is required for Fargate
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: !Ref CpuSize
Memory: !Ref MemorySize
# A role needed by ECS.
# "The ARN of the task execution role that containers in this task can assume. All containers in this task are granted the permissions that are specified in this role."
# "There is an optional task execution IAM role that you can specify with Fargate to allow your Fargate tasks to make API calls to Amazon ECR."
ExecutionRoleArn: !Ref ExecutionRole
# "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that grants containers in the task permission to call AWS APIs on your behalf."
TaskRoleArn: !Ref TaskRole
ContainerDefinitions:
- Name: !Ref AWS::StackName
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${AWS::StackName}:latest
PortMappings:
- 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"
# Send logs to CloudWatch Logs
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Ref AWS::Region
awslogs-group: !Ref LogGroup
awslogs-stream-prefix: ecs
# A role needed by ECS
ExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["", [!Ref "AWS::StackName", ExecutionRole]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "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]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
# A role needed for auto scaling
AutoScalingRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join ["", [!Ref "AWS::StackName", AutoScalingRole]]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole"
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription:
!Join ["", [!Ref "AWS::StackName", ContainerSecurityGroup]]
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref ContainerPort
ToPort: !Ref ContainerPort
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
LoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription:
!Join ["", [!Ref "AWS::StackName", LoadBalancerSecurityGroup]]
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref LoadBalancerPort
ToPort: !Ref LoadBalancerPort
CidrIp: 0.0.0.0/0
Service:
Type: AWS::ECS::Service
# This dependency is needed so that the load balancer is setup correctly in time
DependsOn:
- ListenerHTTPS
Properties:
ServiceName: !Ref AWS::StackName
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DeploymentConfiguration:
MinimumHealthyPercent: 100
MaximumPercent: 200
DesiredCount: 1
# This may need to be adjusted if the container takes a while to start up
HealthCheckGracePeriodSeconds: 30
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
# change to DISABLED if you're using private subnets that have access to a NAT gateway
AssignPublicIp: ENABLED
Subnets:
- !Ref SubnetA
- !Ref SubnetB
SecurityGroups:
- !Ref ContainerSecurityGroup
- !Ref DatabaseSecurityGroup
LoadBalancers:
- ContainerName: !Ref AWS::StackName
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref TargetGroup
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 15
# will look for a 200 status code by default unless specified otherwise
HealthCheckPath: !Ref HealthCheckPath
HealthCheckTimeoutSeconds: 10
UnhealthyThresholdCount: 2
HealthyThresholdCount: 2
Name: !Join ["", [!Ref "AWS::StackName", Group]]
Port: !Ref ContainerPort
Protocol: HTTP
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60 # default is 300
TargetType: ip
VpcId: !Ref VPC
ListenerHTTPS:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: !Ref LoadBalancerPort
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref Certificate
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: 1200
Name: !Join ["", [!Ref "AWS::StackName", LB]]
# "internal" is also an option
Scheme: internet-facing
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets:
- !Ref SubnetA
- !Ref SubnetB
DNSRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Join ["", [!Ref HostedZoneName, .]]
Name: !Join ["", [!Ref Subdomain, ., !Ref HostedZoneName, .]]
Type: A
AliasTarget:
DNSName: !GetAtt LoadBalancer.DNSName
HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
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]]
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."
RoleARN: !GetAtt AutoScalingRole.Arn
AutoScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Join ["", [!Ref "AWS::StackName", AutoScalingPolicy]]
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref AutoScalingTarget
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
ScaleInCooldown: 10
ScaleOutCooldown: 10
# Keep things at or lower than 50% CPU utilization, for example
TargetValue: !Ref AutoScalingTargetValue
## Deployment pipeline below
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: root
PolicyDocument:
Version: 2012-10-17
Statement:
- Resource: "*"
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- ecr:GetAuthorizationToken
- Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetObjectVersion
- Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${AWS::StackName}
Effect: Allow
Action:
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- ecr:BatchCheckLayerAvailability
- ecr:PutImage
- ecr:InitiateLayerUpload
- ecr:UploadLayerPart
- ecr:CompleteLayerUpload
- Resource: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/dockerhub/*
Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: root
PolicyDocument:
Version: 2012-10-17
Statement:
- Resource:
- !Sub arn:aws:s3:::${ArtifactBucket}/*
Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketVersioning
- Resource: "*"
Effect: Allow
Action:
- ecs:List*
- ecs:Describe*
- ecs:RegisterTaskDefinition
- ecs:UpdateService
- codebuild:StartBuild
- codebuild:BatchGetBuilds
- iam:PassRole
ArtifactBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: CODEPIPELINE
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
install:
runtime-versions:
docker: 19
pre_build:
commands:
- echo Logging in to Docker Hub...
- echo $DOCKERHUB_PASSWORD | docker login --username $DOCKERHUB_USERNAME --password-stdin
- TAG="$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | head -c 8)"
- IMAGE_URI="${REPOSITORY_URI}:${TAG}"
- IMAGE_LATEST_URI="${REPOSITORY_URI}:latest"
build:
commands:
- docker build --tag "$IMAGE_URI" .
- docker tag "$IMAGE_URI" "$IMAGE_LATEST_URI"
post_build:
commands:
- echo Logging in to ECR to push image
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- docker push "$IMAGE_URI"
- docker push "$IMAGE_LATEST_URI"
- printf '[{"name":"%s","imageUri":"%s"}]' "$IMAGE_NAME" "$IMAGE_LATEST_URI" > images.json
artifacts:
files: images.json
Environment:
ComputeType: BUILD_GENERAL1_SMALL
PrivilegedMode: true
Image: aws/codebuild/standard:4.0
Type: LINUX_CONTAINER
EnvironmentVariables:
- Name: AWS_DEFAULT_REGION
Value: !Ref AWS::Region
- Name: AWS_ACCOUNT_ID
Value: !Ref AWS::AccountId
- Name: REPOSITORY_URI
Value: !Ref Image
- Name: IMAGE_NAME
Value: !Ref AWS::StackName
- Name: DOCKERHUB_USERNAME
Value: /dockerhub/username
Type: PARAMETER_STORE
- Name: DOCKERHUB_PASSWORD
Value: /dockerhub/password
Type: PARAMETER_STORE
Name: !Ref AWS::StackName
ServiceRole: !Ref CodeBuildServiceRole
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
RoleArn: !GetAtt CodePipelineServiceRole.Arn
Name: !Join ["", [!Ref "AWS::StackName", Pipeline]]
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
Stages:
- Name: Source
Actions:
- Name: App
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
Configuration:
Owner: !Ref GitHubUser
Repo: !Ref GitHubRepo
Branch: !Ref GitHubBranch
OAuthToken: !Ref GitHubToken
OutputArtifacts:
- Name: App
RunOrder: 1
- Name: Build
Actions:
- Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuildProject
InputArtifacts:
- Name: App
OutputArtifacts:
- Name: BuildOutput
RunOrder: 1
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: ECS
Configuration:
ClusterName: !Ref Cluster
ServiceName: !Ref Service
FileName: images.json
InputArtifacts:
- Name: BuildOutput
RunOrder: 1
# What outputs to expose
Outputs:
Endpoint:
Description: Endpoint
Value: !Join ["", ["https://", !Ref DNSRecord]]
Service:
Value: Service
PipelineUrl:
Value: !Sub https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/${Pipeline}
ContainerSecurityGroup:
Value: !Ref ContainerSecurityGroup
TaskDefinition:
Value: !Ref TaskDefinition
TargetGroup:
Value: !Ref TargetGroup
LoadBalancerUrl:
Description: The URL of the ALB
Value: !GetAtt LoadBalancer.DNSName
+21
View File
@@ -0,0 +1,21 @@
version: '3.8'
services:
api:
build:
context: ./
dockerfile: ./Dockerfile-dev
volumes:
- ./:/app
container_name: photowall.graphql
hostname: graphql.local
ports:
- '4000:4000'
environment:
- COGNITO_POOL_ID=eu-west-1_3O4VfvPn7
- DATABASE_URL=postgresql://fondberg:@docker.for.mac.localhost/photowall
networks:
default:
external:
name: photowall-docker-network
+4 -3
View File
@@ -1,18 +1,19 @@
version: '3.7' version: '3.8'
services: services:
api: api:
build: build:
context: ./ context: ./
dockerfile: ./Dockerfile dockerfile: ./Dockerfile
volumes: # volumes:
- ./:/app # - ./:/app
container_name: photowall.graphql container_name: photowall.graphql
hostname: graphql.local hostname: graphql.local
ports: ports:
- '4000:4000' - '4000:4000'
environment: environment:
- COGNITO_POOL_ID=eu-west-1_3O4VfvPn7 - COGNITO_POOL_ID=eu-west-1_3O4VfvPn7
- DATABASE_URL=postgresql://fondberg:@docker.for.mac.localhost/photowall
networks: networks:
default: default:
+117 -84
View File
@@ -1251,9 +1251,9 @@
"dev": true "dev": true
}, },
"@nodelib/fs.walk": { "@nodelib/fs.walk": {
"version": "1.2.7", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@nodelib/fs.scandir": "2.1.5", "@nodelib/fs.scandir": "2.1.5",
@@ -1380,9 +1380,9 @@
} }
}, },
"@types/archiver": { "@types/archiver": {
"version": "5.3.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.1.1.tgz",
"integrity": "sha512-qJ79qsmq7O/k9FYwsF6O1xVA1PeLV+9Bh3TYkVCu3VzMR6vN9JQkgEOh/rrQ0R+F4Ta+R3thHGewxQtFglwVfg==", "integrity": "sha512-heuaCk0YH5m274NOLSi66H1zX6GtZoMsdE6TYFcpFFjBjg0FoU4i4/M/a/kNlgNg26Xk3g364mNOYe1JaiEPOQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/glob": "*" "@types/glob": "*"
@@ -1468,9 +1468,9 @@
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==" "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
}, },
"@types/dockerode": { "@types/dockerode": {
"version": "3.2.3", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.4.tgz",
"integrity": "sha512-nZRhpSxm3PYianRBcRExcHxDvEzYHUPfGCnRL5Fe4/fSEZbtxrRNJ7okzCans3lXxj2t298EynFHGTnTC2f1Iw==", "integrity": "sha512-CSpvSohBQib0KCyg99h6RSmLZ4V9wF9hx6vT2FatplBNQpRaUzJh/XGjesRPzvqOlpp/Ol7aLFEgiE02nj/m2g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@@ -1506,9 +1506,9 @@
} }
}, },
"@types/glob": { "@types/glob": {
"version": "7.1.3", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/minimatch": "*", "@types/minimatch": "*",
@@ -1559,9 +1559,9 @@
} }
}, },
"@types/jest": { "@types/jest": {
"version": "26.0.23", "version": "26.0.24",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz",
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==", "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==",
"dev": true, "dev": true,
"requires": { "requires": {
"jest-diff": "^26.0.0", "jest-diff": "^26.0.0",
@@ -1569,9 +1569,9 @@
} }
}, },
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.7", "version": "7.0.8",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
"dev": true "dev": true
}, },
"@types/keygrip": { "@types/keygrip": {
@@ -1613,9 +1613,9 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
}, },
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.4", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
@@ -1649,9 +1649,9 @@
} }
}, },
"@types/ssh2": { "@types/ssh2": {
"version": "0.5.46", "version": "0.5.47",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.46.tgz", "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.47.tgz",
"integrity": "sha512-1pC8FHrMPYdkLoUOwTYYifnSEPzAFZRsp3JFC/vokQ+dRrVI+hDBwz0SNmQ3pL6h39OSZlPs0uCG7wKJkftnaA==", "integrity": "sha512-ZhqJg8BRV7OsCi0KVqPr27lUMMmLEeHYw1VXUNGGDlQEDq9HTsKx+wYvi8E6oNC6gRZ7PV99ZMZmMr5vztcYYA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
@@ -1659,9 +1659,9 @@
} }
}, },
"@types/ssh2-streams": { "@types/ssh2-streams": {
"version": "0.1.8", "version": "0.1.9",
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.8.tgz", "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz",
"integrity": "sha512-I7gixRPUvVIyJuCEvnmhr3KvA2dC0639kKswqD4H5b4/FOcnPtNU+qWLiXdKIqqX9twUvi5j0U1mwKE5CUsrfA==", "integrity": "sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@@ -1682,9 +1682,9 @@
} }
}, },
"@types/yargs": { "@types/yargs": {
"version": "15.0.13", "version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
"integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/yargs-parser": "*" "@types/yargs-parser": "*"
@@ -1697,13 +1697,13 @@
"dev": true "dev": true
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "4.28.2", "version": "4.28.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.3.tgz",
"integrity": "sha512-PGqpLLzHSxq956rzNGasO3GsAPf2lY9lDUBXhS++SKonglUmJypaUtcKzRtUte8CV7nruwnDxtLUKpVxs0wQBw==", "integrity": "sha512-jW8sEFu1ZeaV8xzwsfi6Vgtty2jf7/lJmQmDkDruBjYAbx5DA8JtbcMnP0rNPUG+oH5GoQBTSp+9613BzuIpYg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/experimental-utils": "4.28.2", "@typescript-eslint/experimental-utils": "4.28.3",
"@typescript-eslint/scope-manager": "4.28.2", "@typescript-eslint/scope-manager": "4.28.3",
"debug": "^4.3.1", "debug": "^4.3.1",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"regexpp": "^3.1.0", "regexpp": "^3.1.0",
@@ -1738,28 +1738,28 @@
} }
}, },
"@typescript-eslint/experimental-utils": { "@typescript-eslint/experimental-utils": {
"version": "4.28.2", "version": "4.28.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.3.tgz",
"integrity": "sha512-MwHPsL6qo98RC55IoWWP8/opTykjTp4JzfPu1VfO2Z0MshNP0UZ1GEV5rYSSnZSUI8VD7iHvtIPVGW5Nfh7klQ==", "integrity": "sha512-zZYl9TnrxwEPi3FbyeX0ZnE8Hp7j3OCR+ELoUfbwGHGxWnHg9+OqSmkw2MoCVpZksPCZYpQzC559Ee9pJNHTQw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/json-schema": "^7.0.7", "@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.28.2", "@typescript-eslint/scope-manager": "4.28.3",
"@typescript-eslint/types": "4.28.2", "@typescript-eslint/types": "4.28.3",
"@typescript-eslint/typescript-estree": "4.28.2", "@typescript-eslint/typescript-estree": "4.28.3",
"eslint-scope": "^5.1.1", "eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0" "eslint-utils": "^3.0.0"
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "4.28.2", "version": "4.28.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.3.tgz",
"integrity": "sha512-Q0gSCN51eikAgFGY+gnd5p9bhhCUAl0ERMiDKrTzpSoMYRubdB8MJrTTR/BBii8z+iFwz8oihxd0RAdP4l8w8w==", "integrity": "sha512-ZyWEn34bJexn/JNYvLQab0Mo5e+qqQNhknxmc8azgNd4XqspVYR5oHq9O11fLwdZMRcj4by15ghSlIEq+H5ltQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/scope-manager": "4.28.2", "@typescript-eslint/scope-manager": "4.28.3",
"@typescript-eslint/types": "4.28.2", "@typescript-eslint/types": "4.28.3",
"@typescript-eslint/typescript-estree": "4.28.2", "@typescript-eslint/typescript-estree": "4.28.3",
"debug": "^4.3.1" "debug": "^4.3.1"
}, },
"dependencies": { "dependencies": {
@@ -1781,29 +1781,29 @@
} }
}, },
"@typescript-eslint/scope-manager": { "@typescript-eslint/scope-manager": {
"version": "4.28.2", "version": "4.28.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.3.tgz",
"integrity": "sha512-MqbypNjIkJFEFuOwPWNDjq0nqXAKZvDNNs9yNseoGBB1wYfz1G0WHC2AVOy4XD7di3KCcW3+nhZyN6zruqmp2A==", "integrity": "sha512-/8lMisZ5NGIzGtJB+QizQ5eX4Xd8uxedFfMBXOKuJGP0oaBBVEMbJVddQKDXyyB0bPlmt8i6bHV89KbwOelJiQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.28.2", "@typescript-eslint/types": "4.28.3",
"@typescript-eslint/visitor-keys": "4.28.2" "@typescript-eslint/visitor-keys": "4.28.3"
} }
}, },
"@typescript-eslint/types": { "@typescript-eslint/types": {
"version": "4.28.2", "version": "4.28.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.3.tgz",
"integrity": "sha512-Gr15fuQVd93uD9zzxbApz3wf7ua3yk4ZujABZlZhaxxKY8ojo448u7XTm/+ETpy0V0dlMtj6t4VdDvdc0JmUhA==", "integrity": "sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA==",
"dev": true "dev": true
}, },
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "4.28.2", "version": "4.28.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz",
"integrity": "sha512-86lLstLvK6QjNZjMoYUBMMsULFw0hPHJlk1fzhAVoNjDBuPVxiwvGuPQq3fsBMCxuDJwmX87tM/AXoadhHRljg==", "integrity": "sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.28.2", "@typescript-eslint/types": "4.28.3",
"@typescript-eslint/visitor-keys": "4.28.2", "@typescript-eslint/visitor-keys": "4.28.3",
"debug": "^4.3.1", "debug": "^4.3.1",
"globby": "^11.0.3", "globby": "^11.0.3",
"is-glob": "^4.0.1", "is-glob": "^4.0.1",
@@ -1838,12 +1838,12 @@
} }
}, },
"@typescript-eslint/visitor-keys": { "@typescript-eslint/visitor-keys": {
"version": "4.28.2", "version": "4.28.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz",
"integrity": "sha512-aT2B4PLyyRDUVUafXzpZFoc0C9t0za4BJAKP5sgWIhG+jHECQZUEjuQSCIwZdiJJ4w4cgu5r3Kh20SOdtEBl0w==", "integrity": "sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.28.2", "@typescript-eslint/types": "4.28.3",
"eslint-visitor-keys": "^2.0.0" "eslint-visitor-keys": "^2.0.0"
} }
}, },
@@ -2051,15 +2051,48 @@
} }
}, },
"apollo-datasource-rest": { "apollo-datasource-rest": {
"version": "0.14.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/apollo-datasource-rest/-/apollo-datasource-rest-0.14.0.tgz", "resolved": "https://registry.npmjs.org/apollo-datasource-rest/-/apollo-datasource-rest-3.0.0.tgz",
"integrity": "sha512-OXh+kbpZPx0F5fNJLYmxIa8Nt+UIC7oK1pnoWtLMDws9/TiICaG6J7SHHAKnz03ZVnZ4XE9gKtm9RTCjCYJtSw==", "integrity": "sha512-pjx2uOvQSGHCE6fEeItTUniqYshL3OvD02ybwhbISg+7iQXKsyvgYKDgzY77tDjIiNSdUZsFOkNw2lmfnORBxA==",
"requires": { "requires": {
"apollo-datasource": "^0.9.0", "apollo-datasource": "^3.0.0",
"apollo-server-caching": "^0.7.0", "apollo-server-caching": "^3.0.0",
"apollo-server-env": "^3.1.0", "apollo-server-env": "^4.0.0",
"apollo-server-errors": "^2.5.0", "apollo-server-errors": "^3.0.0",
"http-cache-semantics": "^4.0.0" "http-cache-semantics": "^4.1.0"
},
"dependencies": {
"apollo-datasource": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.0.0.tgz",
"integrity": "sha512-mX+o0AQj/C4Z5M6r4g0BLRS6Ey7zxp4EvAhhLuMFB3xUnpcLxPxaq+FBLmxxo8OfE7Ay3AUe6mJgnAYIC2eqlg==",
"requires": {
"apollo-server-caching": "^3.0.0",
"apollo-server-env": "^4.0.0"
}
},
"apollo-server-caching": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-3.0.0.tgz",
"integrity": "sha512-5ziYq8qv4PBAteY4dxVgiUq4h9OyQPY9BS/xadsyoa3VNChfXu1ByVrSx6v9hUcYkZAf5eioH0eT74EG/rjSgw==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"apollo-server-env": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-4.0.0.tgz",
"integrity": "sha512-NpdtkJRZq07XrZGjjrL0joU8N1OivUNYun00gafCxzZqNg+D0YkXIU0any6brXFh+u0qIXuvYS+eKFcjiVWCJg==",
"requires": {
"node-fetch": "^2.6.1",
"util.promisify": "^1.0.1"
}
},
"apollo-server-errors": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-3.0.0.tgz",
"integrity": "sha512-fyvyn/3EN4RO4A9GL26EWOEGbqmbeOy5vgh0MK9YErfKctWWNFvH4cuCWAy30hiqYnwR1apNKDedOKEhe+/CIg=="
}
} }
}, },
"apollo-graphql": { "apollo-graphql": {
@@ -4247,9 +4280,9 @@
"dev": true "dev": true
}, },
"fast-glob": { "fast-glob": {
"version": "3.2.6", "version": "3.2.7",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.6.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
"integrity": "sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ==", "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.stat": "^2.0.2",
@@ -7786,9 +7819,9 @@
"dev": true "dev": true
}, },
"nodemon": { "nodemon": {
"version": "2.0.9", "version": "2.0.12",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.9.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz",
"integrity": "sha512-6O4k7C8f2HQArGpaPBOqGGddjzDLQAqCYmq3tKMeNIbz7Is/hOphMHy2dcY10sSq5wl3cqyn9Iz+Ep2j51JOLg==", "integrity": "sha512-egCTmNZdObdBxUBw6ZNwvZ/xzk24CKRs5K6d+5zbmrMr7rOpPmfPeF6OxM3DDpaRx331CQRFEktn+wrFFfBSOA==",
"requires": { "requires": {
"chokidar": "^3.2.2", "chokidar": "^3.2.2",
"debug": "^3.2.6", "debug": "^3.2.6",
@@ -9318,9 +9351,9 @@
} }
}, },
"testcontainers": { "testcontainers": {
"version": "7.11.1", "version": "7.12.2",
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-7.11.1.tgz", "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-7.12.2.tgz",
"integrity": "sha512-lfZeys5bLkADjOaoXfQy0V0+G8sGKr8ESANz7MhSVBwC+OTTxkP3+FVwP48bW4mwRcQ4Hojwbfw10OYT80QZmQ==", "integrity": "sha512-rp35IGJPPB6ldOjho/LP2OTSmxI5RIpmd0G1HMgm7oYgdAmpRvSLazIHkUMzG3g7tg5fJtTl7qO18KUv+bU3YQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/archiver": "^5.1.0", "@types/archiver": "^5.1.0",
@@ -9504,9 +9537,9 @@
} }
}, },
"ts-node": { "ts-node": {
"version": "10.0.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.0.0.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.1.0.tgz",
"integrity": "sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg==", "integrity": "sha512-6szn3+J9WyG2hE+5W8e0ruZrzyk1uFLYye6IGMBadnOzDh8aP7t8CbFpsfCiEx2+wMixAhjFt7lOZC4+l+WbEA==",
"requires": { "requires": {
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7", "@tsconfig/node12": "^1.0.7",
+7 -7
View File
@@ -12,7 +12,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"apollo-datasource": "^0.9.0", "apollo-datasource": "^0.9.0",
"apollo-datasource-rest": "^0.14.0", "apollo-datasource-rest": "^3.0.0",
"apollo-server": "^2.25.2", "apollo-server": "^2.25.2",
"async-redis": "^2.0.0", "async-redis": "^2.0.0",
"axios": "^0.21.1", "axios": "^0.21.1",
@@ -23,26 +23,26 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"jwk-to-pem": "^2.0.5", "jwk-to-pem": "^2.0.5",
"knex-stringcase": "^1.4.5", "knex-stringcase": "^1.4.5",
"nodemon": "^2.0.9", "nodemon": "^2.0.12",
"pg": "^8.6.0", "pg": "^8.6.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"stringcase": "^4.3.1", "stringcase": "^4.3.1",
"ts-node": "^10.0.0", "ts-node": "^10.1.0",
"typescript": "^4.3.5", "typescript": "^4.3.5",
"util": "^0.12.4", "util": "^0.12.4",
"validator": "^13.6.0" "validator": "^13.6.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^26.0.23", "@types/jest": "^26.0.24",
"@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.28.2", "@typescript-eslint/parser": "^4.28.3",
"eslint": "^7.30.0", "eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"jest": "^27.0.6", "jest": "^27.0.6",
"testcontainers": "^7.11.1", "testcontainers": "^7.12.2",
"ts-jest": "^27.0.3" "ts-jest": "^27.0.3"
} }
} }
+151 -48
View File
@@ -1,32 +1,29 @@
{ {
products(limit: 10000, browsable: true, visible: true) { products(limit: 10000, browsable: true, visible: true) {
id id
keywords {
id
value
type
}
type
fields_json
inserted inserted
path
updated updated
visible visible
browsable browsable
printProducts { categories {
group
id id
updated name
path
pathNumeric
isLeaf
depth
childCount
lft
rgt
} }
blacklisting {
group
id
marketId
updated
}
}
}
{
product(id: 42280) {
id
inserted
updated
visible
browsable
designerId
designer { designer {
id id
name name
@@ -46,37 +43,78 @@
} }
} }
# Orders
{ {
orders( orders(
filter: { limit: 1, dates: { from: "2021-03-01", to: "2021-04-30" } } filter: {
) { limit: 100
id offset: 0
market { dates: { from: "2021-03-01", to: "2021-04-30" }
id
name
vat
currency
priceAdjustment
} }
rows { ) {
total
offset
limit
items {
id id
order { paid
delivered
confirmed
billingInformation {
email
phone
address {
firstname
lastname
recipientName
companyname
address1
address2
zipcode
stateCountyOrRegion
}
}
deliveryInformation {
email
phone
address {
firstname
lastname
recipientName
companyname
address1
address2
zipcode
stateCountyOrRegion
}
}
market {
id id
name
vat
currency
priceAdjustment
}
rows {
id
order {
id
}
inserted
orderId
productId
productGroup
artNo
path
name
price
designerId
commissionAmount
status
pwintyImageId
frameColor
pwintySku
} }
inserted
orderId
productId
productGroup
artNo
path
name
price
designerId
commissionAmount
status
pwintyImageId
frameColor
pwintySku
} }
} }
} }
@@ -154,14 +192,51 @@
# JULY 2021 # JULY 2021
{ {
product(id: 73803) { product(id: 42166) {
id id
stockid
fields_json
orientation
related {
id
fields {
name
}
}
ref1
ref2
ref3
wallpaperTypes
keywords { keywords {
id id
value value
type type
} }
fields_json type
fields {
artNo
name
height
width
photowallResolution
canvasResolution
wallpaperResolution
copyright
proportionsWarning
marginWidthMax
marginWidthMin
marginHeightMax
marginHeightMin
focusXpoint
focusYpoint
focusXpoint2
focusYpoint2
batch
imageResolution
printFileWidth
printFileHeight
printFileDpi
}
inserted inserted
path path
updated updated
@@ -171,6 +246,7 @@
id id
name name
path path
pathNumeric
isLeaf isLeaf
depth depth
childCount childCount
@@ -186,6 +262,14 @@
group group
id id
updated updated
inserted
interiors {
id
roomName
uri
position
roomType
}
} }
blacklisting { blacklisting {
group group
@@ -195,7 +279,6 @@
} }
} }
} }
# Categories # Categories
{ {
category(id: 1653) { category(id: 1653) {
@@ -236,3 +319,23 @@
} }
} }
} }
{
markets {
id
name
vat
currency
priceAdjustment
}
}
{
market(name: "GB") {
id
name
vat
currency
priceAdjustment
}
}
+1 -5
View File
@@ -140,7 +140,7 @@ describe('GraphQL Apollo tests', () => {
it('should be possible to fetch a designers with limit', async () => { it('should be possible to fetch a designers with limit', async () => {
// CONTROL // CONTROL
const db_response = await allContainers.db.raw( const db_response = await allContainers.db.raw(
`SELECT * FROM designers LIMIT 3;`, `SELECT * FROM designers ORDER BY name LIMIT 3;`,
); );
const controlData = db_response.rows; const controlData = db_response.rows;
// ------------------- // -------------------
@@ -186,10 +186,6 @@ describe('GraphQL Apollo tests', () => {
expect(designers.length).toBe(3); expect(designers.length).toBe(3);
expect(designers[0].id).toBe('1');
expect(designers[1].id).toBe('2');
expect(designers[2].id).toBe('3');
expect(designers[0].name).toBe(controlData[0].name); expect(designers[0].name).toBe(controlData[0].name);
expect(designers[1].name).toBe(controlData[1].name); expect(designers[1].name).toBe(controlData[1].name);
expect(designers[2].name).toBe(controlData[2].name); expect(designers[2].name).toBe(controlData[2].name);
-20
View File
@@ -1,20 +0,0 @@
import { RESTDataSource } from 'apollo-datasource-rest';
export class Api1DataSource extends RESTDataSource {
authHeader: string;
constructor() {
super();
this.baseURL = 'http://docker.for.mac.localhost:8082';
this.authHeader = 'Basic ZGV2OmRldg==';
}
willSendRequest(request) {
request.headers.set('Authorization', this.authHeader);
}
async getProduct(id) {
return await this.get(`/products/${id}?` + new URLSearchParams({}), null, {
cacheOptions: { ttl: 5 },
});
}
}
-41
View File
@@ -1,41 +0,0 @@
import { RESTDataSource } from 'apollo-datasource-rest';
/*
https://wE4D45790.api.esales.apptus.cloud/api/v2/panels/category
?esales.market=SE&
esales.customerKey=471c09ac-a775-4dd2-8bfc-041bfbb6d7b7&
esales.sessionKey=b106299c-eac0-4f6f-96d2-93e57c255784&
window_first=1&
window_last=10&
selected_category=categories_SE:'root/animals'&
root_category=categories_SE:'root'
&max_facets=50
&filter=blacklisted:'0' AND visible_in_listing:'1' AND visible_in_shop:'1' AND (group:'poster' OR group:'framed-print')
blacklisted:'0'%20AND%20visible_in_listing:'1'%20AND%20visible_in_shop:'1'%20AND%20(group:'poster'%20OR%20group:'framed-print')
*/
export class Api2DataSource extends RESTDataSource {
authHeader: string;
constructor() {
super();
this.baseURL = 'https://api-staging.photowall.com/';
this.authHeader =
'Basic ZGExNzU5ZWEtOTBiNC00OTNjLWJjYWItNjAwNDg1NjA5YjkyOg==';
}
willSendRequest(request) {
request.headers.set('Authorization', this.authHeader);
}
async getPrice(market, width, height, sku) {
return await this.get(
'prices/wallpaper?' +
new URLSearchParams({
width: '200',
height: '200',
market: '1',
product: '58941',
}),
);
}
}
+14 -23
View File
@@ -21,7 +21,7 @@ WHERE product_category.product_id = ?
data.rows.map((row) => { data.rows.map((row) => {
return { return {
...row, ...row,
path: row.path.replace(/^root/, ''), pathNumeric: row.path_numeric,
}; };
}), }),
); );
@@ -29,33 +29,16 @@ WHERE product_category.product_id = ?
} }
async getCategories(): Promise<Array<Category>> { async getCategories(): Promise<Array<Category>> {
return await this.knex return this.knex.select('*').from('v_categorytree').cache(MINUTE);
.select('*')
.from('v_categorytree')
.cache(MINUTE)
.then((rows) => {
return rows.map((row) => {
return {
...row,
path: row.path.replace(/^root/, ''),
};
});
});
} }
async getCategoryById(id: number): Promise<Category> { async getCategoryById(id: number): Promise<Category> {
return await this.knex return this.knex
.select('*') .select('*')
.from('v_categorytree') .from('v_categorytree')
.where('id', id) .where('id', id)
.first() .first()
.cache(MINUTE) .cache(MINUTE);
.then((row) => {
return {
...row,
path: row.path.replace(/^root/, ''),
};
});
} }
/** /**
@@ -65,10 +48,18 @@ WHERE product_category.product_id = ?
SELECT category_keyword.category_id, keywords.value SELECT category_keyword.category_id, keywords.value
FROM category_keyword FROM category_keyword
JOIN keywords ON category_keyword.keyword_id = keywords.id; JOIN keywords ON category_keyword.keyword_id = keywords.id;
FROM categorydata cd FROM categorydata cd
JOIN categorydatakeys cdk ON cd.datakey_id = cdk.id JOIN categorydatakeys cdk ON cd.datakey_id = cdk.id
* *
* category texts
SELECT name,
text
FROM categorydata cd
JOIN categorydatakeys cdk ON cd.datakey_id = cdk.id
WHERE category_id = ?
AND locale_id = ?;
* category teaser images???
*/ */
} }
+4 -3
View File
@@ -9,7 +9,7 @@ export class DesignerAPI extends BaseSQLDataSource {
} }
async getDesignerById(id: number): Promise<Designer> { async getDesignerById(id: number): Promise<Designer> {
return await this.knex return this.knex
.select('*') .select('*')
.from('designers') .from('designers')
.where('designerid', id) .where('designerid', id)
@@ -24,11 +24,12 @@ export class DesignerAPI extends BaseSQLDataSource {
} }
async getDesigners(limit: Maybe<number>): Promise<Array<Designer>> { async getDesigners(limit: Maybe<number>): Promise<Array<Designer>> {
limit = limit ?? 100; limit = limit ?? 5000;
return await this.knex return this.knex
.select('*') .select('*')
.from('designers') .from('designers')
.limit(limit) .limit(limit)
.orderBy('name')
.cache(MINUTE) .cache(MINUTE)
.then((rows) => { .then((rows) => {
return rows.map((row) => { return rows.map((row) => {
+29
View File
@@ -0,0 +1,29 @@
import { RESTDataSource } from 'apollo-datasource-rest';
import { InteriorType } from '../types/interior-types';
import { Orientation } from '../types/product-types';
export class ImageServerApi extends RESTDataSource {
constructor() {
super();
this.baseURL =
process.env.ENVIRONMENT_NAME === 'production'
? 'http://images.photowall.com'
: 'http://images-dev.photowall.com';
}
willSendRequest(request) {
request.headers.set('X-Photowall', 'pele');
}
async getInteriorsForProduct(
productId: number,
type: InteriorType,
orientation: Orientation,
): Promise<Array<{ uri: string }>> {
const it = type.toString().toLowerCase();
const or = orientation.toString().toLowerCase();
return this.get(`/rooms/${productId}/${or}/${it}/`, null, {
cacheOptions: { ttl: 5 },
});
}
}
+122
View File
@@ -0,0 +1,122 @@
import {
Interior,
InteriorsFilter,
InteriorType,
} from '../types/interior-types';
import { Orientation } from '../types/product-types';
import { Maybe } from '../types/types';
import { BaseSQLDataSource } from './BaseSQLDataSource';
import { ImageServerApi } from './imageserver-api';
import { convertToPossibleType } from './utils';
const MINUTE = 60;
export class InteriorAPI extends BaseSQLDataSource {
imageServerApi: ImageServerApi;
constructor(config, imageServerApi: ImageServerApi) {
super(config);
this.imageServerApi = imageServerApi;
}
getInteriorType(roomTypePart: string): string {
switch (roomTypePart) {
case 'wallpaper':
return InteriorType[InteriorType.WALLPAPER];
case 'painting':
return InteriorType[InteriorType.PAINTING];
case 'poster':
return InteriorType[InteriorType.POSTER];
case 'framed-print':
return InteriorType[InteriorType.FRAMED_PRINT];
default:
throw new Error(`Unknown room type: ${roomTypePart}`);
}
}
getInteriorOrientation(roomOrientationPart: string): string {
switch (roomOrientationPart) {
case 'landscape':
return Orientation[Orientation.LANDSCAPE];
case 'standing':
return Orientation[Orientation.PORTRAIT];
case 'square':
return Orientation[Orientation.SQUARE];
default:
throw new Error(`Unknown room orientation: ${roomOrientationPart}`);
}
}
getInteriorRoomNumber(roomNumberPart: string): number {
return convertToPossibleType(roomNumberPart.slice(4));
}
decorateRow(row: any): Interior {
const parts = row.roomName.split('_');
row.roomNumber = this.getInteriorRoomNumber(parts[0]);
row.type = this.getInteriorType(parts[1]);
row.orientation = this.getInteriorOrientation(parts[2]);
return row;
}
async getInteriors(filter: Maybe<InteriorsFilter>): Promise<Array<Interior>> {
const sql = /* sql */ `
SELECT rooms.*, room_types.name roomType FROM rooms
LEFT JOIN room_types ON rooms.room_type_id = room_types.id
`;
const allInteriors = await this.cachedRaw(sql)
.cache(MINUTE * 60)
.then((data) =>
data.rows.map((row) => {
const res = {
...row,
roomName: row.name,
roomType: row.roomtype,
};
this.decorateRow(res);
return res;
}),
);
// Based on filter get valid interiors and filter out any else from all interiors
if (filter) {
const validUris = await this.imageServerApi.getInteriorsForProduct(
filter.productId,
filter.type,
filter.orientation,
);
return allInteriors.filter((i: Interior) => {
const it = i.type.toString().toLowerCase();
const or = i.orientation.toString().toLocaleLowerCase();
const uriMatch = `${or}/${it}/room${i.roomNumber}.`;
const foundUri = validUris.find(({ uri }) => uri.includes(uriMatch));
if (foundUri) {
i.uri = foundUri.uri;
return true;
}
return false;
});
}
// No filter so return all
return allInteriors;
}
async getPrintProductInteriors(printId: number): Promise<Array<Interior>> {
return this.knex
.select('*')
.from('v_interior_image_urls')
.orderBy('position')
.where('print_id', printId)
.cache(MINUTE)
.then((rows) => {
return rows.map((row) => {
const res = {
...row,
id: row.roomId,
};
this.decorateRow(res);
return res;
});
});
}
}
+3 -6
View File
@@ -28,10 +28,9 @@ WHERE product_keyword.product_id = ?
ORDER BY keywords.value ORDER BY keywords.value
`; `;
const res = await this.knex return this.knex
.raw(query, productId) .raw(query, productId)
.then((data) => data.rows.map((row) => this.getType(row))); .then((data) => data.rows.map((row) => this.getType(row)));
return res;
} }
async getKeywords(): Promise<Array<Keyword>> { async getKeywords(): Promise<Array<Keyword>> {
@@ -40,14 +39,13 @@ SELECT keywords.*, keyword_type.type_id FROM keywords
LEFT JOIN keyword_type ON keyword_type.keyword_id = keywords.id LEFT JOIN keyword_type ON keyword_type.keyword_id = keywords.id
ORDER BY keywords.value ORDER BY keywords.value
`; `;
const res = await this.knex return this.knex
.raw(query) .raw(query)
.then((data) => data.rows.map((row) => this.getType(row))); .then((data) => data.rows.map((row) => this.getType(row)));
return res;
} }
async getKeywordById(id: number): Promise<Keyword> { async getKeywordById(id: number): Promise<Keyword> {
const res = await this.knex return this.knex
.select('*') .select('*')
.from('keywords') .from('keywords')
.leftJoin('keyword_type', 'keyword_type.keyword_id', 'keywords.id') .leftJoin('keyword_type', 'keyword_type.keyword_id', 'keywords.id')
@@ -55,7 +53,6 @@ ORDER BY keywords.value
.first() .first()
.cache(MINUTE) .cache(MINUTE)
.then((row) => this.getType(row)); .then((row) => this.getType(row));
return res;
} }
/** /**
+35
View File
@@ -0,0 +1,35 @@
import { SQLDataSource } from 'datasource-sql';
import { Market } from '../types/market-types';
const MINUTE = 60;
export class MarketAPI extends SQLDataSource {
constructor(config) {
super(config);
}
async getMarketById(id: number): Promise<Market> {
return this.knex
.select('*')
.from('markets')
.where('id', id)
.first()
.cache(MINUTE * 5);
}
async getMarketByName(name: string): Promise<Market> {
return this.knex
.select('*')
.from('markets')
.where('name', name)
.first()
.cache(MINUTE * 5);
}
async getMarkets(): Promise<Array<Market>> {
return this.knex
.select('*')
.from('markets')
.cache(MINUTE * 5);
}
}
+11 -37
View File
@@ -3,7 +3,6 @@ import { camelcase } from 'stringcase';
import { import {
Address, Address,
ContactInformation, ContactInformation,
Market,
Order, Order,
OrderRow, OrderRow,
} from '../types/order-types'; } from '../types/order-types';
@@ -17,24 +16,6 @@ export class OrderAPI extends BaseSQLDataSource {
super(config); super(config);
} }
async getMarketById(id: number): Promise<Market> {
return await this.knex
.select('*')
.from('markets')
.where('id', id)
.first()
.cache(MINUTE);
}
async getMarketByName(name: string): Promise<Market> {
return await this.knex
.select('*')
.from('markets')
.where('name', name)
.first()
.cache(MINUTE);
}
async getAddress(addressId: number): Promise<Maybe<Address>> { async getAddress(addressId: number): Promise<Maybe<Address>> {
const row = await this.knex const row = await this.knex
.select('*') .select('*')
@@ -80,20 +61,13 @@ export class OrderAPI extends BaseSQLDataSource {
return row; return row;
} }
async getOrdersTotal(input: Maybe<GeneralInput>): Promise<Number> { async getOrdersTotal(input: Maybe<GeneralInput>): Promise<number> {
let query = this.knex.table('orders'); const res = await this.getOrdersQuery(input)
.clone()
if (input?.dates?.from) { .count()
query = query.where('inserted', '>=', input.dates.from); .first()
} .cache(MINUTE * 60);
return convertToPossibleType(res['count']); // Optimize later to return Promise
if (input?.dates?.to) {
query = query.where('inserted', '<=', input.dates.to);
}
const res = await query.clone().count();
console.log(res);
return 57777;
} }
async getOrders(input: Maybe<GeneralInput>): Promise<Array<Order>> { async getOrders(input: Maybe<GeneralInput>): Promise<Array<Order>> {
@@ -103,7 +77,7 @@ export class OrderAPI extends BaseSQLDataSource {
query = query.limit(input?.limit); query = query.limit(input?.limit);
} }
query = query.orderBy('inserted', 'ASC'); query = query.orderBy('inserted', 'ASC');
return await query return query
.cache(MINUTE) .cache(MINUTE)
.then((rows) => rows.map((row) => this.createOrderFromRow(row))); .then((rows) => rows.map((row) => this.createOrderFromRow(row)));
} }
@@ -122,7 +96,7 @@ export class OrderAPI extends BaseSQLDataSource {
} }
async getOrderById(id: number): Promise<Order> { async getOrderById(id: number): Promise<Order> {
return await this.knex return this.knex
.select('*') .select('*')
.from('orders') .from('orders')
.where('id', id) .where('id', id)
@@ -200,7 +174,7 @@ export class OrderAPI extends BaseSQLDataSource {
.raw('SELECT * FROM "order-rows" WHERE orderid = ?', orderId) .raw('SELECT * FROM "order-rows" WHERE orderid = ?', orderId)
.then((data) => data.rows); .then((data) => data.rows);
return await this.getOrderRows(rows); return this.getOrderRows(rows);
} }
async getOrderRowsByDesignerId( async getOrderRowsByDesignerId(
@@ -228,6 +202,6 @@ export class OrderAPI extends BaseSQLDataSource {
const rows = await this.knex const rows = await this.knex
.raw(query, [designerId, this.getSQLDate(from), this.getSQLDate(to)]) .raw(query, [designerId, this.getSQLDate(from), this.getSQLDate(to)])
.then((data) => data.rows); .then((data) => data.rows);
return await this.getOrderRows(rows); return this.getOrderRows(rows);
} }
} }
+61 -17
View File
@@ -1,4 +1,5 @@
import { SQLDataSource } from 'datasource-sql'; import { BaseSQLDataSource } from './BaseSQLDataSource';
import { camelcase } from 'stringcase';
import * as sql from './sql'; import * as sql from './sql';
import { import {
Product, Product,
@@ -6,12 +7,16 @@ import {
PrintProduct, PrintProduct,
ProductType, ProductType,
ProductBlacklist, ProductBlacklist,
ProductWallpaperType,
ProductFields,
Orientation,
} from '../types/product-types'; } from '../types/product-types';
import { Maybe } from '../types/types'; import { Maybe } from '../types/types';
import { convertToPossibleType } from './utils';
const MINUTE = 60; const MINUTE = 60;
export class ProductAPI extends SQLDataSource { export class ProductAPI extends BaseSQLDataSource {
constructor(config) { constructor(config) {
super(config); super(config);
} }
@@ -21,6 +26,25 @@ export class ProductAPI extends SQLDataSource {
// SELECT materialid, material, (price / 100::float) as price FROM "product-materials" // SELECT materialid, material, (price / 100::float) as price FROM "product-materials"
// } // }
getWallpaperTypes(row: any): Array<ProductWallpaperType> {
return row.wallpapertypes?.map((t: number) => ProductWallpaperType[t]);
}
getProductTypes(row: any): Array<ProductType> {
if (!row.types) {
return [];
}
return row.types.map((type: any) => {
if (type === 'typeillustration') {
return ProductType[ProductType.ILLUSTRATION];
}
if (type === 'typephotography') {
return ProductType[ProductType.PHOTO];
}
return ProductType[ProductType.UNKNOWN];
});
}
getBlacklisting(row: any): Array<ProductBlacklist> { getBlacklisting(row: any): Array<ProductBlacklist> {
if (!row.blacklisting) { if (!row.blacklisting) {
return []; return [];
@@ -42,26 +66,36 @@ export class ProductAPI extends SQLDataSource {
}); });
} }
getProductTypes(row: any): Array<ProductType> { getProductFields(row: any): ProductFields {
if (!row.types) { const fields = Object.keys(row.fields).reduce((mem, key) => {
return []; mem[camelcase(key)] = convertToPossibleType(row.fields[key]);
return mem;
}, {});
return <ProductFields>fields;
}
getOrientation(fields: ProductFields): string {
if (!fields.width || !fields.height) {
return Orientation[Orientation.UNKNOWN];
} }
return row.types.map((type: any) => { if (fields.width > fields.height) {
if (type === 'typeillustration') { return Orientation[Orientation.LANDSCAPE];
return ProductType[ProductType.ILLUSTRATION]; }
} if (fields.width < fields.height) {
if (type === 'typephotography') { return Orientation[Orientation.PORTRAIT];
return ProductType[ProductType.PHOTO]; }
} return Orientation[Orientation.SQUARE];
return ProductType[ProductType.UNKNOWN];
});
} }
createProductFromRow(row: any) { createProductFromRow(row: any) {
row.blacklisting = this.getBlacklisting(row); row.blacklisting = this.getBlacklisting(row);
row.printProducts = this.getPrintProducts(row); row.printProducts = this.getPrintProducts(row);
row.type = this.getProductTypes(row); row.type = this.getProductTypes(row);
row.wallpaperTypes = this.getWallpaperTypes(row);
row.fields = this.getProductFields(row);
row.designerId = row.designerid; row.designerId = row.designerid;
row.orientation = this.getOrientation(row.fields);
row.fields_json = row.fields; row.fields_json = row.fields;
return row; return row;
} }
@@ -74,7 +108,7 @@ export class ProductAPI extends SQLDataSource {
limit = limit ?? 100; limit = limit ?? 100;
const query = sql.products(limit, visible, browsable); const query = sql.products(limit, visible, browsable);
return await this.knex return this.knex
.raw(query) .raw(query)
.then((data) => data.rows.map((row) => this.createProductFromRow(row))); .then((data) => data.rows.map((row) => this.createProductFromRow(row)));
} }
@@ -94,7 +128,7 @@ export class ProductAPI extends SQLDataSource {
async getCategoryProducts(categoryId: number): Promise<Array<Product>> { async getCategoryProducts(categoryId: number): Promise<Array<Product>> {
const query = sql.categoryProducts(categoryId); const query = sql.categoryProducts(categoryId);
return await this.knex return this.knex
.raw(query) .raw(query)
.then((data) => data.rows.map((row) => this.createProductFromRow(row))); .then((data) => data.rows.map((row) => this.createProductFromRow(row)));
} }
@@ -102,8 +136,18 @@ export class ProductAPI extends SQLDataSource {
async getKeywordProducts(keywordId: number): Promise<Array<Product>> { async getKeywordProducts(keywordId: number): Promise<Array<Product>> {
const query = sql.keywordProducts(keywordId); const query = sql.keywordProducts(keywordId);
return await this.knex return this.knex
.raw(query) .raw(query)
.then((data) => data.rows.map((row) => this.createProductFromRow(row))); .then((data) => data.rows.map((row) => this.createProductFromRow(row)));
} }
async getRelatedProducts(id: number): Promise<Array<Product>> {
const ids = await this.cachedRaw(
`SELECT productid2 FROM "product-products_products" WHERE productid1 = ${id}`,
)
.cache(MINUTE * 60)
.then((data) => data.rows.map((r) => r.productid2));
return ids.map(async (id) => this.getProduct(id));
}
} }
+54 -36
View File
@@ -4,57 +4,75 @@ import { Maybe } from '../../types/types';
const baseQuery = /* sql */ ` const baseQuery = /* sql */ `
SELECT products.productid as id, SELECT products.productid AS id,
stock.stockid,
products.path, products.path,
products.visible, products.visible,
products.browsable, products.browsable,
products.designerid, products.designerid,
products.inserted, products.inserted,
products.updated, products.updated,
products.ref1,
products.ref2,
products.ref3,
( (
SELECT json_agg( SELECT json_agg(
json_build_object( json_build_object(
'printId', 'printId',
"product-printproducts".printid, "product-printproducts".printid,
'productId', 'productId',
"product-printproducts".productid, "product-printproducts".productid,
'groupId', 'groupId',
"product-printproducts".groupid, "product-printproducts".groupid,
'inserted', 'inserted',
"product-printproducts".inserted, "product-printproducts".inserted,
'updated', 'updated',
"product-printproducts".updated "product-printproducts".updated
)
) )
) FROM "product-printproducts" WHERE "product-printproducts".productid = products.productid FROM "product-printproducts"
WHERE "product-printproducts".productid = products.productid
) AS printproducts, ) AS printproducts,
( SELECT json_agg( (
json_build_object( SELECT json_agg(
'id', json_build_object(
product_blacklist.id, 'id',
'productId', product_blacklist.id,
product_blacklist.product_id, 'productId',
'marketId', product_blacklist.product_id,
product_blacklist.market_id, 'marketId',
'groupId', product_blacklist.market_id,
product_blacklist.group_id, 'groupId',
'inserted', product_blacklist.group_id,
product_blacklist.created_at, 'inserted',
'updated', product_blacklist.created_at,
product_blacklist.updated_at 'updated',
product_blacklist.updated_at
)
) )
) FROM product_blacklist WHERE product_blacklist.product_id = products.productid FROM product_blacklist
WHERE product_blacklist.product_id = products.productid
) AS blacklisting, ) AS blacklisting,
( SELECT json_object_agg(fields.field, pf.value) (
SELECT json_object_agg(fields.field, pf.value)
FROM "product-products_fields" pf FROM "product-products_fields" pf
JOIN "product-fields" fields ON fields.fieldid = pf.fieldid JOIN "product-fields" fields ON fields.fieldid = pf.fieldid
WHERE pf.productid = products.productid WHERE pf.productid = products.productid
) AS fields, ) AS fields,
( SELECT json_agg(keywords.value) (
SELECT json_agg(keywords.value)
FROM product_keyword pk FROM product_keyword pk
JOIN keywords ON keywords.id = pk.keyword_id JOIN keywords ON keywords.id = pk.keyword_id
WHERE keywords.id IN (1103,1104)AND pk.product_id = products.productid WHERE keywords.id IN (1103, 1104)
) AS types AND pk.product_id = products.productid
FROM "product-products" products ) AS types,
(
SELECT json_agg(wt.wallpapertype_id)
FROM product_wallpapertypes wt
WHERE wt.product_id = products.productid
) AS wallpapertypes
FROM "product-products" products
LEFT JOIN "product-stockproducts" stock ON stock.productid = products.productid
`; `;
export function products( export function products(
+14 -12
View File
@@ -9,33 +9,34 @@ import resolvers from './resolvers';
import { OrderAPI } from './datasources/order-api'; import { OrderAPI } from './datasources/order-api';
import { ProductAPI } from './datasources/product-api'; import { ProductAPI } from './datasources/product-api';
import { DesignerAPI } from './datasources/designer-api'; import { DesignerAPI } from './datasources/designer-api';
import { Api2DataSource } from './datasources/api2-datasource';
import { Api1DataSource } from './datasources/api1-datasource';
import { verifyToken } from './cognito/cognito-client';
import { CategoryAPI } from './datasources/category-api'; import { CategoryAPI } from './datasources/category-api';
import { KeywordAPI } from './datasources/keyword-api'; import { KeywordAPI } from './datasources/keyword-api';
import { MarketAPI } from './datasources/market-api';
import { ImageServerApi } from './datasources/imageserver-api';
import { InteriorAPI } from './datasources/interior-api';
console.log(`dbConfig`, dbConfig); import { verifyToken } from './cognito/cognito-client';
// Should we convert columns? // Should we convert columns?
const knexConfig = knexStringcase(dbConfig); const knexConfig = knexStringcase(dbConfig);
// set up any dataSources our resolvers need // set up any dataSources our resolvers need
const api1 = new Api1DataSource(); const categoryApi = new CategoryAPI(knexConfig);
const api2 = new Api2DataSource(); const designerApi = new DesignerAPI(knexConfig);
const imageServerApi = new ImageServerApi();
const interiorApi = new InteriorAPI(knexConfig, imageServerApi);
const keywordApi = new KeywordAPI(knexConfig);
const marketApi = new MarketAPI(knexConfig);
const orderApi = new OrderAPI(knexConfig); const orderApi = new OrderAPI(knexConfig);
const productApi = new ProductAPI(knexConfig); const productApi = new ProductAPI(knexConfig);
const categoryApi = new CategoryAPI(knexConfig);
const keywordApi = new KeywordAPI(knexConfig);
const designerApi = new DesignerAPI(knexConfig);
const dataSources = () => ({ const dataSources = () => ({
api1,
api2,
categoryApi, categoryApi,
designerApi, designerApi,
interiorApi,
imageServerApi,
keywordApi, keywordApi,
marketApi,
orderApi, orderApi,
productApi, productApi,
}); });
@@ -51,6 +52,7 @@ const context = async ({ req }) => {
const res = await verifyToken({ token }); const res = await verifyToken({ token });
return { auth: res }; return { auth: res };
} else if (authHeader == 'Basic ZGV2OmRldg==') { } else if (authHeader == 'Basic ZGV2OmRldg==') {
// dev:dev remove later
return { auth: 'during development' }; return { auth: 'during development' };
} }
throw new AuthenticationError('Not authorized'); throw new AuthenticationError('Not authorized');
+1 -1
View File
@@ -16,7 +16,7 @@ async function getDesigners(_, { limit }, { dataSources }) {
return (<DesignerAPI>dataSources.designerApi).getDesigners(limit); return (<DesignerAPI>dataSources.designerApi).getDesigners(limit);
} }
async function getDesigner(_, { id }, { dataSources }) { async function getDesigner(parent, { id }, { dataSources }) {
return (<DesignerAPI>dataSources.designerApi).getDesignerById(id); return (<DesignerAPI>dataSources.designerApi).getDesignerById(id);
} }
+16 -2
View File
@@ -1,9 +1,16 @@
import { orderQueryTypeDefs, orderTypeDefs } from './orders-resolver'; import { orderQueryTypeDefs, orderTypeDefs } from './orders-resolver';
import { productQueryTypeDefs, productTypeDefs } from './products-resolver'; import {
productQueryTypeDefs,
productTypeDefs,
productMutationTypeDefs,
} from './products-resolver';
import { designerQueryTypeDefs, designerTypeDefs } from './designers-resolver'; import { designerQueryTypeDefs, designerTypeDefs } from './designers-resolver';
import { categoryQueryTypeDefs, categoryTypeDefs } from './categories-resolver'; import { categoryQueryTypeDefs, categoryTypeDefs } from './categories-resolver';
import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars';
import { keywordsQueryTypeDefs, keywordTypeDefs } from './keywords-resolver'; import { keywordsQueryTypeDefs, keywordTypeDefs } from './keywords-resolver';
import { marketsQueryTypeDefs, marketTypeDefs } from './markets-resolver';
import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars';
import { interiorsQueryTypeDefs, interiorTypeDefs } from './interiors-resolver';
const resolvers = { const resolvers = {
Query: { Query: {
...orderQueryTypeDefs, ...orderQueryTypeDefs,
@@ -11,12 +18,19 @@ const resolvers = {
...designerQueryTypeDefs, ...designerQueryTypeDefs,
...categoryQueryTypeDefs, ...categoryQueryTypeDefs,
...keywordsQueryTypeDefs, ...keywordsQueryTypeDefs,
...marketsQueryTypeDefs,
...interiorsQueryTypeDefs,
},
Mutation: {
...productMutationTypeDefs,
}, },
...orderTypeDefs, ...orderTypeDefs,
...productTypeDefs, ...productTypeDefs,
...designerTypeDefs, ...designerTypeDefs,
...categoryTypeDefs, ...categoryTypeDefs,
...keywordTypeDefs, ...keywordTypeDefs,
...marketTypeDefs,
...interiorTypeDefs,
DateTime: DateTimeResolver, DateTime: DateTimeResolver,
Date: DateResolver, Date: DateResolver,
JSON: JSONResolver, JSON: JSONResolver,
+15
View File
@@ -0,0 +1,15 @@
import { IResolverObject } from 'graphql-tools';
import { InteriorAPI } from '../datasources/interior-api';
import { InteriorsFilterInput } from '../types/interior-types';
const Interior: IResolverObject = {};
async function getInteriors(_, args: InteriorsFilterInput, { dataSources }) {
return (<InteriorAPI>dataSources.interiorApi).getInteriors(args.filter);
}
export const interiorTypeDefs = { Interior };
export const interiorsQueryTypeDefs = {
interiors: getInteriors,
};
+19
View File
@@ -0,0 +1,19 @@
import { IResolverObject } from 'graphql-tools';
import { MarketAPI } from '../datasources/market-api';
const Market: IResolverObject = {};
async function getMarkets(_, _args, { dataSources }) {
return (<MarketAPI>dataSources.marketApi).getMarkets();
}
async function getMarket(_, { name }, { dataSources }) {
return (<MarketAPI>dataSources.marketApi).getMarketByName(name);
}
export const marketTypeDefs = { Market };
export const marketsQueryTypeDefs = {
markets: getMarkets,
market: getMarket,
};
+2 -1
View File
@@ -1,4 +1,5 @@
import { IResolverObject } from 'graphql-tools'; import { IResolverObject } from 'graphql-tools';
import { MarketAPI } from '../datasources/market-api';
import { OrderAPI } from '../datasources/order-api'; import { OrderAPI } from '../datasources/order-api';
import { FilterInput } from '../types/types'; import { FilterInput } from '../types/types';
@@ -14,7 +15,7 @@ const Order: IResolverObject = {
return (<OrderAPI>dataSources.orderApi).getOrderRowsByOrderId(id); return (<OrderAPI>dataSources.orderApi).getOrderRowsByOrderId(id);
}, },
async market({ market }, _args, { dataSources }) { async market({ market }, _args, { dataSources }) {
return (<OrderAPI>dataSources.orderApi).getMarketByName(market); return (<MarketAPI>dataSources.marketApi).getMarketByName(market);
}, },
}; };
+18 -12
View File
@@ -1,16 +1,16 @@
import { IResolverObject } from 'graphql-tools'; import { IResolverObject } from 'graphql-tools';
import { Api2DataSource } from '../datasources/api2-datasource';
import { OrderAPI } from '../datasources/order-api';
import { ProductAPI } from '../datasources/product-api'; import { ProductAPI } from '../datasources/product-api';
import { Product } from '../types/product-types'; import { Product } from '../types/product-types';
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';
import { Keyword } from '../types/keyword-types'; import { Keyword } from '../types/keyword-types';
import { MarketAPI } from '../datasources/market-api';
import { InteriorAPI } from '../datasources/interior-api';
const ProductBlacklist: IResolverObject = { const ProductBlacklist: IResolverObject = {
async market(parent, _args, { dataSources }) { async market(parent, _args, { dataSources }) {
return (<OrderAPI>dataSources.orderApi).getMarketById(parent.marketId); return (<MarketAPI>dataSources.marketApi).getMarketById(parent.marketId);
}, },
}; };
@@ -20,24 +20,22 @@ const Product: IResolverObject = {
return (<CategoryAPI>dataSources.categoryApi).getProductCategories(id); return (<CategoryAPI>dataSources.categoryApi).getProductCategories(id);
}, },
async designer({ designerId }, args, { dataSources }) { async designer({ designerId }, args, { dataSources }) {
if (!designerId) {
return null;
}
return dataSources.designerApi.getDesignerById(designerId); return dataSources.designerApi.getDesignerById(designerId);
}, },
async keywords({ id }, _args, { dataSources }): Promise<Array<Keyword>> { async keywords({ id }, _args, { dataSources }): Promise<Array<Keyword>> {
return (<KeywordAPI>dataSources.keywordApi).getProductKeywords(id); return (<KeywordAPI>dataSources.keywordApi).getProductKeywords(id);
}, },
async api1json({ id }, _args, { dataSources }): Promise<JSON> { async related({ id }, _args, { dataSources }) {
return dataSources.api1.getProduct(id); return (<ProductAPI>dataSources.productApi).getRelatedProducts(id);
}, },
}; };
const PrintProduct: IResolverObject = { const PrintProduct: IResolverObject = {
async price({ id }, { market, width, height, sku }, { dataSources }) { async interiors({ id }, _args, { dataSources }) {
return (<Api2DataSource>dataSources.api2).getPrice( return (<InteriorAPI>dataSources.interiorApi).getPrintProductInteriors(id);
market,
width,
height,
sku,
);
}, },
}; };
@@ -67,3 +65,11 @@ export const productQueryTypeDefs = {
products: getProducts, products: getProducts,
product: getProduct, product: getProduct,
}; };
// Mutations below
export const productMutationTypeDefs = {
addKeyword(_, { productId, keywordId }, { dataSources }) {
return { result: 'OK' };
},
};
+94 -8
View File
@@ -19,6 +19,26 @@ const typeDefs = gql`
keyword(id: Int!): Keyword keyword(id: Int!): Keyword
designers(limit: Int): [Designer] designers(limit: Int): [Designer]
designer(id: Int!): Designer designer(id: Int!): Designer
markets: [Market]
market(name: String!): Market
interiors(filter: InteriorsFilterInput): [Interior]
}
type Mutation {
addKeyword(productId: Int!, keywordId: Int!): MutationResult
}
type MutationResult {
result: String!
}
"""
Only return valid interiors for product, type and orientation
"""
input InteriorsFilterInput {
productId: Int!
type: InteriorType!
orientation: Orientation!
} }
input DateInput { input DateInput {
@@ -134,8 +154,8 @@ const typeDefs = gql`
designerPath: String designerPath: String
discountType: String discountType: String
discountValue: Float discountValue: Float
displayHeight: Int displayHeight: Float
displayWidth: Int displayWidth: Float
edge: String edge: String
frameColor: String frameColor: String
framed: Int framed: Int
@@ -175,6 +195,7 @@ const typeDefs = gql`
id: ID! id: ID!
name: String name: String
path: String path: String
pathNumeric: String
isLeaf: Int isLeaf: Int
depth: Int depth: Int
childCount: Int childCount: Int
@@ -185,6 +206,7 @@ const typeDefs = gql`
type Product { type Product {
id: ID! id: ID!
stockid: Int
path: String! path: String!
visible: Boolean! visible: Boolean!
browsable: Boolean! browsable: Boolean!
@@ -196,15 +218,53 @@ const typeDefs = gql`
designerId: Int designerId: Int
designer: Designer designer: Designer
keywords: [Keyword] keywords: [Keyword]
ref1: String
ref2: String
ref3: String
related: [Product]
wallpaperTypes: [ProductWallpaperType]
fields: ProductFields
orientation: Orientation
""" """
Will be single value return in later release Will be single values return in later release
""" """
type: [ProductType] type: [ProductType]
# Below are for debug # Below are for debug and will be removed soon
api1json: JSON
fields_json: JSON fields_json: JSON
} }
type ProductFields {
artNo: String!
name: String!
height: Int
width: Int
photowallResolution: Int
canvasResolution: Int
wallpaperResolution: Int
copyright: String
proportionsWarning: String
marginWidthMax: Int
marginWidthMin: Int
marginHeightMax: Int
marginHeightMin: Int
focusXpoint: Float
focusYpoint: Float
focusXpoint2: Float
focusYpoint2: Float
batch: String
imageResolution: Int
printFileWidth: Int
printFileHeight: Int
printFileDpi: Int
}
enum Orientation {
UNKNOWN
PORTRAIT
LANDSCAPE
SQUARE
}
enum ProductGroup { enum ProductGroup {
PHOTO_WALLPAPER PHOTO_WALLPAPER
CANVAS CANVAS
@@ -223,6 +283,11 @@ const typeDefs = gql`
ILLUSTRATION ILLUSTRATION
} }
enum ProductWallpaperType {
WALLMURAL
DESIGN
}
type ProductBlacklist { type ProductBlacklist {
id: ID! id: ID!
groupId: Int! groupId: Int!
@@ -239,7 +304,28 @@ const typeDefs = gql`
group: ProductGroup! group: ProductGroup!
inserted: DateTime! inserted: DateTime!
updated: DateTime updated: DateTime
price(market: Int, width: Int, height: Int, sku: String): JSON interiors: [Interior]
}
enum InteriorType {
WALLPAPER
PAINTING
POSTER
FRAMED_PRINT
}
type Interior {
"""
room id
"""
id: ID!
roomName: String
position: Int
roomType: String
uri: String
roomNumber: Int
type: InteriorType
orientation: Orientation
} }
type Designer { type Designer {
@@ -255,7 +341,7 @@ const typeDefs = gql`
} }
type Keyword { type Keyword {
id: Int! id: ID!
value: String! value: String!
type: KeywordType type: KeywordType
products: [Product] products: [Product]
@@ -321,7 +407,7 @@ orderrows example
} }
types:
"doityourselfframe" "doityourselfframe"
"fixed" "fixed"
"tiling" "tiling"
+1
View File
@@ -2,6 +2,7 @@ export interface Category {
id: number; id: number;
name: string; name: string;
path: string; path: string;
pathNumeric: string;
isLeaf: number; isLeaf: number;
depth: number; depth: number;
childCount: number; childCount: number;
+29
View File
@@ -0,0 +1,29 @@
import { Orientation } from './product-types';
export interface Interior {
id: number;
roomName: string;
position: number;
roomType: string;
uri: string;
roomNumber: number;
type: InteriorType;
orientation: Orientation;
}
export enum InteriorType {
WALLPAPER,
PAINTING,
POSTER,
FRAMED_PRINT,
}
export interface InteriorsFilter {
productId: number;
type: InteriorType;
orientation: Orientation;
}
export interface InteriorsFilterInput {
filter?: InteriorsFilter;
}
+7
View File
@@ -0,0 +1,7 @@
export interface Market {
id: number;
name: string;
vat: number;
currency: string;
priceAdjustment: number;
}
-10
View File
@@ -1,13 +1,3 @@
import { DateInput } from './types';
export interface Market {
id: number;
name: string;
vat: number;
currency: string;
priceAdjustment: number;
}
export interface Address { export interface Address {
id: number; id: number;
firstname: string; firstname: string;
+43 -2
View File
@@ -15,6 +15,19 @@ export enum ProductType {
PHOTO = 1, PHOTO = 1,
ILLUSTRATION = 2, ILLUSTRATION = 2,
} }
export enum ProductWallpaperType {
WALLMURAL = 1,
DESIGN = 2,
}
export enum Orientation {
UNKNOWN = 0,
PORTRAIT = 1,
LANDSCAPE = 2,
SQUARE = 3,
}
export interface ProductBlacklist { export interface ProductBlacklist {
id: number; id: number;
groupId: number; groupId: number;
@@ -34,16 +47,44 @@ export interface PrintProduct {
export interface Product { export interface Product {
id: number; id: number;
stockid: number;
path: string; path: string;
visible: boolean; visible: boolean;
browsable: boolean; browsable: boolean;
inserted: Date; inserted: Date;
updated?: Date; updated?: Date;
orientation?: Orientation;
printProducts: [PrintProduct]; printProducts: [PrintProduct];
blacklisting: [ProductBlacklist]; blacklisting: [ProductBlacklist];
type: [ProductType]; type: [ProductType];
api1json: JSON; fields: ProductFields;
fields_json: JSON; fields_json: JSON; // remove later
}
// TODO: test stock products to see what props can be mandatory
export interface ProductFields {
artNo: string;
name: string;
height?: number;
width?: number;
photowallResolution?: number;
canvasResolution?: number;
wallpaperResolution?: number;
copyright?: string;
proportionsWarning?: string;
marginWidthMax?: number;
marginWidthMin?: number;
marginHeightMax?: number;
marginHeightMin?: number;
focusXpoint?: number;
focusYpoint?: number;
focusXpoint2?: number;
focusYpoint2?: number;
batch?: string;
imageResolution?: number;
printFileWidth?: number;
printFileHeight?: number;
printFileDpi?: number;
} }
export function getProductGroupStringFromString(group: string): string { export function getProductGroupStringFromString(group: string): string {
+43
View File
@@ -0,0 +1,43 @@
When added as order rows :
kit 47851:
'stockId'
'productId' 47851
'type' // always 'stock'
'artNo'
'path'
'name'
'baseName'
'price'
'weight'
dyi frame 45479:
'stockId'
'productId' 45479
'product_description'
'type' // always 'stock'
'artNo'
'path'
'name'
'price'
'width'
'height'
'weight'
'measure_unit'
'displayWidth'
'displayHeight'
sample:
'productId'
'name'
'artNo'
'material'
'weight'
'price'
'type' // always 'stock'
'path'