Skip to content

Commit

Permalink
feat: first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Filipe Forattini committed Oct 6, 2024
0 parents commit 03ed3c5
Show file tree
Hide file tree
Showing 13 changed files with 3,533 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
49 changes: 49 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# config

x-service: &service
image: node:21-alpine
restart: always
user: root
working_dir: /home/app
entrypoint: yarn
command: dev
dns:
- 8.8.8.8
expose:
- 8000
environment:
- PORT=8000
- APP_ENV=local
- NODE_ENV=local
- TZ=America/Sao_Paulo


services:

# apps

api:
<<: *service
volumes:
- .:/home/app
ports:
- "8000:8000"

# dependencies

minio:
image: bitnami/minio:latest
volumes:
- minio_data:/bitnami/minio/data
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_FORCE_NEW_KEYS: yes
MINIO_SCHEME: http
MINIO_DEFAULT_BUCKETS: fshortner
MINIO_ROOT_USER: fshortner
MINIO_ROOT_PASSWORD: thisissecret

volumes:
minio_data: {}
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { config } from "dotenv";
config()

import { App } from "./src/app.js";
App.create().then(() => App.start())
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "api",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"license": "MIT",
"scripts": {
"start": "node index.js",
"dev": "nodemon --ignore tmp/ index.js"
},
"dependencies": {
"@whiskeysockets/baileys": "^6.7.8",
"bullmq": "^5.15.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.0",
"express-rate-limit": "^7.4.1",
"express-status-monitor": "^1.3.4",
"express-validator": "^7.2.0",
"helmet": "^8.0.0",
"hpp": "^0.2.3",
"http-errors": "^2.0.0",
"ioredis": "^5.4.1",
"lowdb": "^7.0.1",
"morgan": "^1.10.0",
"node-cron": "^3.0.3",
"qrcode": "^1.5.4",
"s3db.js": "^3.0.2"
},
"devDependencies": {
"nodemon": "^3.1.7"
}
}
20 changes: 20 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { addRoutes } from "./routes.js"
import { startCrons } from "./crons.js"
import { createDB, createServer } from "./resources/index.js"

export const App = {
resources: {},
env: process.env,

async create() {
this.resources.db = await createDB(this)
this.resources.server = await createServer(this)
},

async start() {
addRoutes(this)
startCrons(this)

this.resources.server.listen(this.env.PORT, () => console.info(`server is running on port ${this.env.PORT}`))
}
}
15 changes: 15 additions & 0 deletions src/concerns/minio-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
]
}
]
}
26 changes: 26 additions & 0 deletions src/crons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import cron from 'node-cron'

export function startCrons(App) {
const { db } = App.resources

cron.schedule('*/10 * * * * *', async () => {
const items = await db.resource('report-items').page(1)

if (!items.length) return
else console.log(' clicks :: calculating...')

const report = items.reduce((acc, item) => {
if (!acc[item.urlId]) acc[item.urlId] = 0
acc[item.urlId]++
return acc
}, {})

for (const [urlId, clicks] of Object.entries(report)) {
const url = await db.resource('urls').get(urlId)
await db.resource('urls').update(urlId, { clicks: url.clicks + clicks })
}

await db.resource('report-items').deleteMany(items.map(item => item.id))
console.log(' clicks :: added:', items.length);
});
}
43 changes: 43 additions & 0 deletions src/resources/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { S3db } from 's3db.js'

export async function createDB (App) {
const db = new S3db({
connectionString: App.env.BUCKET_CONNECTION_STRING,
})

await db.connect()

await db.createResource({
name: 'urls',
attributes: {
link: 'string',
ip: 'string',
clicks: 'number|optional|min:0',
},
})

const clicksAttributes = {
urlId: 'string',
ip: 'string',
utm: {
$$type: 'object',
source: 'string|optional',
medium: 'string|optional',
campaign: 'string|optional',
content: 'string|optional',
term: 'string|optional',
}
}

await db.createResource({
name: 'clicks',
attributes: clicksAttributes,
})

await db.createResource({
name: 'report-items',
attributes: clicksAttributes,
})

return db
}
2 changes: 2 additions & 0 deletions src/resources/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './db.js'
export * from './server.js'
27 changes: 27 additions & 0 deletions src/resources/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import hpp from 'hpp'
import cors from 'cors'
import helmet from 'helmet'
import morgan from 'morgan'
import Express from 'express';
import compression from 'compression'
import rateLimit from 'express-rate-limit'
import statusMonitor from 'express-status-monitor'

export async function createServer (App) {
const server = Express();

server.use(statusMonitor());
server.use(Express.json());
server.use(Express.urlencoded({ extended: true }));
server.use(cors());
server.use(hpp());
server.use(helmet());
server.use(compression());
server.use(morgan('dev'));
server.use(rateLimit({
max: 100,
windowMs: 15 * 60 * 1000,
}));

return server
}
98 changes: 98 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import createError from 'http-errors';
import { body, validationResult } from 'express-validator'

export function addRoutes(App) {
const { server, db } = App.resources

// create
server.post('/v1/urls', [
body('link').isURL(),
], async (req, res, next) => {
const errors = validationResult(req);

if (!errors.isEmpty()) {
const err = createError(400, 'invalid data')
err.details = errors.array()
return next(err);
}

const url = await db.resource('urls').insert({
...req.body,
ip: req.ip,
createdAt: Date.now(),
})

delete url.ip

const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
url.shareble = new URL(fullUrl)
url.shareble.pathname = `/${url.id}`
url.shareble = url.shareble.toString()

return res.json(url);
})

// show
server.get('/v1/urls/:id', async (req, res, next) => {
try {
const url = await db.resource('urls').get(req.params.id)
delete url.ip

return res.json(url)
} catch (error) {
const err = createError(404, 'url not found')
err.details = error
return next(err);
}
})

// redirect
server.get('/:id', async (req, res, next) => {
try {
const url = await db.resource('urls').get(req.params.id)

const click = {
urlId: url.id,
ip: req.ip,
utm: {
source: req.query.utm_source,
medium: req.query.utm_medium,
campaign: req.query.utm_campaign,
content: req.query.utm_content,
term: req.query.utm_term,
},
}

await Promise.all([
db.resource('clicks').insert(click),
db.resource('report-items').insert(click),
])

return res.redirect(302, url.link)
} catch (error) {
const err = createError(404, 'url not found')
err.details = error
return next(err);
}
})

server.use((req, res, next) => {
next(createError(404, 'route not found'));
});

server.use((err, req, res, next) => {
res.status(err.status || 500);

const errorResponse = {
status: err.status || 500,
message: err.message,
};

if (err.details) {
errorResponse.details = err.details;
}

res.json(errorResponse);
});
}

16 changes: 16 additions & 0 deletions src/utils/obj.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function deepMerge(target, source) {
Object.keys(source).forEach(key => {
const sourceValue = source[key];
const targetValue = target[key];

if (sourceValue && typeof sourceValue === 'object') {
if (!targetValue || typeof targetValue !== 'object') {
target[key] = Array.isArray(sourceValue) ? [] : {};
}
deepMerge(target[key], sourceValue);
} else {
target[key] = sourceValue;
}
});
return target;
};
Loading

0 comments on commit 03ed3c5

Please sign in to comment.