Skip to content

Commit 3fd86ed

Browse files
committed
init
0 parents  commit 3fd86ed

21 files changed

+5426
-0
lines changed

.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Database
2+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/hiring_task_dev
3+
4+
# RabbitMQ
5+
RABBITMQ_URL=amqp://guest:guest@localhost:5672
6+
7+
# External Service
8+
EXTERNAL_SERVICE_API_KEY=your_api_key_here
9+
10+
# JWT
11+
JWT_SECRET=your_jwt_secret_here

.eslintrc.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
module.exports = {
2+
parser: "@typescript-eslint/parser",
3+
extends: [
4+
"eslint:recommended",
5+
"plugin:import/recommended",
6+
"plugin:import/typescript",
7+
"plugin:@typescript-eslint/eslint-recommended",
8+
"plugin:@typescript-eslint/recommended",
9+
"plugin:prettier/recommended",
10+
],
11+
plugins: ["import", "@typescript-eslint"],
12+
parserOptions: {
13+
sourceType: "module",
14+
ecmaVersion: 2018,
15+
},
16+
env: {
17+
es6: true,
18+
node: true,
19+
},
20+
rules: {
21+
"import/no-named-as-default-member": "off",
22+
"import/no-named-as-default": "off",
23+
"@typescript-eslint/no-explicit-any": ["off"],
24+
"@typescript-eslint/ban-ts-comment": "off",
25+
"@typescript-eslint/no-non-null-assertion": "off",
26+
"no-duplicate-imports": "error",
27+
"import/no-useless-path-segments": [
28+
"error",
29+
{
30+
noUselessIndex: true,
31+
},
32+
],
33+
"import/order": "error",
34+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
35+
"arrow-body-style": ["error", "as-needed"],
36+
"import/no-unresolved": "off",
37+
},
38+
settings: {
39+
"import/parsers": {
40+
"@typescript-eslint/parser": [".ts", ".tsx"],
41+
},
42+
"import/resolver": {
43+
node: {
44+
extensions: [".ts", ".tsx", ".js", ".jsx", ".vue"],
45+
},
46+
},
47+
},
48+
}

.prettierrc.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
semi: false
2+
singleQuote: false
3+
tabWidth: 2
4+
quoteProps: "consistent"
5+
printWidth: 120

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Code Review Challenge
2+
3+
## Overview
4+
5+
This is a code review challenge designed to evaluate your understanding of complex distributed systems, database internals, and system design. The project represents a simplified e-commerce system with multiple services handling orders, payments, and analytics.
6+
7+
The system is designed to work with both MySQL and PostgreSQL databases, which introduces interesting challenges due to their different behaviors and characteristics.
8+
9+
## Challenge Description
10+
11+
Your task is to review this codebase and identify potential issues, problems, and areas for improvement. The codebase intentionally contains various issues that a senior engineer might encounter in real-world distributed systems.
12+
13+
## Evaluation Criteria
14+
15+
Strong candidates will:
16+
17+
- Identify significant architectural and design issues
18+
- Understand database internals and their implications
19+
- Recognize distributed systems challenges
20+
- Propose practical, well-reasoned solutions
21+
- Consider trade-offs in their recommendations
22+
- Propose systematic approaches to preventing similar issues

