paintstand

Paintstand

Paint Stand ๐ŸŽจ is an e-commerce platform that lets users create and sell virtual paintings.

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.



๐Ÿ“‹ Table of Contents

๐Ÿ’ป Features

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:

  • Users can signup or login using their desired email and password
  • Users can upload, update, sell, and delete images
  • Users can set and update an image's price
  • Users cannot update or delete images they did not upload
  • Users can search for images to purchase
  • Users can purchase images
  • Users can set private viewing permissions on their images
  • Users can add tags to their images to improve their search visibility

๐Ÿ› ๏ธ Setup

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:

  • run bundle to install all ruby gems related to the project
  • run rake db:migrate and rake db:seed to migrate the database and seed it with data
  • run rails s or rails server
  • View localhost:3000 and you should see

Client Setup (optional)

The 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

๐Ÿณ Docker Setup

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

API Setup

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:

Schema

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

    • Relations
      • Has Many: Created_Images (Images the user drew)
      • Has Many: Owned_Images (Images the user drew or purchased)
      • Has Many: Purchases
      • Has Many: Sales (Purchase records where the user was the merchant)
    • Attributes
      • balance (integer): Represents the user's currency balance
      • email (string): Represents the user's email
      • role (string): Represents the user's role (admin etc.)
      • username (string): Represents the user's username
      • password_digest (string): Represents the hashed version of the user's password
  • Image: Represents an uploaded and purchasable image

    • Relations
      • Belongs To: Creator (the user who drew this image)
      • Belongs To: Owner (the user who currently owns this image)
      • Has Many: Tags (Through ImageTags Join)
      • Belongs To: Purchases
      • Has One: Active Storage Attachment (Image File)
    • Attributes
      • description (text): Represents the image's description.
      • price (integer): Represents how much it costs for a user to purchase
      • state (string): Represents whether the image is viewable by all users or only its creator
      • title (string): Represents the image's title
  • ImageTag: Join between mage and Tag for associating specific images with a tags that represent their characteristics

    • Relations
      • Belongs To: Image
      • Belongs To: Tag
  • Tag: Represents a characteristic of another entity.

    • Relations
      • Has Many: ImageTags
      • Has Many: Images (Through ImageTags Join)
    • Attributes
      • name (string): Represents the characteristic of a given tag
  • Purchase: Represents an Image's purchase or sale

    • Relations
      • Belongs To: Customer (The User who bought this Image)
      • Belongs To: Merchant (The User who sold this Image)
      • Belongs To: Image
    • Attributes
      • cost (integer): Represents the cost of the transaction

โ˜๏ธ API

The Paint Stand API is built using GraphQL.

API Design

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.

Queries

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.

Mutations

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

Pagination

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
        }
    }
}

Cache

For caching, I utilized Redis using the "cache-aside" caching strategy. I also utilized Shopify's IdentityCache gem to cache models and their relationships.

๐Ÿ’ฐ Payments

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

๐Ÿ” Security

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.

๐Ÿงน Linting

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

โœ๏ธ Documentation

In addition to this readme, I have documented this project through inline comments and GraphQL client documentation.

๐Ÿงช Testing

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.

Continuous Integration

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.

License

MIT. See LICENSE for more details.

Top categories

svelte logo

Want a Svelte site built?

Hire a Svelte developer
Loading Svelte Themes