Added product admin features
This commit is contained in:
+2
-1
@@ -10,6 +10,7 @@
|
||||
"extends": ["prettier"],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": ["error"]
|
||||
"prettier/prettier": ["error"],
|
||||
"no-return-await": "error"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# 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
|
||||
# specified network ports at runtime
|
||||
EXPOSE 4000
|
||||
|
||||
@@ -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"]
|
||||
@@ -17,6 +17,7 @@ Then head over to [The playground GUI](https://localhost:4000) in the browser
|
||||
## 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.
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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)"
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"rulePriority": 1,
|
||||
"description": "Only keep 8 images",
|
||||
"selection": {
|
||||
"tagStatus": "any",
|
||||
"countType": "imageCountMoreThan",
|
||||
"countNumber": 8
|
||||
},
|
||||
"action": { "type": "expire" }
|
||||
}]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -1,18 +1,19 @@
|
||||
version: '3.7'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
volumes:
|
||||
- ./:/app
|
||||
# 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:
|
||||
|
||||
Generated
+117
-84
@@ -1251,9 +1251,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@nodelib/fs.walk": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz",
|
||||
"integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==",
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -1380,9 +1380,9 @@
|
||||
}
|
||||
},
|
||||
"@types/archiver": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz",
|
||||
"integrity": "sha512-qJ79qsmq7O/k9FYwsF6O1xVA1PeLV+9Bh3TYkVCu3VzMR6vN9JQkgEOh/rrQ0R+F4Ta+R3thHGewxQtFglwVfg==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.1.1.tgz",
|
||||
"integrity": "sha512-heuaCk0YH5m274NOLSi66H1zX6GtZoMsdE6TYFcpFFjBjg0FoU4i4/M/a/kNlgNg26Xk3g364mNOYe1JaiEPOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/glob": "*"
|
||||
@@ -1468,9 +1468,9 @@
|
||||
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
|
||||
},
|
||||
"@types/dockerode": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.3.tgz",
|
||||
"integrity": "sha512-nZRhpSxm3PYianRBcRExcHxDvEzYHUPfGCnRL5Fe4/fSEZbtxrRNJ7okzCans3lXxj2t298EynFHGTnTC2f1Iw==",
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.4.tgz",
|
||||
"integrity": "sha512-CSpvSohBQib0KCyg99h6RSmLZ4V9wF9hx6vT2FatplBNQpRaUzJh/XGjesRPzvqOlpp/Ol7aLFEgiE02nj/m2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -1506,9 +1506,9 @@
|
||||
}
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
||||
"integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==",
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz",
|
||||
"integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/minimatch": "*",
|
||||
@@ -1559,9 +1559,9 @@
|
||||
}
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "26.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
|
||||
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
|
||||
"version": "26.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz",
|
||||
"integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jest-diff": "^26.0.0",
|
||||
@@ -1569,9 +1569,9 @@
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz",
|
||||
"integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/keygrip": {
|
||||
@@ -1613,9 +1613,9 @@
|
||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
@@ -1649,9 +1649,9 @@
|
||||
}
|
||||
},
|
||||
"@types/ssh2": {
|
||||
"version": "0.5.46",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.46.tgz",
|
||||
"integrity": "sha512-1pC8FHrMPYdkLoUOwTYYifnSEPzAFZRsp3JFC/vokQ+dRrVI+hDBwz0SNmQ3pL6h39OSZlPs0uCG7wKJkftnaA==",
|
||||
"version": "0.5.47",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.47.tgz",
|
||||
"integrity": "sha512-ZhqJg8BRV7OsCi0KVqPr27lUMMmLEeHYw1VXUNGGDlQEDq9HTsKx+wYvi8E6oNC6gRZ7PV99ZMZmMr5vztcYYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
@@ -1659,9 +1659,9 @@
|
||||
}
|
||||
},
|
||||
"@types/ssh2-streams": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.8.tgz",
|
||||
"integrity": "sha512-I7gixRPUvVIyJuCEvnmhr3KvA2dC0639kKswqD4H5b4/FOcnPtNU+qWLiXdKIqqX9twUvi5j0U1mwKE5CUsrfA==",
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz",
|
||||
"integrity": "sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -1682,9 +1682,9 @@
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "15.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
|
||||
"integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
|
||||
"version": "15.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
|
||||
"integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
@@ -1697,13 +1697,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.2.tgz",
|
||||
"integrity": "sha512-PGqpLLzHSxq956rzNGasO3GsAPf2lY9lDUBXhS++SKonglUmJypaUtcKzRtUte8CV7nruwnDxtLUKpVxs0wQBw==",
|
||||
"version": "4.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.3.tgz",
|
||||
"integrity": "sha512-jW8sEFu1ZeaV8xzwsfi6Vgtty2jf7/lJmQmDkDruBjYAbx5DA8JtbcMnP0rNPUG+oH5GoQBTSp+9613BzuIpYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.28.2",
|
||||
"@typescript-eslint/scope-manager": "4.28.2",
|
||||
"@typescript-eslint/experimental-utils": "4.28.3",
|
||||
"@typescript-eslint/scope-manager": "4.28.3",
|
||||
"debug": "^4.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^3.1.0",
|
||||
@@ -1738,28 +1738,28 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.2.tgz",
|
||||
"integrity": "sha512-MwHPsL6qo98RC55IoWWP8/opTykjTp4JzfPu1VfO2Z0MshNP0UZ1GEV5rYSSnZSUI8VD7iHvtIPVGW5Nfh7klQ==",
|
||||
"version": "4.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.3.tgz",
|
||||
"integrity": "sha512-zZYl9TnrxwEPi3FbyeX0ZnE8Hp7j3OCR+ELoUfbwGHGxWnHg9+OqSmkw2MoCVpZksPCZYpQzC559Ee9pJNHTQw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"@typescript-eslint/scope-manager": "4.28.2",
|
||||
"@typescript-eslint/types": "4.28.2",
|
||||
"@typescript-eslint/typescript-estree": "4.28.2",
|
||||
"@typescript-eslint/scope-manager": "4.28.3",
|
||||
"@typescript-eslint/types": "4.28.3",
|
||||
"@typescript-eslint/typescript-estree": "4.28.3",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.2.tgz",
|
||||
"integrity": "sha512-Q0gSCN51eikAgFGY+gnd5p9bhhCUAl0ERMiDKrTzpSoMYRubdB8MJrTTR/BBii8z+iFwz8oihxd0RAdP4l8w8w==",
|
||||
"version": "4.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.3.tgz",
|
||||
"integrity": "sha512-ZyWEn34bJexn/JNYvLQab0Mo5e+qqQNhknxmc8azgNd4XqspVYR5oHq9O11fLwdZMRcj4by15ghSlIEq+H5ltQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.28.2",
|
||||
"@typescript-eslint/types": "4.28.2",
|
||||
"@typescript-eslint/typescript-estree": "4.28.2",
|
||||
"@typescript-eslint/scope-manager": "4.28.3",
|
||||
"@typescript-eslint/types": "4.28.3",
|
||||
"@typescript-eslint/typescript-estree": "4.28.3",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -1781,29 +1781,29 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.2.tgz",
|
||||
"integrity": "sha512-MqbypNjIkJFEFuOwPWNDjq0nqXAKZvDNNs9yNseoGBB1wYfz1G0WHC2AVOy4XD7di3KCcW3+nhZyN6zruqmp2A==",
|
||||
"version": "4.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.3.tgz",
|
||||
"integrity": "sha512-/8lMisZ5NGIzGtJB+QizQ5eX4Xd8uxedFfMBXOKuJGP0oaBBVEMbJVddQKDXyyB0bPlmt8i6bHV89KbwOelJiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.2",
|
||||
"@typescript-eslint/visitor-keys": "4.28.2"
|
||||
"@typescript-eslint/types": "4.28.3",
|
||||
"@typescript-eslint/visitor-keys": "4.28.3"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.2.tgz",
|
||||
"integrity": "sha512-Gr15fuQVd93uD9zzxbApz3wf7ua3yk4ZujABZlZhaxxKY8ojo448u7XTm/+ETpy0V0dlMtj6t4VdDvdc0JmUhA==",
|
||||
"version": "4.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.3.tgz",
|
||||
"integrity": "sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.2.tgz",
|
||||
"integrity": "sha512-86lLstLvK6QjNZjMoYUBMMsULFw0hPHJlk1fzhAVoNjDBuPVxiwvGuPQq3fsBMCxuDJwmX87tM/AXoadhHRljg==",
|
||||
"version": "4.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz",
|
||||
"integrity": "sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.2",
|
||||
"@typescript-eslint/visitor-keys": "4.28.2",
|
||||
"@typescript-eslint/types": "4.28.3",
|
||||
"@typescript-eslint/visitor-keys": "4.28.3",
|
||||
"debug": "^4.3.1",
|
||||
"globby": "^11.0.3",
|
||||
"is-glob": "^4.0.1",
|
||||
@@ -1838,12 +1838,12 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.2.tgz",
|
||||
"integrity": "sha512-aT2B4PLyyRDUVUafXzpZFoc0C9t0za4BJAKP5sgWIhG+jHECQZUEjuQSCIwZdiJJ4w4cgu5r3Kh20SOdtEBl0w==",
|
||||
"version": "4.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz",
|
||||
"integrity": "sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.2",
|
||||
"@typescript-eslint/types": "4.28.3",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -2051,15 +2051,48 @@
|
||||
}
|
||||
},
|
||||
"apollo-datasource-rest": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-datasource-rest/-/apollo-datasource-rest-0.14.0.tgz",
|
||||
"integrity": "sha512-OXh+kbpZPx0F5fNJLYmxIa8Nt+UIC7oK1pnoWtLMDws9/TiICaG6J7SHHAKnz03ZVnZ4XE9gKtm9RTCjCYJtSw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-datasource-rest/-/apollo-datasource-rest-3.0.0.tgz",
|
||||
"integrity": "sha512-pjx2uOvQSGHCE6fEeItTUniqYshL3OvD02ybwhbISg+7iQXKsyvgYKDgzY77tDjIiNSdUZsFOkNw2lmfnORBxA==",
|
||||
"requires": {
|
||||
"apollo-datasource": "^0.9.0",
|
||||
"apollo-server-caching": "^0.7.0",
|
||||
"apollo-server-env": "^3.1.0",
|
||||
"apollo-server-errors": "^2.5.0",
|
||||
"http-cache-semantics": "^4.0.0"
|
||||
"apollo-datasource": "^3.0.0",
|
||||
"apollo-server-caching": "^3.0.0",
|
||||
"apollo-server-env": "^4.0.0",
|
||||
"apollo-server-errors": "^3.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": {
|
||||
@@ -4247,9 +4280,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.6.tgz",
|
||||
"integrity": "sha512-GnLuqj/pvQ7pX8/L4J84nijv6sAnlwvSDpMkJi9i7nPmPxGtRPkBSStfvDW5l6nMdX9VWe+pkKWFTgD+vF2QSQ==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
|
||||
"integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -7786,9 +7819,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"nodemon": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.9.tgz",
|
||||
"integrity": "sha512-6O4k7C8f2HQArGpaPBOqGGddjzDLQAqCYmq3tKMeNIbz7Is/hOphMHy2dcY10sSq5wl3cqyn9Iz+Ep2j51JOLg==",
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz",
|
||||
"integrity": "sha512-egCTmNZdObdBxUBw6ZNwvZ/xzk24CKRs5K6d+5zbmrMr7rOpPmfPeF6OxM3DDpaRx331CQRFEktn+wrFFfBSOA==",
|
||||
"requires": {
|
||||
"chokidar": "^3.2.2",
|
||||
"debug": "^3.2.6",
|
||||
@@ -9318,9 +9351,9 @@
|
||||
}
|
||||
},
|
||||
"testcontainers": {
|
||||
"version": "7.11.1",
|
||||
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-7.11.1.tgz",
|
||||
"integrity": "sha512-lfZeys5bLkADjOaoXfQy0V0+G8sGKr8ESANz7MhSVBwC+OTTxkP3+FVwP48bW4mwRcQ4Hojwbfw10OYT80QZmQ==",
|
||||
"version": "7.12.2",
|
||||
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-7.12.2.tgz",
|
||||
"integrity": "sha512-rp35IGJPPB6ldOjho/LP2OTSmxI5RIpmd0G1HMgm7oYgdAmpRvSLazIHkUMzG3g7tg5fJtTl7qO18KUv+bU3YQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/archiver": "^5.1.0",
|
||||
@@ -9504,9 +9537,9 @@
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.0.0.tgz",
|
||||
"integrity": "sha512-ROWeOIUvfFbPZkoDis0L/55Fk+6gFQNZwwKPLinacRl6tsxstTF1DbAcLKkovwnpKMVvOMHP1TIbnwXwtLg1gg==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.1.0.tgz",
|
||||
"integrity": "sha512-6szn3+J9WyG2hE+5W8e0ruZrzyk1uFLYye6IGMBadnOzDh8aP7t8CbFpsfCiEx2+wMixAhjFt7lOZC4+l+WbEA==",
|
||||
"requires": {
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
|
||||
+7
-7
@@ -12,7 +12,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"apollo-datasource": "^0.9.0",
|
||||
"apollo-datasource-rest": "^0.14.0",
|
||||
"apollo-datasource-rest": "^3.0.0",
|
||||
"apollo-server": "^2.25.2",
|
||||
"async-redis": "^2.0.0",
|
||||
"axios": "^0.21.1",
|
||||
@@ -23,26 +23,26 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwk-to-pem": "^2.0.5",
|
||||
"knex-stringcase": "^1.4.5",
|
||||
"nodemon": "^2.0.9",
|
||||
"nodemon": "^2.0.12",
|
||||
"pg": "^8.6.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"prettier": "^2.3.2",
|
||||
"stringcase": "^4.3.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "^4.3.5",
|
||||
"util": "^0.12.4",
|
||||
"validator": "^13.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.23",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.3",
|
||||
"@typescript-eslint/parser": "^4.28.3",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jest": "^27.0.6",
|
||||
"testcontainers": "^7.11.1",
|
||||
"testcontainers": "^7.12.2",
|
||||
"ts-jest": "^27.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
+151
-48
@@ -1,32 +1,29 @@
|
||||
{
|
||||
products(limit: 10000, browsable: true, visible: true) {
|
||||
id
|
||||
keywords {
|
||||
id
|
||||
value
|
||||
type
|
||||
}
|
||||
type
|
||||
fields_json
|
||||
inserted
|
||||
path
|
||||
updated
|
||||
visible
|
||||
browsable
|
||||
printProducts {
|
||||
group
|
||||
categories {
|
||||
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 {
|
||||
id
|
||||
name
|
||||
@@ -46,37 +43,78 @@
|
||||
}
|
||||
}
|
||||
|
||||
# Orders
|
||||
{
|
||||
orders(
|
||||
filter: { limit: 1, dates: { from: "2021-03-01", to: "2021-04-30" } }
|
||||
) {
|
||||
id
|
||||
market {
|
||||
id
|
||||
name
|
||||
vat
|
||||
currency
|
||||
priceAdjustment
|
||||
filter: {
|
||||
limit: 100
|
||||
offset: 0
|
||||
dates: { from: "2021-03-01", to: "2021-04-30" }
|
||||
}
|
||||
rows {
|
||||
) {
|
||||
total
|
||||
offset
|
||||
limit
|
||||
items {
|
||||
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
|
||||
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
|
||||
{
|
||||
product(id: 73803) {
|
||||
product(id: 42166) {
|
||||
id
|
||||
stockid
|
||||
fields_json
|
||||
orientation
|
||||
related {
|
||||
id
|
||||
fields {
|
||||
name
|
||||
}
|
||||
}
|
||||
ref1
|
||||
ref2
|
||||
ref3
|
||||
wallpaperTypes
|
||||
keywords {
|
||||
id
|
||||
value
|
||||
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
|
||||
path
|
||||
updated
|
||||
@@ -171,6 +246,7 @@
|
||||
id
|
||||
name
|
||||
path
|
||||
pathNumeric
|
||||
isLeaf
|
||||
depth
|
||||
childCount
|
||||
@@ -186,6 +262,14 @@
|
||||
group
|
||||
id
|
||||
updated
|
||||
inserted
|
||||
interiors {
|
||||
id
|
||||
roomName
|
||||
uri
|
||||
position
|
||||
roomType
|
||||
}
|
||||
}
|
||||
blacklisting {
|
||||
group
|
||||
@@ -195,7 +279,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Categories
|
||||
{
|
||||
category(id: 1653) {
|
||||
@@ -236,3 +319,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
markets {
|
||||
id
|
||||
name
|
||||
vat
|
||||
currency
|
||||
priceAdjustment
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
market(name: "GB") {
|
||||
id
|
||||
name
|
||||
vat
|
||||
currency
|
||||
priceAdjustment
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ describe('GraphQL Apollo tests', () => {
|
||||
it('should be possible to fetch a designers with limit', async () => {
|
||||
// CONTROL
|
||||
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;
|
||||
// -------------------
|
||||
@@ -186,10 +186,6 @@ describe('GraphQL Apollo tests', () => {
|
||||
|
||||
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[1].name).toBe(controlData[1].name);
|
||||
expect(designers[2].name).toBe(controlData[2].name);
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ WHERE product_category.product_id = ?
|
||||
data.rows.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
path: row.path.replace(/^root/, ''),
|
||||
pathNumeric: row.path_numeric,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -29,33 +29,16 @@ WHERE product_category.product_id = ?
|
||||
}
|
||||
|
||||
async getCategories(): Promise<Array<Category>> {
|
||||
return await this.knex
|
||||
.select('*')
|
||||
.from('v_categorytree')
|
||||
.cache(MINUTE)
|
||||
.then((rows) => {
|
||||
return rows.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
path: row.path.replace(/^root/, ''),
|
||||
};
|
||||
});
|
||||
});
|
||||
return this.knex.select('*').from('v_categorytree').cache(MINUTE);
|
||||
}
|
||||
|
||||
async getCategoryById(id: number): Promise<Category> {
|
||||
return await this.knex
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('v_categorytree')
|
||||
.where('id', id)
|
||||
.first()
|
||||
.cache(MINUTE)
|
||||
.then((row) => {
|
||||
return {
|
||||
...row,
|
||||
path: row.path.replace(/^root/, ''),
|
||||
};
|
||||
});
|
||||
.cache(MINUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,10 +48,18 @@ WHERE product_category.product_id = ?
|
||||
SELECT category_keyword.category_id, keywords.value
|
||||
FROM category_keyword
|
||||
JOIN keywords ON category_keyword.keyword_id = keywords.id;
|
||||
|
||||
|
||||
FROM categorydata cd
|
||||
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???
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class DesignerAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getDesignerById(id: number): Promise<Designer> {
|
||||
return await this.knex
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('designers')
|
||||
.where('designerid', id)
|
||||
@@ -24,11 +24,12 @@ export class DesignerAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getDesigners(limit: Maybe<number>): Promise<Array<Designer>> {
|
||||
limit = limit ?? 100;
|
||||
return await this.knex
|
||||
limit = limit ?? 5000;
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('designers')
|
||||
.limit(limit)
|
||||
.orderBy('name')
|
||||
.cache(MINUTE)
|
||||
.then((rows) => {
|
||||
return rows.map((row) => {
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,9 @@ WHERE product_keyword.product_id = ?
|
||||
ORDER BY keywords.value
|
||||
`;
|
||||
|
||||
const res = await this.knex
|
||||
return this.knex
|
||||
.raw(query, productId)
|
||||
.then((data) => data.rows.map((row) => this.getType(row)));
|
||||
return res;
|
||||
}
|
||||
|
||||
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
|
||||
ORDER BY keywords.value
|
||||
`;
|
||||
const res = await this.knex
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.then((data) => data.rows.map((row) => this.getType(row)));
|
||||
return res;
|
||||
}
|
||||
|
||||
async getKeywordById(id: number): Promise<Keyword> {
|
||||
const res = await this.knex
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('keywords')
|
||||
.leftJoin('keyword_type', 'keyword_type.keyword_id', 'keywords.id')
|
||||
@@ -55,7 +53,6 @@ ORDER BY keywords.value
|
||||
.first()
|
||||
.cache(MINUTE)
|
||||
.then((row) => this.getType(row));
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { camelcase } from 'stringcase';
|
||||
import {
|
||||
Address,
|
||||
ContactInformation,
|
||||
Market,
|
||||
Order,
|
||||
OrderRow,
|
||||
} from '../types/order-types';
|
||||
@@ -17,24 +16,6 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
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>> {
|
||||
const row = await this.knex
|
||||
.select('*')
|
||||
@@ -80,20 +61,13 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
return row;
|
||||
}
|
||||
|
||||
async getOrdersTotal(input: Maybe<GeneralInput>): Promise<Number> {
|
||||
let query = this.knex.table('orders');
|
||||
|
||||
if (input?.dates?.from) {
|
||||
query = query.where('inserted', '>=', input.dates.from);
|
||||
}
|
||||
|
||||
if (input?.dates?.to) {
|
||||
query = query.where('inserted', '<=', input.dates.to);
|
||||
}
|
||||
|
||||
const res = await query.clone().count();
|
||||
console.log(res);
|
||||
return 57777;
|
||||
async getOrdersTotal(input: Maybe<GeneralInput>): Promise<number> {
|
||||
const res = await this.getOrdersQuery(input)
|
||||
.clone()
|
||||
.count()
|
||||
.first()
|
||||
.cache(MINUTE * 60);
|
||||
return convertToPossibleType(res['count']); // Optimize later to return Promise
|
||||
}
|
||||
|
||||
async getOrders(input: Maybe<GeneralInput>): Promise<Array<Order>> {
|
||||
@@ -103,7 +77,7 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
query = query.limit(input?.limit);
|
||||
}
|
||||
query = query.orderBy('inserted', 'ASC');
|
||||
return await query
|
||||
return query
|
||||
.cache(MINUTE)
|
||||
.then((rows) => rows.map((row) => this.createOrderFromRow(row)));
|
||||
}
|
||||
@@ -122,7 +96,7 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
}
|
||||
|
||||
async getOrderById(id: number): Promise<Order> {
|
||||
return await this.knex
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('orders')
|
||||
.where('id', id)
|
||||
@@ -200,7 +174,7 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
.raw('SELECT * FROM "order-rows" WHERE orderid = ?', orderId)
|
||||
.then((data) => data.rows);
|
||||
|
||||
return await this.getOrderRows(rows);
|
||||
return this.getOrderRows(rows);
|
||||
}
|
||||
|
||||
async getOrderRowsByDesignerId(
|
||||
@@ -228,6 +202,6 @@ export class OrderAPI extends BaseSQLDataSource {
|
||||
const rows = await this.knex
|
||||
.raw(query, [designerId, this.getSQLDate(from), this.getSQLDate(to)])
|
||||
.then((data) => data.rows);
|
||||
return await this.getOrderRows(rows);
|
||||
return this.getOrderRows(rows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SQLDataSource } from 'datasource-sql';
|
||||
import { BaseSQLDataSource } from './BaseSQLDataSource';
|
||||
import { camelcase } from 'stringcase';
|
||||
import * as sql from './sql';
|
||||
import {
|
||||
Product,
|
||||
@@ -6,12 +7,16 @@ import {
|
||||
PrintProduct,
|
||||
ProductType,
|
||||
ProductBlacklist,
|
||||
ProductWallpaperType,
|
||||
ProductFields,
|
||||
Orientation,
|
||||
} from '../types/product-types';
|
||||
import { Maybe } from '../types/types';
|
||||
import { convertToPossibleType } from './utils';
|
||||
|
||||
const MINUTE = 60;
|
||||
|
||||
export class ProductAPI extends SQLDataSource {
|
||||
export class ProductAPI extends BaseSQLDataSource {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
@@ -21,6 +26,25 @@ export class ProductAPI extends SQLDataSource {
|
||||
// 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> {
|
||||
if (!row.blacklisting) {
|
||||
return [];
|
||||
@@ -42,26 +66,36 @@ export class ProductAPI extends SQLDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
getProductTypes(row: any): Array<ProductType> {
|
||||
if (!row.types) {
|
||||
return [];
|
||||
getProductFields(row: any): ProductFields {
|
||||
const fields = Object.keys(row.fields).reduce((mem, key) => {
|
||||
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 (type === 'typeillustration') {
|
||||
return ProductType[ProductType.ILLUSTRATION];
|
||||
}
|
||||
if (type === 'typephotography') {
|
||||
return ProductType[ProductType.PHOTO];
|
||||
}
|
||||
return ProductType[ProductType.UNKNOWN];
|
||||
});
|
||||
if (fields.width > fields.height) {
|
||||
return Orientation[Orientation.LANDSCAPE];
|
||||
}
|
||||
if (fields.width < fields.height) {
|
||||
return Orientation[Orientation.PORTRAIT];
|
||||
}
|
||||
return Orientation[Orientation.SQUARE];
|
||||
}
|
||||
|
||||
createProductFromRow(row: any) {
|
||||
row.blacklisting = this.getBlacklisting(row);
|
||||
row.printProducts = this.getPrintProducts(row);
|
||||
row.type = this.getProductTypes(row);
|
||||
row.wallpaperTypes = this.getWallpaperTypes(row);
|
||||
row.fields = this.getProductFields(row);
|
||||
row.designerId = row.designerid;
|
||||
row.orientation = this.getOrientation(row.fields);
|
||||
row.fields_json = row.fields;
|
||||
return row;
|
||||
}
|
||||
@@ -74,7 +108,7 @@ export class ProductAPI extends SQLDataSource {
|
||||
limit = limit ?? 100;
|
||||
const query = sql.products(limit, visible, browsable);
|
||||
|
||||
return await this.knex
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.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>> {
|
||||
const query = sql.categoryProducts(categoryId);
|
||||
|
||||
return await this.knex
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.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>> {
|
||||
const query = sql.keywordProducts(keywordId);
|
||||
|
||||
return await this.knex
|
||||
return this.knex
|
||||
.raw(query)
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,57 +4,75 @@ import { Maybe } from '../../types/types';
|
||||
|
||||
const baseQuery = /* sql */ `
|
||||
|
||||
SELECT products.productid as id,
|
||||
SELECT products.productid AS id,
|
||||
stock.stockid,
|
||||
products.path,
|
||||
products.visible,
|
||||
products.browsable,
|
||||
products.designerid,
|
||||
products.inserted,
|
||||
products.updated,
|
||||
products.ref1,
|
||||
products.ref2,
|
||||
products.ref3,
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'printId',
|
||||
"product-printproducts".printid,
|
||||
'productId',
|
||||
"product-printproducts".productid,
|
||||
'groupId',
|
||||
"product-printproducts".groupid,
|
||||
'inserted',
|
||||
"product-printproducts".inserted,
|
||||
'updated',
|
||||
"product-printproducts".updated
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'printId',
|
||||
"product-printproducts".printid,
|
||||
'productId',
|
||||
"product-printproducts".productid,
|
||||
'groupId',
|
||||
"product-printproducts".groupid,
|
||||
'inserted',
|
||||
"product-printproducts".inserted,
|
||||
'updated',
|
||||
"product-printproducts".updated
|
||||
)
|
||||
)
|
||||
) FROM "product-printproducts" WHERE "product-printproducts".productid = products.productid
|
||||
FROM "product-printproducts"
|
||||
WHERE "product-printproducts".productid = products.productid
|
||||
) AS printproducts,
|
||||
( SELECT json_agg(
|
||||
json_build_object(
|
||||
'id',
|
||||
product_blacklist.id,
|
||||
'productId',
|
||||
product_blacklist.product_id,
|
||||
'marketId',
|
||||
product_blacklist.market_id,
|
||||
'groupId',
|
||||
product_blacklist.group_id,
|
||||
'inserted',
|
||||
product_blacklist.created_at,
|
||||
'updated',
|
||||
product_blacklist.updated_at
|
||||
(
|
||||
SELECT json_agg(
|
||||
json_build_object(
|
||||
'id',
|
||||
product_blacklist.id,
|
||||
'productId',
|
||||
product_blacklist.product_id,
|
||||
'marketId',
|
||||
product_blacklist.market_id,
|
||||
'groupId',
|
||||
product_blacklist.group_id,
|
||||
'inserted',
|
||||
product_blacklist.created_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,
|
||||
( SELECT json_object_agg(fields.field, pf.value)
|
||||
(
|
||||
SELECT json_object_agg(fields.field, pf.value)
|
||||
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
|
||||
) AS fields,
|
||||
( SELECT json_agg(keywords.value)
|
||||
(
|
||||
SELECT json_agg(keywords.value)
|
||||
FROM product_keyword pk
|
||||
JOIN keywords ON keywords.id = pk.keyword_id
|
||||
WHERE keywords.id IN (1103,1104)AND pk.product_id = products.productid
|
||||
) AS types
|
||||
FROM "product-products" products
|
||||
JOIN keywords ON keywords.id = pk.keyword_id
|
||||
WHERE keywords.id IN (1103, 1104)
|
||||
AND pk.product_id = products.productid
|
||||
) 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(
|
||||
|
||||
+14
-12
@@ -9,33 +9,34 @@ import resolvers from './resolvers';
|
||||
import { OrderAPI } from './datasources/order-api';
|
||||
import { ProductAPI } from './datasources/product-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 { 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?
|
||||
const knexConfig = knexStringcase(dbConfig);
|
||||
|
||||
// set up any dataSources our resolvers need
|
||||
const api1 = new Api1DataSource();
|
||||
const api2 = new Api2DataSource();
|
||||
const categoryApi = new CategoryAPI(knexConfig);
|
||||
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 productApi = new ProductAPI(knexConfig);
|
||||
const categoryApi = new CategoryAPI(knexConfig);
|
||||
const keywordApi = new KeywordAPI(knexConfig);
|
||||
const designerApi = new DesignerAPI(knexConfig);
|
||||
|
||||
const dataSources = () => ({
|
||||
api1,
|
||||
api2,
|
||||
categoryApi,
|
||||
designerApi,
|
||||
interiorApi,
|
||||
imageServerApi,
|
||||
keywordApi,
|
||||
marketApi,
|
||||
orderApi,
|
||||
productApi,
|
||||
});
|
||||
@@ -51,6 +52,7 @@ const context = async ({ req }) => {
|
||||
const res = await verifyToken({ token });
|
||||
return { auth: res };
|
||||
} else if (authHeader == 'Basic ZGV2OmRldg==') {
|
||||
// dev:dev remove later
|
||||
return { auth: 'during development' };
|
||||
}
|
||||
throw new AuthenticationError('Not authorized');
|
||||
|
||||
@@ -16,7 +16,7 @@ async function getDesigners(_, { limit }, { dataSources }) {
|
||||
return (<DesignerAPI>dataSources.designerApi).getDesigners(limit);
|
||||
}
|
||||
|
||||
async function getDesigner(_, { id }, { dataSources }) {
|
||||
async function getDesigner(parent, { id }, { dataSources }) {
|
||||
return (<DesignerAPI>dataSources.designerApi).getDesignerById(id);
|
||||
}
|
||||
|
||||
|
||||
+16
-2
@@ -1,9 +1,16 @@
|
||||
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 { categoryQueryTypeDefs, categoryTypeDefs } from './categories-resolver';
|
||||
import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars';
|
||||
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 = {
|
||||
Query: {
|
||||
...orderQueryTypeDefs,
|
||||
@@ -11,12 +18,19 @@ const resolvers = {
|
||||
...designerQueryTypeDefs,
|
||||
...categoryQueryTypeDefs,
|
||||
...keywordsQueryTypeDefs,
|
||||
...marketsQueryTypeDefs,
|
||||
...interiorsQueryTypeDefs,
|
||||
},
|
||||
Mutation: {
|
||||
...productMutationTypeDefs,
|
||||
},
|
||||
...orderTypeDefs,
|
||||
...productTypeDefs,
|
||||
...designerTypeDefs,
|
||||
...categoryTypeDefs,
|
||||
...keywordTypeDefs,
|
||||
...marketTypeDefs,
|
||||
...interiorTypeDefs,
|
||||
DateTime: DateTimeResolver,
|
||||
Date: DateResolver,
|
||||
JSON: JSONResolver,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IResolverObject } from 'graphql-tools';
|
||||
import { MarketAPI } from '../datasources/market-api';
|
||||
import { OrderAPI } from '../datasources/order-api';
|
||||
import { FilterInput } from '../types/types';
|
||||
|
||||
@@ -14,7 +15,7 @@ const Order: IResolverObject = {
|
||||
return (<OrderAPI>dataSources.orderApi).getOrderRowsByOrderId(id);
|
||||
},
|
||||
async market({ market }, _args, { dataSources }) {
|
||||
return (<OrderAPI>dataSources.orderApi).getMarketByName(market);
|
||||
return (<MarketAPI>dataSources.marketApi).getMarketByName(market);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { IResolverObject } from 'graphql-tools';
|
||||
import { Api2DataSource } from '../datasources/api2-datasource';
|
||||
import { OrderAPI } from '../datasources/order-api';
|
||||
import { ProductAPI } from '../datasources/product-api';
|
||||
import { Product } from '../types/product-types';
|
||||
import { CategoryAPI } from '../datasources/category-api';
|
||||
import { Category } from '../types/category-types';
|
||||
import { KeywordAPI } from '../datasources/keyword-api';
|
||||
import { Keyword } from '../types/keyword-types';
|
||||
import { MarketAPI } from '../datasources/market-api';
|
||||
import { InteriorAPI } from '../datasources/interior-api';
|
||||
|
||||
const ProductBlacklist: IResolverObject = {
|
||||
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);
|
||||
},
|
||||
async designer({ designerId }, args, { dataSources }) {
|
||||
if (!designerId) {
|
||||
return null;
|
||||
}
|
||||
return dataSources.designerApi.getDesignerById(designerId);
|
||||
},
|
||||
async keywords({ id }, _args, { dataSources }): Promise<Array<Keyword>> {
|
||||
return (<KeywordAPI>dataSources.keywordApi).getProductKeywords(id);
|
||||
},
|
||||
async api1json({ id }, _args, { dataSources }): Promise<JSON> {
|
||||
return dataSources.api1.getProduct(id);
|
||||
async related({ id }, _args, { dataSources }) {
|
||||
return (<ProductAPI>dataSources.productApi).getRelatedProducts(id);
|
||||
},
|
||||
};
|
||||
|
||||
const PrintProduct: IResolverObject = {
|
||||
async price({ id }, { market, width, height, sku }, { dataSources }) {
|
||||
return (<Api2DataSource>dataSources.api2).getPrice(
|
||||
market,
|
||||
width,
|
||||
height,
|
||||
sku,
|
||||
);
|
||||
async interiors({ id }, _args, { dataSources }) {
|
||||
return (<InteriorAPI>dataSources.interiorApi).getPrintProductInteriors(id);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -67,3 +65,11 @@ export const productQueryTypeDefs = {
|
||||
products: getProducts,
|
||||
product: getProduct,
|
||||
};
|
||||
|
||||
// Mutations below
|
||||
|
||||
export const productMutationTypeDefs = {
|
||||
addKeyword(_, { productId, keywordId }, { dataSources }) {
|
||||
return { result: 'OK' };
|
||||
},
|
||||
};
|
||||
|
||||
+94
-8
@@ -19,6 +19,26 @@ const typeDefs = gql`
|
||||
keyword(id: Int!): Keyword
|
||||
designers(limit: 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 {
|
||||
@@ -134,8 +154,8 @@ const typeDefs = gql`
|
||||
designerPath: String
|
||||
discountType: String
|
||||
discountValue: Float
|
||||
displayHeight: Int
|
||||
displayWidth: Int
|
||||
displayHeight: Float
|
||||
displayWidth: Float
|
||||
edge: String
|
||||
frameColor: String
|
||||
framed: Int
|
||||
@@ -175,6 +195,7 @@ const typeDefs = gql`
|
||||
id: ID!
|
||||
name: String
|
||||
path: String
|
||||
pathNumeric: String
|
||||
isLeaf: Int
|
||||
depth: Int
|
||||
childCount: Int
|
||||
@@ -185,6 +206,7 @@ const typeDefs = gql`
|
||||
|
||||
type Product {
|
||||
id: ID!
|
||||
stockid: Int
|
||||
path: String!
|
||||
visible: Boolean!
|
||||
browsable: Boolean!
|
||||
@@ -196,15 +218,53 @@ const typeDefs = gql`
|
||||
designerId: Int
|
||||
designer: Designer
|
||||
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]
|
||||
# Below are for debug
|
||||
api1json: JSON
|
||||
# Below are for debug and will be removed soon
|
||||
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 {
|
||||
PHOTO_WALLPAPER
|
||||
CANVAS
|
||||
@@ -223,6 +283,11 @@ const typeDefs = gql`
|
||||
ILLUSTRATION
|
||||
}
|
||||
|
||||
enum ProductWallpaperType {
|
||||
WALLMURAL
|
||||
DESIGN
|
||||
}
|
||||
|
||||
type ProductBlacklist {
|
||||
id: ID!
|
||||
groupId: Int!
|
||||
@@ -239,7 +304,28 @@ const typeDefs = gql`
|
||||
group: ProductGroup!
|
||||
inserted: 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 {
|
||||
@@ -255,7 +341,7 @@ const typeDefs = gql`
|
||||
}
|
||||
|
||||
type Keyword {
|
||||
id: Int!
|
||||
id: ID!
|
||||
value: String!
|
||||
type: KeywordType
|
||||
products: [Product]
|
||||
@@ -321,7 +407,7 @@ orderrows example
|
||||
}
|
||||
|
||||
|
||||
|
||||
types:
|
||||
"doityourselfframe"
|
||||
"fixed"
|
||||
"tiling"
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
pathNumeric: string;
|
||||
isLeaf: number;
|
||||
depth: number;
|
||||
childCount: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Market {
|
||||
id: number;
|
||||
name: string;
|
||||
vat: number;
|
||||
currency: string;
|
||||
priceAdjustment: number;
|
||||
}
|
||||
@@ -1,13 +1,3 @@
|
||||
import { DateInput } from './types';
|
||||
|
||||
export interface Market {
|
||||
id: number;
|
||||
name: string;
|
||||
vat: number;
|
||||
currency: string;
|
||||
priceAdjustment: number;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
id: number;
|
||||
firstname: string;
|
||||
|
||||
@@ -15,6 +15,19 @@ export enum ProductType {
|
||||
PHOTO = 1,
|
||||
ILLUSTRATION = 2,
|
||||
}
|
||||
|
||||
export enum ProductWallpaperType {
|
||||
WALLMURAL = 1,
|
||||
DESIGN = 2,
|
||||
}
|
||||
|
||||
export enum Orientation {
|
||||
UNKNOWN = 0,
|
||||
PORTRAIT = 1,
|
||||
LANDSCAPE = 2,
|
||||
SQUARE = 3,
|
||||
}
|
||||
|
||||
export interface ProductBlacklist {
|
||||
id: number;
|
||||
groupId: number;
|
||||
@@ -34,16 +47,44 @@ export interface PrintProduct {
|
||||
|
||||
export interface Product {
|
||||
id: number;
|
||||
stockid: number;
|
||||
path: string;
|
||||
visible: boolean;
|
||||
browsable: boolean;
|
||||
inserted: Date;
|
||||
updated?: Date;
|
||||
orientation?: Orientation;
|
||||
printProducts: [PrintProduct];
|
||||
blacklisting: [ProductBlacklist];
|
||||
type: [ProductType];
|
||||
api1json: JSON;
|
||||
fields_json: JSON;
|
||||
fields: ProductFields;
|
||||
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 {
|
||||
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user