Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #16

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open

Dev #16

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
87 changes: 69 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,82 @@
# GlobalWebIndex Engineering Challenge
# NOTES

## Introduction
## Design

This challenge is designed to give you the opportunity to demonstrate your abilities as a software engineer and specifically your knowledge of the Go language.
For the requirement a backend application with will implemented using Go. This backend will expose a RESTful API.

On the surface the challenge is trivial to solve, however you should choose to add features or capabilities which you feel demonstrate your skills and knowledge the best. For example, you could choose to optimise for performance and concurrency, you could choose to add a robust security layer or ensure your application is highly available. Or all of these.
## Public API:

Of course, usually we would choose to solve any given requirement with the simplest possible solution, however that is not the spirit of this challenge.
- `POST /users` -> user sign-up
- `POST /tokens` -> user's login

## Challenge
## Protected API:

Let's say that in GWI platform all of our users have access to a huge list of assets. We want our users to have a peronal list of favourites, meaning assets that favourite or “star” so that they have them in their frontpage dashboard for quick access. An asset can be one the following
* Chart (that has a small title, axes titles and data)
* Insight (a small piece of text that provides some insight into a topic, e.g. "40% of millenials spend more than 3hours on social media daily")
* Audience (which is a series of characteristics, for that exercise lets focus on gender (Male, Female), birth country, age groups, hours spent daily on social media, number of purchases last month)
e.g. Males from 24-35 that spent more than 3 hours on social media daily.
- `GET /assets` -> get all assets
- `GET /assets/:assetId` -> get asset details
- `PATCH /assets/:assetId` -> patch asset's description
- `GET /assets/audiences` -> get all audiences
- `GET /assets/charts` -> get all charts
- `GET /assets/insights` -> get all insights
- `GET /users/:userId/favorites` -> get all user's favorites
- `POST /users/:userId/favorites` -> set user's favorite (add to favorites)
- `DELETE /users/:userId/:favoriteId` -> delete user's favorite

Build a web server which has some endpoint to receive a user id and return a list of all the user’s favourites. Also we want endpoints that would add an asset to favourites, remove it, or edit its description. Assets obviously can share some common attributes (like their description) but they also have completely different structure and data. It’s up to you to decide the structure and we are not looking for something overly complex here (especially for the cases of audiences). There is no need to have/deploy/create an actual database although we would like to discuss about storage options and data representations.
## Models:

Note that users have no limit on how many assets they want on their favourites so your service will need to provide a reasonable response time.
- User (passwords will be hashed using `bcrypt`)
- Asset (`polymorphic` has-one relationship with Audience, Chart, Insight)
- Audience (pseudo `enum`s should enforce properties like `gender` and `ageGroup`)
- Chart
- Insight
- Favorite

A working server application with functional API is required, along with a clear readme.md. Useful and passing tests would be also be viewed favourably
Asset's foreign key will be the combination of `RelatedID` with `RelatedType` -> `RelatedType` could either be `charts` or `insights` or `audiences`

It is appreciated, though not required, if a Dockerfile is included.
## Various Decisions

## Submission
- To support efficiently a large amount of data, pagination should be used for all the relevant entities. The pattern should rely on query parameters of `page` and `limit`

Just create a fork from the current repo and send it to us!
- Caching layer should be introduced between server and database improve read operations and lift some height from the DB. The most potentially demanding query is to get user's favorites. Favorites could be stored in cache (Redis) using a key like `user:<userId>:favorites`.
Additionally if we want to enhance this to support pagination then the cache key could become `user:<userId>:favorites:page:<pageNumber>:limit:<limitSize>`. However this is a bit problematic as if it is important to allow users to change frequently the page size the above key becomes useless. In this case we need to use `ZRANGE` (is we are storing sorted sets) or `LRANGE` (if we are storing lists) and the overall complexity of using the cache increases.
Invalidation of cache should happen when a user adds/removes favorites

Good luck, potential colleague!
* Seeding script should populate records in database

### Backend (Golang with Gin and Gorm)

#### Folder Structure:

- cmd/api -> `main.go`, server start
- internals/controllers -> application's controllers
- internals/database -> connection to DB
- internals/middlewares -> mainly auth logic for protected endpoints
- internals/models -> application's models
- internals/server -> init Gin server and application's routes declaration
- internals/services -> application's logic used from controllers
- internals/serializers -> map external entities to internal structures
- internals/utils -> small functions for handling hashing, tokens
- internals/helpers -> small reusable functions that interact with data layer

