Skip to content
Open
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
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**/node_modules/
**/dist
.git
npm-debug.log
.coverage
.coverage.*
.env.*
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"semi": true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great 🏆

}
1 change: 0 additions & 1 deletion ABOUT.md

This file was deleted.

Binary file added docs/ER_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 129 additions & 0 deletions docs/README.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can link the ERD image here somewhere as well

Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# URL Shortener Project - Database Design (ER Diagram)

## 📘 Overview

This document describes the **Entity Relationship (ER) structure** for the URL Shortener project built with **PostgreSQL**.
The database supports core functionalities such as:

- User authentication and email verification
- URL shortening and management
- Analytics tracking (hits, device info, etc.)
- Notification system for user alerts (e.g., URL expiry)

---

## 🧩 Entities and Relationships

ER diagram link: `https://dbdiagram.io/d/68e70e16d2b621e422f4d115`

### 1. 🧑‍💻 users
Stores user information and authentication details.

| Column | Type | Description |
|---------|------|--------------|
| id | SERIAL (PK) | Unique user ID |
| username | VARCHAR | User’s chosen username |
| fullName | VARCHAR | User’s full name |
| email | VARCHAR(255) | User’s email address (unique) |
| password | Encrypted password |
| verifiedAt | BOOLEAN | Email verification status |
| createdAt | TIMESTAMP | Record creation date |
| updatedAt | TIMESTAMP | Record update date |
| deletedAt | TIMESTAMP | Record delete date |

**Relations:**
- One user → many `urls`
- One user → many `notifications`
- One user → many `email_verifications`

---

### 2. ✉️ email_verifications
Stores email verification tokens for user account confirmation.

| Column | Type | Description |
|---------|------|--------------|
| id | SERIAL (PK) | Unique record ID |
| userId | INT (FK) | Linked user |
| token | VARCHAR(100) | Unique email verification token |
| createdAt | TIMESTAMP | Token creation time |
| expiresAt | TIMESTAMP | Token expiration time |
| verifiedAt | TIMESTAMP | Time when the user verified their email |

**Relations:**
- Many tokens → belong to one user (`users.id < email_verifications.userId`)

---

### 3. 🔗 urls
Stores shortened URLs created by users.

| Column | Type | Description |
|---------|------|--------------|
| id | SERIAL (PK) | Unique URL ID |
| userId | INT (FK) | Linked user |
| original_url | TEXT | Original long URL |
| shortUrl | VARCHAR(20) | Shortened URL code |
| expiresAt | TIMESTAMP | Expiration date of the short URL |
| isActive | BOOLEAN | Whether the URL is active |
| createdAt | TIMESTAMP | Record creation date |
| updatedAt | TIMESTAMP | Record update date |
| deletedAt | TIMESTAMP | Record delete date |

**Relations:**
- One user → many URLs (`users.id < urls.userId`)
- One URL → many hits (`urls.id < hits.urlId`)

---

### 4. 📊 hits
Stores analytics for every time a short URL is accessed.

| Column | Type | Description |
|---------|------|--------------|
| id | SERIAL (PK) | Unique record ID |
| urlId | INT (FK) | Associated URL |
| redirectedAt | TIMESTAMP | Access time |
| ip | VARCHAR(100) | Visitor’s IP address |
| country | VARCHAR(10) | Parsed country code |
| userAgent | TEXT | Full user-agent string |

**Relations:**
- Many hits → belong to one URL (`urls.id < hits.urlId`)

---



## 🔗 Relationship Summary

| Relationship | Description |
|---------------|--------------|
| users → urls | A user can create multiple URLs |
| users → email_verifications | A user can have multiple email verification tokens |
| urls → hits | A shortened URL can be accessed multiple times |

---

## ⚙️ Design Notes

- **Email verification** is managed via the `email_verifications` table (supports multiple tokens and expiry handling).
- **Rate limiting** and other logic (like URL redirection and expiry) are handled in application middleware.
- **Analytics** from the `hits` table allow detailed reporting (browser, OS, device, referrer, etc.).

---

## 🧩 ER Diagram Tools

- **Editor:** [dbdiagram.io](https://dbdiagram.io)
- **Database:** PostgreSQL
- **ORM Compatibility:** TypeORM / Prisma
- **Containerization:** Docker + Docker Compose

---

## 👨‍💻 Author

**Laxman Rumba**
Version: `v1.2 (with Email Verification Table)`
Database Design: October 2025
34 changes: 34 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);
8 changes: 8 additions & 0 deletions nest-cli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
87 changes: 87 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"name": "url-shortener",
"version": "0.0.1",
"description": "url-shortener",
"author": "laxman",
"private": true,
"license": "UNLICENSED",
"scripts": {
"typeorm": "ts-node --project tsconfig.json -r dotenv/config -r tsconfig-paths/register ./node_modules/typeorm/cli.js --dataSource ./src/data-source.ts",
"migration:generate": "pnpm run typeorm migration:generate",
"migration:run": "pnpm run typeorm migration:run",
"migration:revert": "pnpm run typeorm migration:revert",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.27"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"packageManager": "[email protected]+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
}
Loading