db/schema.sql

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
2+
3+
-- Function to update timestamp
4+
CREATE OR REPLACE FUNCTION update_updated_at_column()
5+
RETURNS TRIGGER AS $$
6+
BEGIN
7+
NEW.updated_at = CURRENT_TIMESTAMP;
8+
RETURN NEW;
9+
END;
10+
$$ language 'plpgsql';
11+
12+
CREATE TABLE users (
13+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
14+
name VARCHAR(255) NOT NULL,
15+
email VARCHAR(255) NOT NULL,
16+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
17+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
18+
CONSTRAINT users_email_unique UNIQUE (email)
19+
);
20+
21+
CREATE INDEX users_email_idx ON users(email);
22+
23+
CREATE TABLE products (
24+
id SERIAL PRIMARY KEY,
25+
name VARCHAR(255) NOT NULL,
26+
price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
27+
stock INTEGER NOT NULL CHECK (stock >= 0),
28+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
29+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
30+
);
31+
32+
CREATE TABLE orders (
33+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
34+
user_id UUID NOT NULL,
35+
total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount >= 0),
36+
status INTEGER NOT NULL,
37+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
38+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
39+
FOREIGN KEY (user_id) REFERENCES users(id)
40+
);
41+
42+
CREATE INDEX orders_user_id_idx ON orders(user_id);
43+
CREATE INDEX orders_status_idx ON orders(status);
44+
45+
CREATE TABLE order_items (
46+
id SERIAL PRIMARY KEY,
47+
order_id UUID NOT NULL,
48+
product_id INTEGER NOT NULL,
49+
quantity INTEGER NOT NULL CHECK (quantity > 0),
50+
price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
51+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
52+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
53+
FOREIGN KEY (order_id) REFERENCES orders(id),
54+
FOREIGN KEY (product_id) REFERENCES products(id)
55+
);
56+
57+
CREATE INDEX order_items_order_id_idx ON order_items(order_id);
58+
CREATE INDEX order_items_product_id_idx ON order_items(product_id);
59+
60+
CREATE TABLE payments (
61+
id SERIAL PRIMARY KEY,
62+
order_id UUID NOT NULL,
63+
amount DECIMAL(10,2) NOT NULL CHECK (amount >= 0),
64+
payment_method VARCHAR(255) NOT NULL,
65+
transaction_id VARCHAR(255),
66+
status VARCHAR(20) NOT NULL CHECK (status IN ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED')),
67+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
68+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
69+
FOREIGN KEY (order_id) REFERENCES orders(id)
70+
);
71+
72+
CREATE INDEX payments_status_idx ON payments(status);
73+
74+
CREATE TABLE product_stats (
75+
id BIGSERIAL PRIMARY KEY,
76+
product_id INTEGER NOT NULL,
77+
orders_count INTEGER NOT NULL DEFAULT 0 CHECK (orders_count >= 0),
78+
FOREIGN KEY (product_id) REFERENCES products(id)
79+
);
80+
81+
CREATE UNIQUE INDEX product_stats_product_id_idx ON product_stats(product_id);
82+
83+
CREATE TABLE audit_logs (
84+
id BIGSERIAL PRIMARY KEY,
85+
exchange VARCHAR(255) NOT NULL,
86+
timestamp INTEGER,
87+
data JSONB NOT NULL,
88+
CONSTRAINT audit_logs_data_not_empty CHECK (data != 'null'::jsonb)
89+
);
90+
91+
CREATE INDEX audit_logs_exchange_idx ON audit_logs(exchange);
92+
CREATE INDEX audit_logs_timestamp_idx ON audit_logs(timestamp);

package.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "hiring-engineering-code-review-task",
3+
"version": "1.0.0",
4+
"description": "Code review task in TypeScript",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"start": "ts-node src/index.ts",
8+
"build": "tsc",
9+
"test": "jest",
10+
"lint": "eslint . --ext .ts",
11+
"lint:fix": "eslint . --ext .ts",
12+
"format": "prettier --write \"**/*.{ts,tsx,md}\""
13+
},
14+
"dependencies": {
15+
"amqplib": "^0.10.3",
16+
"axios": "^1.6.7",
17+
"dotenv": "^16.4.1",
18+
"express": "^4.18.2",
19+
"pg": "^8.11.3",
20+
"tsc": "^2.0.4",
21+
"uuid": "^9.0.1"
22+
},
23+
"devDependencies": {
24+
"@types/amqplib": "^0.10.4",
25+
"@types/express": "^4.17.21",
26+
"@types/jest": "^29.5.11",
27+
"@types/node": "^20.11.10",
28+
"@types/pg": "^8.10.9",
29+
"@types/uuid": "^9.0.7",
30+
"@typescript-eslint/eslint-plugin": "^6.19.1",
31+
"@typescript-eslint/parser": "^6.19.1",
32+
"eslint": "^8.57.1",
33+
"eslint-config-prettier": "^10.0.1",
34+
"eslint-plugin-import": "^2.31.0",
35+
"eslint-plugin-prettier": "^5.2.3",
36+
"jest": "^29.7.0",
37+
"prettier": "^3.4.2",
38+
"ts-jest": "^29.1.2",
39+
"ts-node": "^10.9.2",
40+
"typescript": "~5.3.0"
41+
}
42+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import RabbitMQConnection from "../mq/connection"
2+
import { AuditService } from "../services/auditService"
3+
import { query } from "../db"
4+
import { Order, OrderItem } from "../types"
5+
6+
export class OrderCreatedConsumer {
7+
private static readonly QUEUE_NAME = "order_created_queue"
8+
private static readonly EXCHANGE_NAME = "orders"
9+
10+
public static async subscribe(): Promise<void> {
11+
const connection = RabbitMQConnection.getInstance()
12+
const channel = await connection.getChannel()
13+
14+
await channel.assertExchange(this.EXCHANGE_NAME, "fanout", {
15+
durable: true,
16+
})
17+
18+
const queue = await channel.assertQueue(this.QUEUE_NAME, {
19+
durable: true,
20+
})
21+
22+
await channel.bindQueue(queue.queue, this.EXCHANGE_NAME, "")
23+
24+
channel.consume(queue.queue, async (msg) => {
25+
if (!msg) return
26+
27+
const orderData = JSON.parse(msg.content.toString())
28+
await this.processOrder(orderData)
29+
channel.ack(msg)
30+
})
31+
}
32+
33+
private static async processOrder(orderData: { order: Order; items: OrderItem[] }): Promise<void> {
34+
const { order, items } = orderData
35+
36+
await Promise.all(
37+
items.map((item) =>
38+
query(
39+
`UPDATE product_stats
40+
SET orders_count = orders_count + 1
41+
WHERE product_id = $1`,
42+
[item.product_id],
43+
),
44+
),
45+
)
46+
47+
await AuditService.logEvent("order_processed", {
48+
order_id: order.id,
49+
timestamp: new Date(),
50+
})
51+
}
52+
}

src/db/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Pool } from "pg"
2+
import dotenv from "dotenv"
3+
4+
dotenv.config()
5+
6+
const pool = new Pool({
7+
connectionString: process.env.DATABASE_URL,
8+
})
9+
10+
export const query = async (text: string, params?: any[]) => {
11+
const client = await pool.connect()
12+
try {
13+
return await client.query(text, params)
14+
} finally {
15+
client.release()
16+
}
17+
}
18+
19+
export const getClient = async () => {
20+
const client = await pool.connect()
21+
return client
22+
}
23+
24+
export default pool

src/handlers/eventsHandler.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Request, Response } from "express"
2+
import { AuditService } from "../services/auditService"
3+
4+
export class EventsHandler {
5+
public static async index(req: Request, res: Response): Promise<void> {
6+
try {
7+
const { exchange, limit = 25, offset = 0 } = req.query
8+
9+
if (typeof exchange !== "string") {
10+
res.status(404).json({ error: "Exchange parameter is required" })
11+
return
12+
}
13+
14+
const events = await AuditService.getEvents(exchange, Number(limit), Number(offset))
15+
16+
res.json(events)
17+
} catch (error) {
18+
res.status(500).json({ error: "Failed to fetch events" })
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)