### Backend (React app using Vite and Antd for the UI)

Simple frontend which provides a login page and a dashboard

### Docker

Two containers, one for the frontend and one for the backend will be provided. Furthermore, compose files should make the boot of the application a breeze :)

### Quick Start

From the root of the cloned repo, one should execute
`docker compose up` or `docker-compose up` depending on the version of `docker` installed on user's OS

## TODOs

- [x] implement dummy frontend
- [x] implement backend models
- [x] implement backend API
- [x] implement proper frontend
- [x] write OpenAPI spec
- [ ] introduce a caching layer for caching users' favorites. This will improve response times of the main query which is responsible to fetch all the favorites of a logged-in user based on `userId`.
- [ ] write unit tests for the backend to thoroughly test the behavior of controllers and services
- [ ] implement validations of inputs in both frontend and backend
51 changes: 51 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
services:
client:
restart: unless-stopped
build:
context: ./packages/client
dockerfile: Dockerfile
ports:
- ${CLIENT_PORT:-4000}:8080
volumes:
- ./packages/client/src:/home/node/app/src
- ./packages/client/public:/home/node/app/public

server:
restart: unless-stopped
build:
context: ./packages/server
dockerfile: Dockerfile
depends_on:
db:
condition: service_healthy
environment:
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=${DB_NAME:-gwi_dev}
- DB_USERNAME=${DB_USERNAME:-gwi_dev_user}
- DB_PASSWORD=${DB_PASSWORD:-dev_user_password}
- SERVER_SECRET=${SERVER_SECRET:-superSecretValue}
- CLIENT_URL=${CLIENT_URL:-http://localhost:4000}
command: "make run"
ports:
- ${SERVER_PORT:-3000}:3000
volumes:
- ./packages/server/cmd:/home/go/server/cmd
- ./packages/server/internal:/home/go/server/internal
- ./packages/server/seeds:/home/go/server/seeds

db:
image: postgres:16.3-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -d gwi_dev -U gwi_dev_user"]
interval: 5s
timeout: 5s
retries: 5
ports:
- ${DB_PORT:-5433}:5432
environment:
- POSTGRES_DB=${DB_NAME:-gwi_dev}
- POSTGRES_USER=${DB_USERNAME:-gwi_dev_user}
- POSTGRES_PASSWORD=${DB_PASSWORD:-dev_user_password}
volumes:
- ./scripts/init_db.sql:/docker-entrypoint-initdb.d/init_db.sql
1 change: 1 addition & 0 deletions packages/client/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules
22 changes: 22 additions & 0 deletions packages/client/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'eslint-config-prettier',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
24 changes: 24 additions & 0 deletions packages/client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
5 changes: 5 additions & 0 deletions packages/client/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
}
13 changes: 13 additions & 0 deletions packages/client/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:20.14-alpine3.20

Check failure on line 1 in packages/client/Dockerfile

View check run for this annotation

Wiz GWI / Wiz IaC Scanner

Missing User Instruction

Rule ID: 0b5e0683-5a06-4bcd-ac73-28249add06df Severity: High Resource: FROM={{node:20.14-alpine3.20}} A user should be specified in the dockerfile, otherwise the image will run as root
Raw output
Expected: The 'Dockerfile' should contain the 'USER' instruction
Found: The 'Dockerfile' does not contain any 'USER' instruction

Check notice on line 1 in packages/client/Dockerfile

View check run for this annotation

Wiz GWI / Wiz IaC Scanner

Healthcheck Instruction Missing

Rule ID: 704ee966-67b2-4219-871f-12a7e5126cb1 Severity: Low Resource: FROM={{node:20.14-alpine3.20}} Ensure that HEALTHCHECK is being used. The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working
Raw output
Expected: Dockerfile should contain instruction 'HEALTHCHECK'
Found: Dockerfile doesn't contain instruction 'HEALTHCHECK'

WORKDIR /home/node/app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 8080

CMD [ "npm", "run", "dev" ]
8 changes: 8 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
13 changes: 13 additions & 0 deletions packages/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GWI Challenge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "vite-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"antd": "^5.18.3",
"axios": "^1.7.2",
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-query": "^3.39.3",
"react-router-dom": "^6.23.1",
"styled-components": "^6.1.11",
"styled-normalize": "^8.1.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"vite": "^5.3.1"
}
}
1 change: 1 addition & 0 deletions packages/client/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading