diff --git a/aws/cloudformation-templates/base/tables.yaml b/aws/cloudformation-templates/base/tables.yaml index b6031a818..519cffc4b 100644 --- a/aws/cloudformation-templates/base/tables.yaml +++ b/aws/cloudformation-templates/base/tables.yaml @@ -44,14 +44,21 @@ Resources: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - - - AttributeName: "id" + - AttributeName: "id" + AttributeType: "S" + - AttributeName: "name" AttributeType: "S" KeySchema: - - - AttributeName: "id" + - AttributeName: "id" KeyType: "HASH" BillingMode: "PAY_PER_REQUEST" + GlobalSecondaryIndexes: + - IndexName: name-index + KeySchema: + - AttributeName: "name" + KeyType: "HASH" + Projection: + ProjectionType: ALL ExperimentStrategyTable: Type: AWS::DynamoDB::Table diff --git a/images/product_image_coming_soon.png b/images/product_image_coming_soon.png new file mode 100644 index 000000000..c61ffd99f Binary files /dev/null and b/images/product_image_coming_soon.png differ diff --git a/src/.env b/src/.env index f8be3fb9b..12a05fe4c 100644 --- a/src/.env +++ b/src/.env @@ -6,10 +6,16 @@ APPDATA=/tmp AWS_REGION=us-west-2 AWS_DEFAULT_REGION=us-west-2 +# For services that depend on DDB, you can run it locally or connect to +# the real DDB running in your AWS account. To connect to it locally, +# uncomment the following line. Otherwise, comment the following line +# and setup your AWS credentials in environment variables. +DDB_ENDPOINT_OVERRIDE=http://ddb:3001 + # Product service variables: -# DynamoDB table names -DDB_TABLE_PRODUCTS= -DDB_TABLE_CATEGORIES= +# DynamoDB table names (if connecting to DDB in your AWS account, change accordingly) +DDB_TABLE_PRODUCTS=products +DDB_TABLE_CATEGORIES=categories # Root URL to use when building fully qualified URLs to product detail view WEB_ROOT_URL=http://localhost:8080 # Image root URL to use when building fully qualified URLs to product images diff --git a/src/README.md b/src/README.md index 7c6aa8a5f..b03dd65d9 100644 --- a/src/README.md +++ b/src/README.md @@ -14,6 +14,8 @@ Docker Compose will load the [.env](./.env) file to resolve environment variable Some services, such as the [products](./products) and [recommendations](./recommendations) services, need to access AWS services running in your AWS account from your local machine. Given the differences between these container setups, different approaches are needed to pass in the AWS credentials needed to make these connections. For example, for the recommendations service we can map your local `~./.aws` configuration directory into the container's `/root` directory so the AWS SDK in the container can pick up the credentials it needs. Alternatively, since the products service is packaged from a [scratch image](https://hub.docker.com/_/scratch), credentials must be passed using the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` environment variables. In this case, rather than setting these variables in `.env` and risk exposing these values, consider setting these three variables in your shell environment. The following command can be used to obtain a session token which can be used to set your environment variables in your shell. +> DynamoDB is one dependency that can be run locally rather than connecting to the real DynamoDB. This makes it easier to run services like the [products](./products) service completely local to you computer. + ```console foo@bar:~$ aws sts get-session-token ``` diff --git a/src/aws-lambda/retaildemostore-lambda-load-products/src/main.go b/src/aws-lambda/retaildemostore-lambda-load-products/src/main.go index b906da45f..a5436fb28 100644 --- a/src/aws-lambda/retaildemostore-lambda-load-products/src/main.go +++ b/src/aws-lambda/retaildemostore-lambda-load-products/src/main.go @@ -23,17 +23,11 @@ import ( "gopkg.in/yaml.v2" ) -type MyEvent struct { - Table string `json:"table"` - Bucket string `json:"bucket"` - File string `json:"file"` - Datatype string `json:"datatype"` -} - // Product Struct // using omitempty as a DynamoDB optimization to create indexes type Product struct { ID string `json:"id" yaml:"id"` + URL string `json:"url" yaml:"url"` SK string `json:"sk" yaml:"sk"` Name string `json:"name" yaml:"name"` Category string `json:"category" yaml:"category"` @@ -43,6 +37,7 @@ type Product struct { Image string `json:"image" yaml:"image"` Featured string `json:"featured,omitempty" yaml:"featured,omitempty"` GenderAffinity string `json:"gender_affinity,omitempty" yaml:"gender_affinity,omitempty"` + CurrentStock int `json:"current_stock" yaml:"current_stock"` } // Products Array @@ -81,6 +76,7 @@ var ( returnstring string ) +// DynamoDBPutItem - upserts item in DDB table func DynamoDBPutItem(item map[string]*dynamodb.AttributeValue, ddbtable string) { input := &dynamodb.PutItemInput{ Item: item, @@ -102,7 +98,7 @@ func loadData(s3bucket, s3file, ddbtable, datatype string) (string, error) { localfile := "/tmp/load.yaml" - log.Println("Attempting to load "+datatype+"file: ", s3bucket, s3file, localfile) + log.Println("Attempting to load "+datatype+" file: ", s3bucket, s3file, localfile) file, err := os.Create(localfile) if err != nil { @@ -158,9 +154,10 @@ func loadData(s3bucket, s3file, ddbtable, datatype string) (string, error) { log.Println("Loaded in ", time.Since(start)) - return "data loaded from" + s3bucket + s3file + ddbtable + datatype + string(time.Since(start)), nil + return "data loaded from" + s3bucket + s3file + ddbtable + datatype + time.Since(start).String(), nil } +// HandleRequest - handles Lambda request func HandleRequest(ctx context.Context, event cfn.Event) (physicalResourceID string, data map[string]interface{}, err error) { Bucket, _ := event.ResourceProperties["Bucket"].(string) File, _ := event.ResourceProperties["File"].(string) diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 229cde888..f52055c43 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -11,6 +11,15 @@ services: - dev-net ports: - "8003:80" + + ddb: + image: amazon/dynamodb-local + container_name: ddb + command: -jar DynamoDBLocal.jar -port 3001 + networks: + - dev-net + ports: + - 3001:3001 orders: container_name: orders @@ -23,6 +32,8 @@ services: products: container_name: products + depends_on: + - ddb environment: - AWS_REGION - AWS_ACCESS_KEY_ID @@ -30,7 +41,9 @@ services: - AWS_SESSION_TOKEN - DDB_TABLE_PRODUCTS - DDB_TABLE_CATEGORIES + - DDB_ENDPOINT_OVERRIDE - IMAGE_ROOT_URL + - WEB_ROOT_URL build: context: ./products networks: diff --git a/src/products/Dockerfile b/src/products/Dockerfile index 037b144d3..72c8470b8 100644 --- a/src/products/Dockerfile +++ b/src/products/Dockerfile @@ -4,6 +4,8 @@ RUN apk add --no-cache git bash RUN go get -u github.com/gorilla/mux RUN go get -u gopkg.in/yaml.v2 RUN go get -u github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute +RUN go get -u github.com/jinzhu/copier +RUN go get -u github.com/google/uuid COPY src/products-service/*.* /src/ COPY src/products-service/data/*.* /src/data/ RUN CGO_ENABLED=0 go build -o /bin/products-service diff --git a/src/products/README.md b/src/products/README.md index 542e5868f..6940db4d9 100644 --- a/src/products/README.md +++ b/src/products/README.md @@ -1,12 +1,12 @@ # Retail Demo Store Products Service -The Products web service provides a RESTful API for retrieving product information. The [Web UI](../web-ui) makes calls to this service when a user is viewing products and categories. +The Products web service provides a RESTful API for retrieving product information. The [Web UI](../web-ui) makes calls to this service when a user is viewing products and categories and the Personalize workshop connects to this service to retrieve product information for building the items dataset. -When deployed to AWS, CodePipeline is used to build and deploy the Products service as a Docker container to Amazon ECS behind an Application Load Balancer. The Products service can also be run locally in a Docker container. This makes it easier to iterate on and test changes locally before commiting. +When deployed to AWS, CodePipeline is used to build and deploy the Products service as a Docker container in Amazon ECS behind an Application Load Balancer. The Products service can also be run locally in a Docker container. This makes it easier to iterate on and test changes locally before commiting. ## Local Development -The Products service can be built and run locally (in Docker) using Docker Compose. See the [local development instructions](../) for details. **From the `../src` directory**, run the following command to build and deploy the service locally. +The Products service can be built and run locally (in Docker) using Docker Compose. See the [local development instructions](../) for details. Since the Products service has a dependency on DynamoDB as its datastore, you can either connect to DynamoDB in your AWS account or run DynamoDB [locally](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) (default). The [docker-compose.yml](../docker-compose.yml) and default [.env](../.env) is already setup to run DynamoDB locally in Docker. If you want to connect to the real DynamoDB instead, you will need to configure your AWS credentials and comment the `DDB_ENDPOINT_OVERRIDE` environment variable since it is checked first. **From the `../src` directory**, run the following command to build and deploy the service locally. ```console foo@bar:~$ docker-compose up --build products diff --git a/src/products/src/products-service/aws.go b/src/products/src/products-service/aws.go index 075160b9a..f39562f76 100644 --- a/src/products/src/products-service/aws.go +++ b/src/products/src/products-service/aws.go @@ -4,25 +4,41 @@ package main import ( + "log" "os" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/ssm" ) -//var sess *session.Session var sess, err = session.NewSession(&aws.Config{}) -// read DynamoDB tables env variable -var ddb_table_products = os.Getenv("DDB_TABLE_PRODUCTS") -var ddb_table_categories = os.Getenv("DDB_TABLE_CATEGORIES") +// DynamoDB table names passed via environment +var ddbTableProducts = os.Getenv("DDB_TABLE_PRODUCTS") +var ddbTableCategories = os.Getenv("DDB_TABLE_CATEGORIES") -var dynamoclient = dynamodb.New(sess) -var ssm_client = ssm.New(sess) +// Allow DDB endpoint to be overridden to support amazon/dynamodb-local +var ddbEndpointOverride = os.Getenv("DDB_ENDPOINT_OVERRIDE") +var runningLocal bool -// Connect Stuff +var dynamoClient *dynamodb.DynamoDB + +// Initialize clients func init() { - + if len(ddbEndpointOverride) > 0 { + runningLocal = true + log.Println("Creating DDB client with endpoint override: ", ddbEndpointOverride) + creds := credentials.NewStaticCredentials("does", "not", "matter") + awsConfig := &aws.Config{ + Credentials: creds, + Region: aws.String("us-east-1"), + Endpoint: aws.String(ddbEndpointOverride), + } + dynamoClient = dynamodb.New(sess, awsConfig) + } else { + runningLocal = false + dynamoClient = dynamodb.New(sess) + } } diff --git a/src/products/src/products-service/category.go b/src/products/src/products-service/category.go index 0ea39eeeb..4d06fcf5a 100644 --- a/src/products/src/products-service/category.go +++ b/src/products/src/products-service/category.go @@ -4,6 +4,7 @@ package main // Category Struct +// IMPORTANT: if you change the shape of this struct, be sure to update the retaildemostore-lambda-load-products Lambda too! type Category struct { ID string `json:"id" yaml:"id"` URL string `json:"url" yaml:"url"` @@ -11,5 +12,8 @@ type Category struct { Image string `json:"image" yaml:"image"` } +// Initialized - indicates if instance has been initialized or not +func (c *Category) Initialized() bool { return c != nil && len(c.ID) > 0 } + // Categories Array type Categories []Category diff --git a/src/products/src/products-service/data/products.yaml b/src/products/src/products-service/data/products.yaml index b803c3a2a..0cc0c7af2 100644 --- a/src/products/src/products-service/data/products.yaml +++ b/src/products/src/products-service/data/products.yaml @@ -6,6 +6,7 @@ price: 109.99 image: "1.jpg" featured: true + current_stock: 15 - id: 2 name: Striped Shirt category: apparel @@ -15,6 +16,7 @@ image: "1.jpg" featured: true gender_affinity: F + current_stock: 12 - id: 3 name: Beard Oil category: beauty @@ -24,6 +26,7 @@ image: "1.jpg" featured: true gender_affinity: M + current_stock: 15 - id: 4 name: Fitness Tracker category: electronics @@ -32,6 +35,7 @@ price: 89.99 image: "1.jpg" featured: true + current_stock: 11 - id: 5 name: Super Knit Sneakers category: footwear @@ -41,6 +45,7 @@ image: "1.jpg" featured: true gender_affinity: M + current_stock: 7 - id: 6 name: Coffee Gift Package category: housewares @@ -49,6 +54,7 @@ price: 39.99 image: "1.jpg" featured: true + current_stock: 13 - id: 7 name: Gold Anchor Womens Bracelet category: jewelry @@ -57,6 +63,7 @@ price: 49.99 image: "1.jpg" gender_affinity: F + current_stock: 6 - id: 8 name: LED Dog Collar category: outdoors @@ -64,6 +71,7 @@ description: Brightly lit collar for your loved pet. price: 29.99 image: "1.jpg" + current_stock: 6 - id: 9 name: Black Leather Shoulder Bag category: accessories @@ -72,6 +80,7 @@ price: 109.99 image: "2.jpg" gender_affinity: F + current_stock: 11 - id: 10 name: Classic T- Shirt category: apparel @@ -79,6 +88,7 @@ description: A classic look for the summer season. price: 9.99 image: "2.jpg" + current_stock: 13 - id: 11 name: Odorless Essential Oil category: beauty @@ -86,6 +96,7 @@ description: This essential oil in liquid and balm forms smooths rough skin without a scent. price: 9.99 image: "2.jpg" + current_stock: 3 - id: 12 name: Lightning Cable category: electronics @@ -93,6 +104,7 @@ description: Charge your phone on the go. price: 89.99 image: "2.jpg" + current_stock: 6 - id: 13 name: LED Leather Hi-Tops category: footwear @@ -101,6 +113,7 @@ price: 129.99 image: "2.jpg" gender_affinity: M + current_stock: 13 - id: 14 name: Coffee Gift Package category: housewares @@ -108,6 +121,7 @@ description: Mug and Coffee gift set combination package. price: 39.99 image: "2.jpg" + current_stock: 13 - id: 15 name: Leather Anchor Mens Bracelet category: jewelry @@ -117,6 +131,7 @@ image: "2.jpg" gender_affinity: M featured: true + current_stock: 7 - id: 16 name: Neon Colored Fishing Lure category: outdoors @@ -124,6 +139,7 @@ description: This lure will be hard to pass up with its bright neon green and yellow colors. price: 9.99 image: "2.jpg" + current_stock: 13 - id: 17 name: Black Women's Purse category: accessories @@ -132,6 +148,7 @@ price: 109.99 image: "3.jpg" gender_affinity: F + current_stock: 5 - id: 18 name: Accent Scarf category: apparel @@ -140,6 +157,7 @@ price: 9.99 image: "3.jpg" gender_affinity: F + current_stock: 12 - id: 19 name: Vitamin E Oil category: beauty @@ -148,6 +166,7 @@ price: 9.99 image: "3.jpg" gender_affinity: M + current_stock: 9 - id: 20 name: Black Bluetooth Portable Speaker category: electronics @@ -155,6 +174,7 @@ description: Blast your tunes anywhere with this Bluetooth wireless speaker. price: 89.99 image: "3.jpg" + current_stock: 2 - id: 21 name: Knit Black Sneakers category: footwear @@ -163,6 +183,7 @@ price: 129.99 image: "3.jpg" gender_affinity: M + current_stock: 7 - id: 22 name: Coffee Gift Package category: housewares @@ -170,6 +191,7 @@ description: Mug and Coffee gift set combination package. price: 39.99 image: "3.jpg" + current_stock: 14 - id: 23 name: Diamond Studded Classic Gold Bracelet category: jewelry @@ -178,6 +200,7 @@ price: 49.99 image: "3.jpg" gender_affinity: F + current_stock: 4 - id: 24 name: Classic Fishing Pole category: outdoors @@ -185,6 +208,7 @@ description: Land a big one with our classic fishing rod and reel. price: 9.99 image: "3.jpg" + current_stock: 2 - id: 25 name: Red Leather Purse category: accessories @@ -194,6 +218,7 @@ image: "4.jpg" gender_affinity: F featured: true + current_stock: 7 - id: 26 name: Blue Shirt category: apparel @@ -203,6 +228,7 @@ image: "4.jpg" gender_affinity: M featured: true + current_stock: 7 - id: 27 name: Bath Bomb category: beauty @@ -212,6 +238,7 @@ image: "10.jpg" gender_affinity: F featured: true + current_stock: 15 - id: 28 name: Blue Mushroom Wireless Speaker category: electronics @@ -219,6 +246,7 @@ description: This funky wireless speaker features a suction cup to temporarily mount on any flat surface. price: 89.99 image: "4.jpg" + current_stock: 11 - id: 29 name: LED Leather Hi-Tops category: footwear @@ -227,6 +255,7 @@ price: 129.99 image: "4.jpg" gender_affinity: M + current_stock: 2 - id: 30 name: Nutmeg Grater category: housewares @@ -234,6 +263,7 @@ description: Grate fresh nutmeg in your coffee and desserts using this compact grater. price: 39.99 image: "4.jpg" + current_stock: 12 - id: 31 name: Black Bead Womens Bracelet category: jewelry @@ -242,6 +272,7 @@ price: 49.99 image: "4.jpg" gender_affinity: F + current_stock: 3 - id: 32 name: Classic Fishing Reel category: outdoors @@ -249,6 +280,7 @@ description: Add this attractive gold and silver fishing reel to an existing pole. price: 29.99 image: "4.jpg" + current_stock: 5 - id: 33 name: Brown Women's Purse category: accessories @@ -257,6 +289,7 @@ price: 109.99 image: "5.jpg" gender_affinity: F + current_stock: 5 - id: 34 name: Classic Bomber Jacket category: apparel @@ -265,6 +298,7 @@ price: 159.99 image: "5.jpg" gender_affinity: M + current_stock: 12 - id: 35 name: Vitamin E Oil category: beauty @@ -272,6 +306,7 @@ description: Removes scars and blemishes and moisturizes skin. price: 9.99 image: "5.jpg" + current_stock: 2 - id: 36 name: Exercise Headphones category: electronics @@ -280,6 +315,7 @@ price: 19.99 image: "5.jpg" featured: true + current_stock: 7 - id: 37 name: Sporty Deck Shoe category: footwear @@ -287,6 +323,7 @@ description: Whether on deck or on the town, these blue deck shoes with brown leather accents will draw a salute. price: 129.99 image: "5.jpg" + current_stock: 8 - id: 38 name: Coffee Gift Package category: housewares @@ -294,6 +331,7 @@ description: Mug and Coffee gift set combination package. price: 39.99 image: "5.jpg" + current_stock: 11 - id: 39 name: Turquoise Womens Necklace category: jewelry @@ -302,6 +340,7 @@ price: 49.99 image: "5.jpg" gender_affinity: F + current_stock: 7 - id: 40 name: Spotted Fishing Lure category: outdoors @@ -309,6 +348,7 @@ description: Land a big one with this blackspotted lure with green bushy tail. price: 9.99 image: "5.jpg" + current_stock: 7 - id: 41 name: Tan Leather Shoulder Bag category: accessories @@ -317,6 +357,7 @@ price: 109.99 image: "6.jpg" gender_affinity: F + current_stock: 13 - id: 42 name: Multi Color Socks category: apparel @@ -325,6 +366,7 @@ price: 9.99 image: "6.jpg" gender_affinity: F + current_stock: 3 - id: 43 name: Beauty Mask category: beauty @@ -332,6 +374,7 @@ description: Remove dirt and revitalize skin with this black kelp mask. price: 9.99 image: "9.jpg" + current_stock: 12 - id: 44 name: Portable Speaker category: electronics @@ -339,6 +382,7 @@ description: Take this compact portable speaker with you when you travel or when you're outdoors. price: 89.99 image: "6.jpg" + current_stock: 10 - id: 45 name: Classic Black Leather Hi-Tops category: footwear @@ -346,6 +390,7 @@ description: Casual or dressy, this black leather hi-top has you covered. price: 129.99 image: "6.jpg" + current_stock: 9 - id: 46 name: Smoothie Blender category: housewares @@ -353,6 +398,7 @@ description: Blend up flavorful smoothies using this attractive blender price: 39.99 image: "6.jpg" + current_stock: 13 - id: 47 name: Gold Bracelt with Multi-Color Tassels category: jewelry @@ -361,6 +407,7 @@ price: 49.99 image: "6.jpg" gender_affinity: F + current_stock: 3 - id: 48 name: Silver Fishing Lure category: outdoors @@ -369,6 +416,7 @@ price: 9.99 image: "6.jpg" featured: true + current_stock: 5 - id: 49 name: Light Brown Leather Lace-Up Boot category: footwear @@ -376,6 +424,7 @@ description: Sturdy enough for the outdoors yet stylish to wear out on the town. price: 89.95 image: "11.jpg" + current_stock: 15 - id: 50 name: Blue Wind Breaker Jacket category: apparel @@ -384,6 +433,7 @@ price: 79.95 image: "25.jpg" gender_affinity: M + current_stock: 11 - id: 51 name: Ceramic Mixing Bowls category: housewares @@ -392,6 +442,7 @@ price: 49.95 image: "7.jpg" featured: true + current_stock: 7 - id: 52 name: Turquoise Globe Earrings category: jewelry @@ -401,6 +452,7 @@ image: "7.jpg" gender_affinity: F featured: true + current_stock: 14 - id: 53 name: Watermelon Flavored Lip Balm category: beauty @@ -409,6 +461,7 @@ price: 9.99 image: "8.jpg" gender_affinity: F + current_stock: 12 - id: 54 name: Over-Ear Headphones category: electronics @@ -416,6 +469,7 @@ description: Immerse youself in listening pleasure with these high-quality over-the-ear headphones. price: 129.99 image: "7.jpg" + current_stock: 8 - id: 55 name: Pink Leather Purse category: accessories @@ -424,6 +478,7 @@ price: 89.99 image: "9.jpg" gender_affinity: F + current_stock: 9 - id: 56 name: Red Fishing Lure category: outdoors @@ -431,6 +486,7 @@ description: Every tackle box needs a silver and red reflective fishing lure. price: 9.99 image: "12.jpg" + current_stock: 9 - id: 57 name: Eyeshadow Palette - Set of 3 Palettes category: beauty @@ -438,6 +494,7 @@ description: Perfect for trialling different shades, each palette contains 4 different shades. price: 145.00 image: "11.jpg" + current_stock: 19 - id: 58 name: Waterproof Eyeliner and Mascara category: beauty @@ -445,6 +502,7 @@ description: In stylish yellow, guaranteed to stay on in sauna and car-wash. price: 27.00 image: "12.jpg" + current_stock: 9 - id: 59 name: Divine Shine - Satin Rose Lipstick Set category: beauty @@ -452,6 +510,7 @@ description: You get not only lipstick, but that other thing with the mirror. Brilliant! price: 35.00 image: "13.jpg" + current_stock: 1 - id: 60 name: Gloss Bomb Universal Lip Luminizer category: beauty @@ -459,6 +518,7 @@ description: 100% guaranteed universal and 100% luminous. price: 19.00 image: "14.jpg" + current_stock: 12 - id: 61 name: 7-in-1 Daily Wear Palette Essentials category: beauty @@ -466,6 +526,7 @@ description: You get a whole bunch of stuff and it is super good quality too. price: 103.00 image: "15.jpg" + current_stock: 2 - id: 62 name: Eye Care Set - Sparkle Gloss Eyes and Lashes category: beauty @@ -473,6 +534,7 @@ description: Your eyes will dazzle and gloss and shine and fizz pop. price: 95.00 image: "16.jpg" + current_stock: 2 - id: 63 name: 15 Piece Makeup Brush Set with Fold Up Leather Case category: beauty @@ -483,6 +545,7 @@ gender_affinity: F image_license: "Free for Commercial Use" link: https://www.pikrepo.com/fmvtc/black-makeup-brush-set-in-bag + current_stock: 2 - id: 64 name: Lovely Blue Mascara category: beauty @@ -493,6 +556,7 @@ gender_affinity: F image_license: CC0 link: https://pxhere.com/en/photo/57398 + current_stock: 2 - id: 65 name: Nail Varnish for Conquerors of Hearts category: beauty @@ -503,6 +567,7 @@ gender_affinity: F image_license: CC0 link: https://www.needpix.com/photo/1711500/nail-varnish-nail-design-cosmetics-manicure-fingernails-paint-toe-nails-fashionable-beauty + current_stock: 3 - id: 66 name: Rose Pink Blush Brush category: beauty @@ -513,6 +578,7 @@ gender_affinity: F image_license: Free for commercial use - just do not resell as a stock photo link: https://pixabay.com/photos/rouge-brush-cosmetics-rouge-brush-2092439/ + current_stock: 4 - id: 67 name: "Subtle and Fresh: Palette of 15 Concealers" category: beauty @@ -523,6 +589,7 @@ gender_affinity: F image_license: Free for commercial use link: https://www.pxfuel.com/en/free-photo-xidzw + current_stock: 6 - id: 68 name: Deep Disguise Concealer category: beauty @@ -533,6 +600,7 @@ gender_affinity: F image_license: CC0 link: https://commons.m.wikimedia.org/wiki/File:Tcsfoundationlogo.jpg + current_stock: 7 - id: 69 name: Classic Bombshell Lipstick category: beauty @@ -543,6 +611,7 @@ gender_affinity: F image_license: Free for commercial use - just do not resell as a stock photo link: https://pixabay.com/photos/lipstick-lips-makeup-cosmetics-5559338/ + current_stock: 11 - id: 70 name: Intense Matte Lipstick category: beauty @@ -553,6 +622,7 @@ gender_affinity: F image_license: Unsplash - free for commercial use link: https://unsplash.com/photos/rjB_1MT6G18 + current_stock: 12 - id: 71 name: 4-Piece Makeup Brush Set category: beauty @@ -563,6 +633,7 @@ gender_affinity: F image_license: Free for commercial use - just do not resell as a stock photo link: https://pixabay.com/photos/maciag-brush-makeup-brushes-5208359/ + current_stock: 4 - id: 72 name: Gangster-Girl Lipstick category: beauty @@ -573,6 +644,7 @@ gender_affinity: F image_license: Free for commercial use link: https://www.pikrepo.com/fyvwn/red-and-gold-lipstick-on-white-background + current_stock: 7 - id: 73 name: Lip Brush category: beauty @@ -583,6 +655,7 @@ gender_affinity: F image_license: Free for commercial use link: https://unsplash.com/photos/qbo7DPBvnV0 + current_stock: 2 - id: 74 name: Precious Cargo Makeup Containers category: beauty @@ -593,6 +666,7 @@ gender_affinity: F image_license: CC0 link: https://pixy.org/5203022/ + current_stock: 20 - id: 75 name: Burn! Lipstick category: beauty @@ -603,6 +677,7 @@ gender_affinity: F image_license: Public domain link: https://www.pikist.com/free-photo-xvcbj + current_stock: 12 - id: 76 name: Grandma's Mascara category: beauty @@ -613,6 +688,7 @@ gender_affinity: F image_license: Free for commercial use link: https://www.pickpik.com/cosmetics-make-up-makeup-beauty-color-eyes-138539 + current_stock: 13 - id: 77 name: Camera Tripod category: electronics @@ -623,6 +699,7 @@ gender_affinity: F image_license: CC0 link: https://www.needpix.com/photo/908201/gorillapod-with-camera-free-pictures-free-photos-free-images-royalty-free + current_stock: 10 - id: 78 name: Nice Stripy Blouse category: apparel @@ -633,6 +710,7 @@ gender_affinity: F image_license: Made by Dae.mn link: + current_stock: 10 - id: 79 name: Pocket Powder Case category: beauty @@ -643,6 +721,7 @@ gender_affinity: F image_license: Public domain link: https://www.pikist.com/free-photo-ixyyz + current_stock: 12 - id: 80 name: Freestanding Glass Makeup Mirror category: housewares @@ -653,6 +732,7 @@ gender_affinity: F image_license: CC0 link: https://www.needpix.com/photo/download/1336308/mirror-small-reflection-decoration-modern-design-frame-round-shop + current_stock: 11 - id: 81 name: Perfect grey sofa category: housewares @@ -662,6 +742,7 @@ image: "9.jpg" image_license: Free for commercial use - just do not resell as a stock photo link: https://pixabay.com/photos/furniture-modern-luxury-indoors-3271762/ + current_stock: 10 - id: 82 name: Perfect cushions category: housewares @@ -671,6 +752,7 @@ image: "10.jpg" image_license: Free for commercial use - just do not resell as a stock photo link: https://pixabay.com/fr/photos/oreillers-patron-lit-int%C3%A9rieur-4326131/ + current_stock: 11 - id: 83 name: Classic coat-rack category: housewares @@ -680,6 +762,7 @@ image: "11.jpg" image_license: Free for commercial use - just do not resell as a stock photo link: https://pixabay.com/photos/hat-coat-rack-wing-pet-fashion-2176837/ + current_stock: 3 - id: 84 name: Spare bookshelves category: housewares @@ -689,3 +772,4 @@ image: "12.jpg" image_license: CC0 link: https://www.needpix.com/photo/download/1856333/shelf-white-living-world-bookshelf-books-bookshelves-set-up-living-room-book + current_stock: 3 diff --git a/src/products/src/products-service/handlers.go b/src/products/src/products-service/handlers.go index 89d7f0c08..fd513bde8 100644 --- a/src/products/src/products-service/handlers.go +++ b/src/products/src/products-service/handlers.go @@ -4,51 +4,79 @@ package main import ( - "os" - "log" "encoding/json" + "errors" "fmt" + "io" + "io/ioutil" + "log" "net/http" + "os" "github.com/gorilla/mux" "strconv" ) -var image_root_url = os.Getenv("IMAGE_ROOT_URL") +var imageRootURL = os.Getenv("IMAGE_ROOT_URL") +var missingImageFile = "product_image_coming_soon.png" -func FullyQualifyCategoryImageUrl(c Category) Category { - log.Println("Fully qualifying category image URL") - c.Image = image_root_url + c.Name + "/" + c.Image - return c +// initResponse +func initResponse(w *http.ResponseWriter) { + (*w).Header().Set("Access-Control-Allow-Origin", "*") + (*w).Header().Set("Content-Type", "application/json; charset=UTF-8") } -func FullyQualifyCategoryImageUrls(categories Categories) Categories { - log.Println("Fully qualifying category image URLs") - ret := make([]Category, len(categories)) +func fullyQualifyImageURLs(r *http.Request) bool { + param := r.URL.Query().Get("fullyQualifyImageUrls") + if len(param) == 0 { + param = "1" + } + + fullyQualify, _ := strconv.ParseBool(param) + return fullyQualify +} - for i, c := range categories { - c.Image = image_root_url + c.Name + "/" + c.Image - ret[i] = c +// fullyQualifyCategoryImageURL - fully qualifies image URL for a category +func fullyQualifyCategoryImageURL(r *http.Request, c *Category) { + if fullyQualifyImageURLs(r) { + if len(c.Image) > 0 && c.Image != missingImageFile { + c.Image = imageRootURL + c.Name + "/" + c.Image + } else { + c.Image = imageRootURL + missingImageFile + } + } else if len(c.Image) == 0 || c.Image == missingImageFile { + c.Image = missingImageFile } - return ret } -func FullyQualifyProductImageUrl(p Product) Product { - log.Println("Fully qualifying product image URL") - p.Image = image_root_url + p.Category + "/" + p.Image - return p +// fullyQualifyCategoryImageURLs - fully qualifies image URL for categories +func fullyQualifyCategoryImageURLs(r *http.Request, categories *Categories) { + for i := range *categories { + category := &((*categories)[i]) + fullyQualifyCategoryImageURL(r, category) + } } -func FullyQualifyProductImageUrls(products Products) Products { - log.Println("Fully qualifying product image URLs") - ret := make([]Product, len(products)) +// fullyQualifyProductImageURL - fully qualifies image URL for a product +func fullyQualifyProductImageURL(r *http.Request, p *Product) { + if fullyQualifyImageURLs(r) { + if len(p.Image) > 0 && p.Image != missingImageFile { + p.Image = imageRootURL + p.Category + "/" + p.Image + } else { + p.Image = imageRootURL + missingImageFile + } + } else if len(p.Image) == 0 || p.Image == missingImageFile { + p.Image = missingImageFile + } +} - for i, p := range products { - p.Image = image_root_url + p.Category + "/" + p.Image - ret[i] = p +// fullyQualifyProductImageURLs - fully qualifies image URLs for all products +func fullyQualifyProductImageURLs(r *http.Request, products *Products) { + for i := range *products { + product := &((*products)[i]) + fullyQualifyProductImageURL(r, product) } - return ret } // Index Handler @@ -58,18 +86,11 @@ func Index(w http.ResponseWriter, r *http.Request) { // ProductIndex Handler func ProductIndex(w http.ResponseWriter, r *http.Request) { + initResponse(&w) - enableCors(&w) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) + ret := RepoFindALLProducts() - ret := RepoFindALLProduct() - - fullyQualify, _ := strconv.ParseBool(r.URL.Query().Get("fullyQualifyImageUrls")) - if fullyQualify { - ret = FullyQualifyProductImageUrls(ret) - } + fullyQualifyProductImageURLs(r, &ret) if err := json.NewEncoder(w).Encode(ret); err != nil { panic(err) @@ -78,18 +99,11 @@ func ProductIndex(w http.ResponseWriter, r *http.Request) { // CategoryIndex Handler func CategoryIndex(w http.ResponseWriter, r *http.Request) { - - enableCors(&w) - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) + initResponse(&w) ret := RepoFindALLCategories() - fullyQualify, _ := strconv.ParseBool(r.URL.Query().Get("fullyQualifyImageUrls")) - if fullyQualify { - ret = FullyQualifyCategoryImageUrls(ret) - } + fullyQualifyCategoryImageURLs(r, &ret) // TODO if err := json.NewEncoder(w).Encode(ret); err != nil { @@ -99,23 +113,19 @@ func CategoryIndex(w http.ResponseWriter, r *http.Request) { // ProductShow Handler func ProductShow(w http.ResponseWriter, r *http.Request) { - - enableCors(&w) + initResponse(&w) vars := mux.Vars(r) - productID, err := strconv.Atoi(vars["productID"]) - - if err != nil { - panic(err) - } - ret := RepoFindProduct(productID) + ret := RepoFindProduct(vars["productID"]) - fullyQualify, _ := strconv.ParseBool(r.URL.Query().Get("fullyQualifyImageUrls")) - if fullyQualify { - ret = FullyQualifyProductImageUrl(ret) + if !ret.Initialized() { + http.Error(w, "Product not found", http.StatusNotFound) + return } + fullyQualifyProductImageURL(r, &ret) + if err := json.NewEncoder(w).Encode(ret); err != nil { panic(err) } @@ -123,18 +133,34 @@ func ProductShow(w http.ResponseWriter, r *http.Request) { // CategoryShow Handler func CategoryShow(w http.ResponseWriter, r *http.Request) { + initResponse(&w) - enableCors(&w) + vars := mux.Vars(r) + + ret := RepoFindCategory(vars["categoryID"]) + + if !ret.Initialized() { + http.Error(w, "Category not found", http.StatusNotFound) + return + } + + fullyQualifyCategoryImageURL(r, &ret) + + if err := json.NewEncoder(w).Encode(ret); err != nil { + panic(err) + } +} + +// ProductInCategory Handler +func ProductInCategory(w http.ResponseWriter, r *http.Request) { + initResponse(&w) vars := mux.Vars(r) categoryName := vars["categoryName"] ret := RepoFindProductByCategory(categoryName) - fullyQualify, _ := strconv.ParseBool(r.URL.Query().Get("fullyQualifyImageUrls")) - if fullyQualify { - ret = FullyQualifyProductImageUrls(ret) - } + fullyQualifyProductImageURLs(r, &ret) if err := json.NewEncoder(w).Encode(ret); err != nil { panic(err) @@ -143,22 +169,185 @@ func CategoryShow(w http.ResponseWriter, r *http.Request) { // ProductFeatured Handler func ProductFeatured(w http.ResponseWriter, r *http.Request) { - - enableCors(&w) + initResponse(&w) ret := RepoFindFeatured() - fullyQualify, _ := strconv.ParseBool(r.URL.Query().Get("fullyQualifyImageUrls")) - if fullyQualify { - ret = FullyQualifyProductImageUrls(ret) - } + fullyQualifyProductImageURLs(r, &ret) if err := json.NewEncoder(w).Encode(ret); err != nil { panic(err) } } -// enableCors -func enableCors(w *http.ResponseWriter) { - (*w).Header().Set("Access-Control-Allow-Origin", "*") +func validateProduct(product *Product) error { + if len(product.Name) == 0 { + return errors.New("Product name is required") + } + + if product.Price < 0 { + return errors.New("Product price cannot be a negative value") + } + + if product.CurrentStock < 0 { + return errors.New("Product current stock cannot be a negative value") + } + + if len(product.Category) > 0 { + categories := RepoFindCategoriesByName(product.Category) + if len(categories) == 0 { + return errors.New("Invalid product category; does not exist") + } + } + + return nil +} + +// UpdateProduct - updates a product +func UpdateProduct(w http.ResponseWriter, r *http.Request) { + initResponse(&w) + + vars := mux.Vars(r) + + print(vars) + var product Product + + body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) + if err != nil { + panic(err) + } + if err := r.Body.Close(); err != nil { + panic(err) + } + if err := json.Unmarshal(body, &product); err != nil { + http.Error(w, "Invalid request payload", http.StatusUnprocessableEntity) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + } + + if err := validateProduct(&product); err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + existingProduct := RepoFindProduct(vars["productID"]) + if !existingProduct.Initialized() { + // Existing product does not exist + http.Error(w, "Product not found", http.StatusNotFound) + return + } + + if err := RepoUpdateProduct(&existingProduct, &product); err != nil { + http.Error(w, "Internal error updating product", http.StatusInternalServerError) + return + } + + fullyQualifyProductImageURL(r, &product) + + if err := json.NewEncoder(w).Encode(product); err != nil { + panic(err) + } +} + +// UpdateInventory - updates stock quantity for one item +func UpdateInventory(w http.ResponseWriter, r *http.Request) { + initResponse(&w) + + vars := mux.Vars(r) + + var inventory Inventory + + body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) + if err != nil { + panic(err) + } + if err := r.Body.Close(); err != nil { + panic(err) + } + log.Println("UpdateInventory Body ", body) + + if err := json.Unmarshal(body, &inventory); err != nil { + http.Error(w, "Invalid request payload", http.StatusUnprocessableEntity) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + } + + log.Println("UpdateInventory --> ", inventory) + + // Get the current product + product := RepoFindProduct(vars["productID"]) + if !product.Initialized() { + // Existing product does not exist + http.Error(w, "Product not found", http.StatusNotFound) + return + } + + if err := RepoUpdateInventoryDelta(&product, inventory.StockDelta); err != nil { + panic(err) + } + + fullyQualifyProductImageURL(r, &product) + + if err := json.NewEncoder(w).Encode(product); err != nil { + panic(err) + } +} + +// NewProduct - creates a new Product +func NewProduct(w http.ResponseWriter, r *http.Request) { + initResponse(&w) + + var product Product + body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) + if err != nil { + panic(err) + } + if err := r.Body.Close(); err != nil { + panic(err) + } + if err := json.Unmarshal(body, &product); err != nil { + http.Error(w, "Invalid request payload", http.StatusUnprocessableEntity) + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + } + + log.Println("NewProduct ", product) + + if err := validateProduct(&product); err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + if err := RepoNewProduct(&product); err != nil { + http.Error(w, "Internal error creating product", http.StatusInternalServerError) + return + } + + fullyQualifyProductImageURL(r, &product) + + if err := json.NewEncoder(w).Encode(product); err != nil { + panic(err) + } +} + +// DeleteProduct - deletes a single product +func DeleteProduct(w http.ResponseWriter, r *http.Request) { + initResponse(&w) + + vars := mux.Vars(r) + + // Get the current product + product := RepoFindProduct(vars["productID"]) + if !product.Initialized() { + // Existing product does not exist + http.Error(w, "Product not found", http.StatusNotFound) + return + } + + if err := RepoDeleteProduct(&product); err != nil { + http.Error(w, "Internal error deleting product", http.StatusInternalServerError) + } } diff --git a/src/products/src/products-service/localdev.go b/src/products/src/products-service/localdev.go new file mode 100644 index 000000000..7845ff23b --- /dev/null +++ b/src/products/src/products-service/localdev.go @@ -0,0 +1,306 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/* + * Supports developing locally where DDB is running locally using + * amazon/dynamodb-local (Docker) or local DynamoDB. + * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html + */ + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + yaml "gopkg.in/yaml.v2" +) + +func init() { + if runningLocal { + waitForLocalDDB() + loadData() + } +} + +// waitForLocalDDB - since local DDB can take a couple seconds to startup, we give it some time. +func waitForLocalDDB() { + log.Println("Verifying that local DynamoDB is running at: ", ddbEndpointOverride) + + ddbRunning := false + + for i := 0; i < 5; i++ { + resp, _ := http.Get(ddbEndpointOverride) + + if resp != nil && resp.StatusCode >= 200 { + log.Println("Received HTTP response from local DynamoDB service!") + ddbRunning = true + break + } + + log.Println("Local DynamoDB service is not ready yet... pausing before trying again") + time.Sleep(2 * time.Second) + } + + if !ddbRunning { + log.Panic("Local DynamoDB service not responding; verify that your docker-compose .env file is setup correctly") + } +} + +func loadData() { + err := createProductsTable() + if err != nil { + log.Panic("Unable to create products table.") + } + + err = loadProducts("/bin/data/products.yaml") + if err != nil { + log.Panic("Unable to load products file.") + } + + err = createCategoriesTable() + if err != nil { + log.Panic("Unable to create categories table.") + } + + err = loadCategories("/bin/data/categories.yaml") + if err != nil { + log.Panic("Unable to load category file.") + } + + log.Println("Successfully loaded product and category data into DDB") +} + +func createProductsTable() error { + log.Println("Creating products table: ", ddbTableProducts) + + // Table definition mapped from /aws/cloudformation-templates/base/tables.yaml + input := &dynamodb.CreateTableInput{ + AttributeDefinitions: []*dynamodb.AttributeDefinition{ + { + AttributeName: aws.String("id"), + AttributeType: aws.String("S"), + }, + { + AttributeName: aws.String("category"), + AttributeType: aws.String("S"), + }, + { + AttributeName: aws.String("featured"), + AttributeType: aws.String("S"), + }, + }, + KeySchema: []*dynamodb.KeySchemaElement{ + { + AttributeName: aws.String("id"), + KeyType: aws.String("HASH"), + }, + { + AttributeName: aws.String("category"), + KeyType: aws.String("RANGE"), + }, + }, + BillingMode: aws.String("PAY_PER_REQUEST"), + LocalSecondaryIndexes: []*dynamodb.LocalSecondaryIndex{ + { + IndexName: aws.String("id-featured-index"), + KeySchema: []*dynamodb.KeySchemaElement{ + { + AttributeName: aws.String("id"), + KeyType: aws.String("HASH"), + }, + { + AttributeName: aws.String("featured"), + KeyType: aws.String("RANGE"), + }, + }, + Projection: &dynamodb.Projection{ + ProjectionType: aws.String("ALL"), + }, + }, + }, + GlobalSecondaryIndexes: []*dynamodb.GlobalSecondaryIndex{ + { + IndexName: aws.String("category-index"), + KeySchema: []*dynamodb.KeySchemaElement{ + { + AttributeName: aws.String("category"), + KeyType: aws.String("HASH"), + }, + }, + Projection: &dynamodb.Projection{ + ProjectionType: aws.String("ALL"), + }, + }, + }, + TableName: aws.String(ddbTableProducts), + } + + _, err := dynamoClient.CreateTable(input) + if err != nil { + log.Println("Error creating products table: ", ddbTableProducts) + + if aerr, ok := err.(awserr.Error); ok { + if aerr.Code() == dynamodb.ErrCodeResourceInUseException { + log.Println("Table already exists; continuing") + err = nil + } else { + log.Println(err.Error()) + } + } else { + log.Println(err.Error()) + } + } + + return err +} + +func loadProducts(filename string) error { + start := time.Now() + + log.Println("Loading products from file: ", filename) + + var r Products + + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + err = yaml.Unmarshal(bytes, &r) + if err != nil { + return err + } + + for _, item := range r { + + av, err := dynamodbattribute.MarshalMap(item) + + if err != nil { + return err + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(ddbTableProducts), + } + + _, err = dynamoClient.PutItem(input) + if err != nil { + fmt.Println("Got error calling PutItem:") + fmt.Println(err.Error()) + + } + + } + + log.Println("Products loaded in ", time.Since(start)) + + return nil +} + +func createCategoriesTable() error { + log.Println("Creating categories table: ", ddbTableCategories) + + // Table definition mapped from /aws/cloudformation-templates/base/tables.yaml + input := &dynamodb.CreateTableInput{ + AttributeDefinitions: []*dynamodb.AttributeDefinition{ + { + AttributeName: aws.String("id"), + AttributeType: aws.String("S"), + }, + { + AttributeName: aws.String("name"), + AttributeType: aws.String("S"), + }, + }, + KeySchema: []*dynamodb.KeySchemaElement{ + { + AttributeName: aws.String("id"), + KeyType: aws.String("HASH"), + }, + }, + BillingMode: aws.String("PAY_PER_REQUEST"), + GlobalSecondaryIndexes: []*dynamodb.GlobalSecondaryIndex{ + { + IndexName: aws.String("name-index"), + KeySchema: []*dynamodb.KeySchemaElement{ + { + AttributeName: aws.String("name"), + KeyType: aws.String("HASH"), + }, + }, + Projection: &dynamodb.Projection{ + ProjectionType: aws.String("ALL"), + }, + }, + }, + TableName: aws.String(ddbTableCategories), + } + + _, err := dynamoClient.CreateTable(input) + if err != nil { + log.Println("Error creating categories table: ", ddbTableCategories) + + if aerr, ok := err.(awserr.Error); ok { + if aerr.Code() == dynamodb.ErrCodeResourceInUseException { + log.Println("Table already exists; continuing") + err = nil + } else { + log.Println(err.Error()) + } + } else { + log.Println(err.Error()) + } + } + + return err +} + +func loadCategories(filename string) error { + + start := time.Now() + + log.Println("Loading categories from file: ", filename) + + var r Categories + + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + err = yaml.Unmarshal(bytes, &r) + if err != nil { + return err + } + for _, item := range r { + av, err := dynamodbattribute.MarshalMap(item) + + if err != nil { + return err + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(ddbTableCategories), + } + + _, err = dynamoClient.PutItem(input) + if err != nil { + fmt.Println("Got error calling PutItem:") + fmt.Println(err.Error()) + } + } + + log.Println("Categories loaded in ", time.Since(start)) + + return nil +} diff --git a/src/products/src/products-service/product.go b/src/products/src/products-service/product.go index e8f07c42c..e38f9326d 100644 --- a/src/products/src/products-service/product.go +++ b/src/products/src/products-service/product.go @@ -5,9 +5,10 @@ package main // Product Struct // using omitempty as a DynamoDB optimization to create indexes +// IMPORTANT: if you change the shape of this struct, be sure to update the retaildemostore-lambda-load-products Lambda too! type Product struct { - ID string `json:"id" yaml:"id"` - URL string `json:"url" yaml:"url"` + ID string `json:"id" yaml:"id" copier:"-"` + URL string `json:"url" yaml:"url" copier:"-"` SK string `json:"sk" yaml:"sk"` Name string `json:"name" yaml:"name"` Category string `json:"category" yaml:"category"` @@ -17,7 +18,16 @@ type Product struct { Image string `json:"image" yaml:"image"` Featured string `json:"featured,omitempty" yaml:"featured,omitempty"` GenderAffinity string `json:"gender_affinity,omitempty" yaml:"gender_affinity,omitempty"` + CurrentStock int `json:"current_stock" yaml:"current_stock"` } +// Initialized - indicates if instance has been initialized or not +func (p *Product) Initialized() bool { return p != nil && len(p.ID) > 0 } + // Products Array type Products []Product + +// Inventory Struct +type Inventory struct { + StockDelta int `json:"stock_delta" yaml:"stock_delta"` +} diff --git a/src/products/src/products-service/repository.go b/src/products/src/products-service/repository.go index ab14f935b..57e2d8260 100644 --- a/src/products/src/products-service/repository.go +++ b/src/products/src/products-service/repository.go @@ -5,182 +5,172 @@ package main import ( "fmt" - "io/ioutil" "log" "os" "strconv" - "time" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" "github.com/aws/aws-sdk-go/service/dynamodb/expression" - - yaml "gopkg.in/yaml.v2" + guuuid "github.com/google/uuid" + "github.com/jinzhu/copier" ) -var products Products -var categories Categories -var exp_true bool = true - // Root/base URL to use when building fully-qualified URLs to product detail view. -var web_root_url = os.Getenv("WEB_ROOT_URL") - -func init() { -} - -func loadData() { +var webRootURL = os.Getenv("WEB_ROOT_URL") - err := loadProducts("/bin/data/products.yaml") - if err != nil { - log.Panic("Unable to load products file.") +func setProductURL(p *Product) { + if len(webRootURL) > 0 { + p.URL = webRootURL + "/#/product/" + p.ID } +} - err = loadCategories("/bin/data/categories.yaml") - if err != nil { - log.Panic("Unable to load category file.") +func setCategoryURL(c *Category) { + if len(webRootURL) > 0 && len(c.Name) > 0 { + c.URL = webRootURL + "/#/category/" + c.Name } } -func loadProducts(filename string) error { - start := time.Now() +// RepoFindProduct Function +func RepoFindProduct(id string) Product { + var product Product - log.Println("Attempting to load products file: ", filename) + id = strings.ToLower(id) - var r Products + log.Println("RepoFindProduct: ", id, ddbTableProducts) - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return err + keycond := expression.Key("id").Equal(expression.Value(id)) + expr, err := expression.NewBuilder().WithKeyCondition(keycond).Build() + // Build the query input parameters + params := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(ddbTableProducts), } + // Make the DynamoDB Query API call + result, err := dynamoClient.Query(params) - err = yaml.Unmarshal(bytes, &r) if err != nil { - return err + log.Println("get item error " + string(err.Error())) + return product } - for _, item := range r { - - av, err := dynamodbattribute.MarshalMap(item) + if len(result.Items) > 0 { + err = dynamodbattribute.UnmarshalMap(result.Items[0], &product) if err != nil { - return err + panic(fmt.Sprintf("Failed to unmarshal Record, %v", err)) } - input := &dynamodb.PutItemInput{ - Item: av, - TableName: aws.String(ddb_table_products), - } - - _, err = dynamoclient.PutItem(input) - if err != nil { - fmt.Println("Got error calling PutItem:") - fmt.Println(err.Error()) - - } + setProductURL(&product) + log.Println("RepoFindProduct returning: ", product.Name, product.Category) } - log.Println("Products loaded in ", time.Since(start)) - return nil + return product } -func loadCategories(filename string) error { +// RepoFindCategory Function +func RepoFindCategory(id string) Category { + var category Category - start := time.Now() + id = strings.ToLower(id) - log.Println("Attempting to load categories file: ", filename) + log.Println("RepoFindCategory: ", id, ddbTableCategories) - var r Categories - - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return err + keycond := expression.Key("id").Equal(expression.Value(id)) + expr, err := expression.NewBuilder().WithKeyCondition(keycond).Build() + // Build the query input parameters + params := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(ddbTableCategories), } + // Make the DynamoDB Query API call + result, err := dynamoClient.Query(params) - err = yaml.Unmarshal(bytes, &r) if err != nil { - return err + log.Println("get item error " + string(err.Error())) + return category } - for _, item := range r { - av, err := dynamodbattribute.MarshalMap(item) - if err != nil { - return err - } + if len(result.Items) > 0 { + err = dynamodbattribute.UnmarshalMap(result.Items[0], &category) - input := &dynamodb.PutItemInput{ - Item: av, - TableName: aws.String(ddb_table_categories), - } - - _, err = dynamoclient.PutItem(input) if err != nil { - fmt.Println("Got error calling PutItem:") - fmt.Println(err.Error()) - + panic(fmt.Sprintf("Failed to unmarshal Record, %v", err)) } - } - - log.Println("Categories loaded in ", time.Since(start)) + setCategoryURL(&category) - return nil -} - -func SetProductURL(p Product) Product { - if len(web_root_url) > 0 { - p.URL = web_root_url + "/#/product/" + p.ID + log.Println("RepoFindCategory returning: ", category.Name) } - return p + return category } -func SetCategoryURL(c Category) Category { - if len(web_root_url) > 0 { - c.URL = web_root_url + "/#/category/" + c.Name - } - - return c -} +// RepoFindCategoriesByName Function +func RepoFindCategoriesByName(name string) Categories { + var categories Categories -// RepoFindProduct Function -func RepoFindProduct(id int) Product { + log.Println("RepoFindCategoriesByName: ", name, ddbTableCategories) - var product Product + keycond := expression.Key("name").Equal(expression.Value(name)) + proj := expression.NamesList(expression.Name("id"), + expression.Name("name"), + expression.Name("image")) + expr, err := expression.NewBuilder().WithKeyCondition(keycond).WithProjection(proj).Build() - log.Println("RepoFindProduct: ", strconv.Itoa(id), ddb_table_products) + if err != nil { + log.Println("Got error building expression:") + log.Println(err.Error()) + } - keycond := expression.Key("id").Equal(expression.Value(strconv.Itoa(id))) - expr, err := expression.NewBuilder().WithKeyCondition(keycond).Build() // Build the query input parameters params := &dynamodb.QueryInput{ ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.KeyCondition(), ProjectionExpression: expr.Projection(), - TableName: aws.String(ddb_table_products), + TableName: aws.String(ddbTableCategories), + IndexName: aws.String("name-index"), } // Make the DynamoDB Query API call - result, err := dynamoclient.Query(params) + result, err := dynamoClient.Query(params) if err != nil { - log.Println("get item error" + string(err.Error())) - return product + log.Println("Got error QUERY expression:") + log.Println(err.Error()) } - err = dynamodbattribute.UnmarshalMap(result.Items[0], &product) + log.Println("RepoFindCategoriesByName / items found = ", len(result.Items)) - if err != nil { - panic(fmt.Sprintf("Failed to unmarshal Record, %v", err)) - } + for _, i := range result.Items { + item := Category{} - product = SetProductURL(product) + err = dynamodbattribute.UnmarshalMap(i, &item) - log.Println("RepoFindProduct returning: ", product.Name, product.Category) + if err != nil { + log.Println("Got error unmarshalling:") + log.Println(err.Error()) + } else { + setCategoryURL(&item) + } - // return the uniq item returned. - return product + categories = append(categories, item) + } + + if len(result.Items) == 0 { + categories = make([]Category, 0) + } + + return categories } // RepoFindProductByCategory Function @@ -198,7 +188,8 @@ func RepoFindProductByCategory(category string) Products { expression.Name("style"), expression.Name("description"), expression.Name("price"), - expression.Name("gender_affinity")) + expression.Name("gender_affinity"), + expression.Name("current_stock")) expr, err := expression.NewBuilder().WithKeyCondition(keycond).WithProjection(proj).Build() if err != nil { @@ -212,11 +203,11 @@ func RepoFindProductByCategory(category string) Products { ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.KeyCondition(), ProjectionExpression: expr.Projection(), - TableName: aws.String(ddb_table_products), + TableName: aws.String(ddbTableProducts), IndexName: aws.String("category-index"), } // Make the DynamoDB Query API call - result, err := dynamoclient.Query(params) + result, err := dynamoClient.Query(params) if err != nil { log.Println("Got error QUERY expression:") @@ -234,12 +225,16 @@ func RepoFindProductByCategory(category string) Products { log.Println("Got error unmarshalling:") log.Println(err.Error()) } else { - item = SetProductURL(item) + setProductURL(&item) } f = append(f, item) } + if len(result.Items) == 0 { + f = make([]Product, 0) + } + return f } @@ -265,11 +260,11 @@ func RepoFindFeatured() Products { ExpressionAttributeValues: expr.Values(), FilterExpression: expr.Filter(), ProjectionExpression: expr.Projection(), - TableName: aws.String(ddb_table_products), + TableName: aws.String(ddbTableProducts), IndexName: aws.String("id-featured-index"), } // Make the DynamoDB Query API call - result, err := dynamoclient.Scan(params) + result, err := dynamoClient.Scan(params) if err != nil { log.Println("Got error scan expression:") @@ -287,17 +282,22 @@ func RepoFindFeatured() Products { log.Println("Got error unmarshalling:") log.Println(err.Error()) } else { - item = SetProductURL(item) + setProductURL(&item) } f = append(f, item) } + if len(result.Items) == 0 { + f = make([]Product, 0) + } + return f } -// TODO: implement some caching +// RepoFindALLCategories - loads all categories func RepoFindALLCategories() Categories { + // TODO: implement some caching log.Println("RepoFindALLCategories: ") @@ -305,10 +305,10 @@ func RepoFindALLCategories() Categories { // Build the query input parameters params := &dynamodb.ScanInput{ - TableName: aws.String(ddb_table_categories), + TableName: aws.String(ddbTableCategories), } // Make the DynamoDB Query API call - result, err := dynamoclient.Scan(params) + result, err := dynamoClient.Scan(params) if err != nil { log.Println("Got error scan expression:") @@ -326,35 +326,39 @@ func RepoFindALLCategories() Categories { log.Println("Got error unmarshalling:") log.Println(err.Error()) } else { - item = SetCategoryURL(item) + setCategoryURL(&item) } f = append(f, item) } + if len(result.Items) == 0 { + f = make([]Category, 0) + } + return f } -// RepoFindALLProduct Function -func RepoFindALLProduct() Products { +// RepoFindALLProducts Function +func RepoFindALLProducts() Products { - log.Println("RepoFindALLProduct: ") + log.Println("RepoFindALLProducts") var f Products // Build the query input parameters params := &dynamodb.ScanInput{ - TableName: aws.String(ddb_table_products), + TableName: aws.String(ddbTableProducts), } // Make the DynamoDB Query API call - result, err := dynamoclient.Scan(params) + result, err := dynamoClient.Scan(params) if err != nil { log.Println("Got error scan expression:") log.Println(err.Error()) } - log.Println("RepoFindALLProduct / items found = ", len(result.Items)) + log.Println("RepoFindALLProducts / items found = ", len(result.Items)) for _, i := range result.Items { item := Product{} @@ -365,11 +369,148 @@ func RepoFindALLProduct() Products { log.Println("Got error unmarshalling:") log.Println(err.Error()) } else { - item = SetProductURL(item) + setProductURL(&item) } f = append(f, item) } + if len(result.Items) == 0 { + f = make([]Product, 0) + } + return f } + +// RepoUpdateProduct - updates an existing product +func RepoUpdateProduct(existingProduct *Product, updatedProduct *Product) error { + updatedProduct.ID = existingProduct.ID // Ensure we're not changing product ID. + updatedProduct.URL = "" // URL is generated so ignore if specified + log.Printf("UpdateProduct from %#v to %#v", existingProduct, updatedProduct) + + copier.Copy(existingProduct, updatedProduct) + log.Printf("after Copier %#v", updatedProduct) + + av, err := dynamodbattribute.MarshalMap(updatedProduct) + + if err != nil { + fmt.Println("Got error calling dynamodbattribute MarshalMap:") + fmt.Println(err.Error()) + return err + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(ddbTableProducts), + } + + _, err = dynamoClient.PutItem(input) + if err != nil { + fmt.Println("Got error calling PutItem:") + fmt.Println(err.Error()) + } + + setProductURL(updatedProduct) + + return err +} + +// RepoUpdateInventoryDelta - updates a product's current inventory +func RepoUpdateInventoryDelta(product *Product, stockDelta int) error { + + log.Printf("RepoUpdateInventoryDelta for product %#v, delta: %v", product, stockDelta) + + if product.CurrentStock+stockDelta < 0 { + // ensuring we don't get negative stocks, just down to zero stock + // FUTURE: allow backorders via negative current stock? + stockDelta = -product.CurrentStock + } + + input := &dynamodb.UpdateItemInput{ + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":stock_delta": { + N: aws.String(strconv.Itoa(stockDelta)), + }, + ":currstock": { + N: aws.String(strconv.Itoa(product.CurrentStock)), + }, + }, + TableName: aws.String(ddbTableProducts), + Key: map[string]*dynamodb.AttributeValue{ + "id": { + S: aws.String(product.ID), + }, + "category": { + S: aws.String(product.Category), + }, + }, + ReturnValues: aws.String("UPDATED_NEW"), + UpdateExpression: aws.String("set current_stock = current_stock + :stock_delta"), + ConditionExpression: aws.String("current_stock = :currstock"), + } + + _, err = dynamoClient.UpdateItem(input) + if err != nil { + fmt.Println("Got error calling UpdateItem:") + fmt.Println(err.Error()) + } else { + product.CurrentStock = product.CurrentStock + stockDelta + } + + return err +} + +// RepoNewProduct - initializes and persists new product +func RepoNewProduct(product *Product) error { + log.Printf("RepoNewProduct --> %#v", product) + + product.ID = strings.ToLower(guuuid.New().String()) + av, err := dynamodbattribute.MarshalMap(product) + + if err != nil { + fmt.Println("Got error calling dynamodbattribute MarshalMap:") + fmt.Println(err.Error()) + return err + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(ddbTableProducts), + } + + _, err = dynamoClient.PutItem(input) + if err != nil { + fmt.Println("Got error calling PutItem:") + fmt.Println(err.Error()) + } + + setProductURL(product) + + return err +} + +// RepoDeleteProduct - deletes a single product +func RepoDeleteProduct(product *Product) error { + log.Println("Deleting product: ", product) + + input := &dynamodb.DeleteItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "id": { + S: aws.String(product.ID), + }, + "category": { + S: aws.String(product.Category), + }, + }, + TableName: aws.String(ddbTableProducts), + } + + _, err := dynamoClient.DeleteItem(input) + + if err != nil { + fmt.Println("Got error calling DeleteItem:") + fmt.Println(err.Error()) + } + + return err +} diff --git a/src/products/src/products-service/routes.go b/src/products/src/products-service/routes.go index 28373d189..1820cbb00 100644 --- a/src/products/src/products-service/routes.go +++ b/src/products/src/products-service/routes.go @@ -29,12 +29,6 @@ var routes = Routes{ "/products/all", ProductIndex, }, - Route{ - "CategoryIndex", - "GET", - "/categories/all", - CategoryIndex, - }, Route{ "ProductShow", "GET", @@ -48,9 +42,45 @@ var routes = Routes{ ProductFeatured, }, Route{ - "CategoryShow", + "ProductInCategory", "GET", "/products/category/{categoryName}", + ProductInCategory, + }, + Route{ + "ProductUpdate", + "PUT", + "/products/id/{productID}", + UpdateProduct, + }, + Route{ + "ProductDelete", + "DELETE", + "/products/id/{productID}", + DeleteProduct, + }, + Route{ + "NewProduct", + "POST", + "/products", + NewProduct, + }, + Route{ + "InventoryUpdate", + "PUT", + "/products/id/{productID}/inventory", + UpdateInventory, + }, + Route{ + "CategoryIndex", + "GET", + "/categories/all", + CategoryIndex, + }, + Route{ + "CategoryShow", + "GET", + "/categories/id/{categoryID}", CategoryShow, }, } diff --git a/src/users/src/users-service/handlers.go b/src/users/src/users-service/handlers.go index de4d4c162..9f5a6d07d 100644 --- a/src/users/src/users-service/handlers.go +++ b/src/users/src/users-service/handlers.go @@ -35,9 +35,11 @@ func UserIndex(w http.ResponseWriter, r *http.Request) { panic(err) } - if i > -1 { - offset = i + if i < 0 { + http.Error(w, "Offset must be >= 0", http.StatusUnprocessableEntity) + return } + offset = i } var countParam = keys.Get("count") @@ -47,25 +49,35 @@ func UserIndex(w http.ResponseWriter, r *http.Request) { panic(err) } - if i > 0 { - count = i + if i < 1 { + http.Error(w, "Count must be > 0", http.StatusUnprocessableEntity) + return } + + if i > 10000 { + http.Error(w, "Count exceeds maximum value; please use paging by offset", http.StatusUnprocessableEntity) + return + } + + count = i } w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) var end = offset + count if end > len(users) { end = len(users) } - var ret []User + ret := make([]User, 0, count) - if offset < len(users) { - ret = users[offset:end] - } else { - ret = make([]User, 0) + idx := offset + for len(ret) < count && idx < len(users) { + // Do NOT return any users with an associated identity ID. + if len(users[idx].IdentityId) == 0 { + ret = append(ret, users[idx]) + } + idx++ } if err := json.NewEncoder(w).Encode(ret); err != nil { diff --git a/src/users/src/users-service/repository.go b/src/users/src/users-service/repository.go index da188231e..df049f8dc 100644 --- a/src/users/src/users-service/repository.go +++ b/src/users/src/users-service/repository.go @@ -105,8 +105,13 @@ func RepoUpdateUser(t User) User { u.SignUpDate = t.SignUpDate u.LastSignInDate = t.LastSignInDate + if len(u.IdentityId) > 0 && u.IdentityId != t.IdentityId { + delete(usersByIdentityId, u.IdentityId) + } + + u.IdentityId = t.IdentityId + if len(t.IdentityId) > 0 { - u.IdentityId = t.IdentityId usersByIdentityId[t.IdentityId] = idx } @@ -124,9 +129,24 @@ func RepoCreateUser(t User) (User, error) { } idx := len(users) - t.ID = strconv.Itoa(idx) + + if len(t.ID) > 0 { + // ID provided by caller (provisionally created on storefront) so make + // sure it's not already taken. + if _, ok := usersById[t.ID]; ok { + return User{}, errors.New("User with this ID already exists") + } + } else { + t.ID = strconv.Itoa(idx) + } + users = append(users, t) usersById[t.ID] = idx usersByUsername[t.Username] = idx + + if len(t.IdentityId) > 0 { + usersByIdentityId[t.IdentityId] = idx + } + return t, nil } diff --git a/src/web-ui/.env b/src/web-ui/.env index 1b4d04b35..15d4fa1f6 100644 --- a/src/web-ui/.env +++ b/src/web-ui/.env @@ -35,4 +35,6 @@ VUE_APP_BOT_REGION=us-west-2 # Configure Pinpoint VUE_APP_PINPOINT_APP_ID= -VUE_APP_PINPOINT_REGION=us-west-2 \ No newline at end of file +VUE_APP_PINPOINT_REGION=us-west-2 + +VUE_APP_SEGMENT_WRITE_KEY= \ No newline at end of file diff --git a/src/web-ui/src/analytics/AnalyticsHandler.js b/src/web-ui/src/analytics/AnalyticsHandler.js index 15cc60b67..a3dacd824 100644 --- a/src/web-ui/src/analytics/AnalyticsHandler.js +++ b/src/web-ui/src/analytics/AnalyticsHandler.js @@ -6,6 +6,7 @@ * (event tracker), and partner integrations. */ import Vue from 'vue'; +import AmplifyStore from '@/store/store'; import { Analytics as AmplifyAnalytics } from '@aws-amplify/analytics'; import Amplitude from 'amplitude-js' import { RepositoryFactory } from '@/repositories/RepositoryFactory' @@ -207,11 +208,12 @@ export const AnalyticsHandler = { AmplifyAnalytics.record({ eventType: 'ProductAdded', - userId: user ? user.id : null, + userId: user ? user.id : AmplifyStore.state.provisionalUserID, properties: { itemId: product.id } }, 'AmazonPersonalize') + AmplifyStore.commit('incrementSessionEventsRecorded'); let eventProperties = { userId: user ? user.id : null, @@ -305,11 +307,12 @@ export const AnalyticsHandler = { AmplifyAnalytics.record({ eventType: 'ProductQuantityUpdated', - userId: user ? user.id : null, + userId: user ? user.id : AmplifyStore.state.provisionalUserID, properties: { itemId: cartItem.product_id } }, 'AmazonPersonalize') + AmplifyStore.commit('incrementSessionEventsRecorded'); let eventProperties = { cartId: cart.id, @@ -349,11 +352,12 @@ export const AnalyticsHandler = { AmplifyAnalytics.record({ eventType: 'ProductViewed', - userId: user ? user.id : null, + userId: user ? user.id : AmplifyStore.state.provisionalUserID, properties: { itemId: product.id } }, 'AmazonPersonalize'); + AmplifyStore.commit('incrementSessionEventsRecorded'); if (experimentCorrelationId) { RecommendationsRepository.recordExperimentOutcome(experimentCorrelationId) @@ -406,11 +410,12 @@ export const AnalyticsHandler = { for (var item in cart.items) { AmplifyAnalytics.record({ eventType: 'CartViewed', - userId: user ? user.id : null, + userId: user ? user.id : AmplifyStore.state.provisionalUserID, properties: { itemId: cart.items[item].product_id } }, 'AmazonPersonalize') + AmplifyStore.commit('incrementSessionEventsRecorded'); } let eventProperties = { @@ -449,11 +454,12 @@ export const AnalyticsHandler = { for (var item in cart.items) { AmplifyAnalytics.record({ eventType: 'CheckoutStarted', - userId: user ? user.id : null, + userId: user ? user.id : AmplifyStore.state.provisionalUserID, properties: { itemId: cart.items[item].product_id } }, 'AmazonPersonalize') + AmplifyStore.commit('incrementSessionEventsRecorded'); } let eventProperties = { @@ -509,11 +515,12 @@ export const AnalyticsHandler = { AmplifyAnalytics.record({ eventType: 'OrderCompleted', - userId: user ? user.id : null, + userId: user ? user.id : AmplifyStore.state.provisionalUserID, properties: { itemId: orderItem.product_id } }, 'AmazonPersonalize') + AmplifyStore.commit('incrementSessionEventsRecorded'); if (this.amplitudeEnabled()) { // Amplitude revenue diff --git a/src/web-ui/src/authenticated/Profile.vue b/src/web-ui/src/authenticated/Profile.vue index 5a23bd256..b20cd149c 100644 --- a/src/web-ui/src/authenticated/Profile.vue +++ b/src/web-ui/src/authenticated/Profile.vue @@ -8,7 +8,7 @@

{{ user.username }}

-
{{ user.persona }}
+
{{ user.persona }}

@@ -20,7 +20,16 @@
@@ -99,6 +108,7 @@ import { RepositoryFactory } from '@/repositories/RepositoryFactory' import { AnalyticsHandler } from '@/analytics/AnalyticsHandler' import { AmplifyEventBus } from 'aws-amplify-vue'; +import { Credentials } from '@aws-amplify/core'; import AmplifyStore from '@/store/store' @@ -117,6 +127,8 @@ export default { return { errors: [], user: null, + authdUser: null, + identityId: null, saving: false, users: [], newUserId: AmplifyStore.state.user.id @@ -124,6 +136,7 @@ export default { }, created () { this.getUser(AmplifyStore.state.user.id) + this.getAuthdUser() this.getUsers() }, methods: { @@ -134,11 +147,18 @@ export default { } return this.user }, + async getAuthdUser() { + const credentials = await Credentials.get(); + if (credentials && credentials.identityId) { + this.identityId = credentials.identityId; + const { data } = await UsersRepository.getUserByIdentityId(credentials.identityId); + this.authdUser = data + } + }, async getUsers() { // More users than we can display in dropdown so limit to 300. - const start = Math.max(0, parseInt(AmplifyStore.state.user.id) - 100) - const { data } = await UsersRepository.get(start, start + 300) - this.users = data + const { data } = await UsersRepository.get(0, 300); + this.users = this.users.concat(data); }, async saveChanges () { this.saving = true; diff --git a/src/web-ui/src/public/CategoryDetail.vue b/src/web-ui/src/public/CategoryDetail.vue index 78c012850..9be2568c2 100644 --- a/src/web-ui/src/public/CategoryDetail.vue +++ b/src/web-ui/src/public/CategoryDetail.vue @@ -82,8 +82,8 @@ export default { intermediate = data } - if (this.user && intermediate.length > 0) { - const response = await RecommendationsRepository.getRerankedItems(this.user.id, intermediate, ExperimentFeature) + if (this.personalizeUserID && intermediate.length > 0) { + const response = await RecommendationsRepository.getRerankedItems(this.personalizeUserID, intermediate, ExperimentFeature) if (response.headers) { if (response.headers['x-personalize-recipe']) { @@ -116,6 +116,9 @@ export default { computed: { user() { return AmplifyStore.state.user + }, + personalizeUserID() { + return AmplifyStore.getters.personalizeUserID } }, filters: { diff --git a/src/web-ui/src/public/Main.vue b/src/web-ui/src/public/Main.vue index e5d2f1072..83cf9fa75 100644 --- a/src/web-ui/src/public/Main.vue +++ b/src/web-ui/src/public/Main.vue @@ -1,8 +1,8 @@