Paint Stand ๐จ is an e-commerce platform that lets users create and sell virtual paintings.
The following project is my Shopify Fall 2021 Backend Developer Challenge Submission.
For my solution to the Developer Intern Challenge, I have created Paint Stand, a theoretical platform where users can create and sell illustrations. User's can purchase, collect, and re-sell the art of other users for higher prices to climb a global wealth leaderboard.
The following is a list of features the platform supports:
To set up and configure this application, you can either install the various dependencies as described below or use the provided dockerfile and docker-compose.yml
. If you plan to use the Docker, skip to Docker Setup.
This projects API was built using Ruby on Rails and will require you to have Ruby 2.6.3 and Ruby on Rails 6.0.3.2. Additionally you will need PostgreSQL and Redis installed.
Once you have installed the aforementioned technologies, clone this repo and modify config/database.yml
to match your local PostgreSQL credentials.
development:
<<: *default
database: image_repository_development
username: YOUR_USERNAME
password: YOUR_PASSWORD
test:
<<: *default
database: image_repository_test
username: YOUR_USERNAME
password: YOUR_PASSWORD
You will may also need to modify config/environments/development.rb
and config/environments/test.rb
to configure redis.
config.cache_store = :redis_store_with_cas, {
host: 'redis', # Should be 'localhost' if not using docker-compose setup
port: 6379,
db: 0,
namespace: 'cache',
expires_in: 15.minutes,
race_condition_ttl: 1
}
After you've properly configured PostgreSQL and Redis run the following commands:
bundle
to install all ruby gems related to the projectrake db:migrate
and rake db:seed
to migrate the database and seed it with datarails s
or rails server
localhost:3000
and you should seeThe project's frontend is built with Svelte and uses the Yarn package manager. To setup the client (which is entirely optional because this is a backend development challenge) install Yarn and run the following commands in the client directory.
yarn install
yarn run dev
If you would like to run this app using docker, you will need to verify that the database host name, username, and password in config/database.yml
match the information found in docker-compose.yml
'db' container and that the redis_store
information in config/environments/development.rb
matches the information in the 'redis' container.
The host name in config/database.yml
should match the name of the postgres container ('db') and the host name of the redis_store should match the name of the redis container ('redis').
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
#host: db #uncomment this if using docker-compose
development:
<<: *default
database: image_repository_development
username: YOUR_USERNAME
password: YOUR_PASSWORD
db:
image: postgres:12-alpine
environment:
POSTGRES_USER: YOUR_USERNAME
POSTGRES_PASSWORD: YOUR_PASSWORD
networks:
- imagerepo
Once you've verified all the connections with postgres and redis should be properly configured, run:
docker-compose up
For using this API, I strongly recommend Altair GraphQL Client. Altair is capable of seamlessly using files as GraphQL parameters and can dynamically generate the body of your GraphQL requests.
The GraphQL API can be utilized using POST http://localhost:3000/graphql
.
Many of the GraphQL operations will require you to be logged in and submit a valid JWT Token via the Authentication
header. You can retrieve your JWT Token using the login
mutation.
You can log in using the credentials:
Add the Authentication
Header:
If you do not have a valid JWT token in the Authentication header on specific operations, you will receive this error:
NOTE: To store images, I utilized Rails Active Storage. Active Storage uses polymorphic associations. These are represented above using dotted lines.
User: Represents someone who Interacts with the API
Image: Represents an uploaded and purchasable image
ImageTag: Join between mage and Tag for associating specific images with a tags that represent their characteristics
Tag: Represents a characteristic of another entity.
Purchase: Represents an Image's purchase or sale
The Paint Stand API is built using GraphQL.
In designing this GraphQL API, I choose to follow the guidelines created by the Shopify API Patterns Team (Described in this 2018 GraphQL Conference talk by Leane Shapton).
One of the Shopify API Patterns team's GraphQL guidelines is to Not expose implementation detail in your API design. To follow this guideline, I abstracted out the ImageTags join table from Paint Stands's domain model.
Additionally, to improve the performance of my API and remove multiple unnecessary round trips to datastores from nested GraphQL queries (the N+1 query problem), I have defined batch loaders using Shopify's GraphQL-Batch gem.
image:
A query that returns the information of a specified image.
imageSearch:
A query that returns the results of a text-based search for related images.
imagesListed:
A query that returns all publicly purchasable images.
profile:
A query that returns the profile of the currently logged in user.
purchase:
A query that returns the information of a specified purchase.
user:
A query that returns the profile of the specified user.
users:
A query that returns many users.
addImageTag:
A mutation that adds a Tag to a given Image using the provided tag name.
createImage: (upload image)
A mutation that creates an Image Entity using the provided information.
createPurchase: (purchase image)
A mutation that creates a Purchase entity for the image given with the provided id for the currently logged in user
deleteImage:
A mutation that deletes the Image with the provided id.
login:
A mutation that logs in the user of the provided email and returns a JWT Token.
signUp:
A mutation that creates a new user using the provided information.
updateImage
A mutation that updates the image of the provided id
updateUser
A mutation that updates the current user using the provided information
Queries that return multiple records have been given pagination using the GraphQL Pagination gem.
query imageListed($page: Int!, $limit: Int!){
imageListed(page: $page, limit: $limit){
collection{
attachedImageUrl
description
id
price
title
}
metadata{
currentPage
limitValue
totalCount
totalPages
}
}
}
For caching, I utilized Redis using the "cache-aside" caching strategy. I also utilized Shopify's IdentityCache gem to cache models and their relationships.
All currency in this application is stored as an integer. This is because it is much safer to use integers than deal with the added complexity of floating point numbers.
Additionally all transactions in this application are surrounded in Active Record Transaction blocks to ensure if an error occurs during the transaction, any edited records are rolled back to before the transaction began.
ActiveRecord::Base.transaction do
::Purchase.create!(
cost: image.price,
customer_id: user.id,
merchant_id: image.owner.id,
image_id: image.id
)
...
end
All available GraphQL operations have error handling to prevent any internal errors from being exposed to users.
To verify a user's identity, the API uses JWT tokens. A user can receive their JWT Token by using the login
mutation.
Additionally, all user passwords are hashed using Bcrypt.
For this project, I utilized RuboCop to enforce many guidelines outlined in the community Ruby Style Guide. Additionally, I utilized the Shopify RuboCop config to implement the Shopify Ruby Style guidelines described here.
require:
- rubocop-rails
- rubocop-faker
- rubocop-rspec
inherit_gem:
rubocop-shopify: rubocop.yml
In addition to this readme, I have documented this project through inline comments and GraphQL client documentation.
Several rspec tests have been written for this project, including unit and integration tests for all models, queries, and mutaions. According to the code coverage report generated by the simplecov
gem, this project has 99.61% code coverage.
Simply run:
rspec
to run all provided tests and generate a simplecov code coverage report.
Using Github Actions, this repository has been configured to run all rspec tests and rubocop linting on all commits and pull requests. Futher information about the Github Actions continuous integration configuration can be viewed here.
MIT. See LICENSE for more details.