Skip to content

Commit

Permalink
feat(redirects): store visits
Browse files Browse the repository at this point in the history
  • Loading branch information
RomainLanz committed Dec 14, 2024
1 parent 38d51b3 commit 6e70c2d
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { inject } from '@adonisjs/core';
import { TimeServiceContract } from '#core/contracts/time_service_contract';
import { Visit } from '#redirects/domain/visit';
import { VisitIdentifier } from '#redirects/domain/visit_identifier';
import { RedirectRepository } from '#redirects/repositories/redirect_repository';
import { VisitRepository } from '#redirects/repositories/visit_repository';
import type { HttpContext } from '@adonisjs/core/http';

@inject()
export default class ProcessRedirectController {
constructor(private repository: RedirectRepository) {}
constructor(
private readonly redirectRepository: RedirectRepository,
private readonly visitRepository: VisitRepository,
private readonly timeService: TimeServiceContract
) {}

async execute({ params, response }: HttpContext) {
const redirect = await this.repository.findByUrl(params['*']);
async execute({ params, request, response }: HttpContext) {
const redirect = await this.redirectRepository.findByUrl(params['*']);

if (!redirect) {
return response.status(404).send('Not found');
}
const visit = Visit.create({
id: VisitIdentifier.generate(),
createdAt: this.timeService.now(),
ipAddressRaw: request.ip(),
referer: request.header('referer') ?? '',
redirectId: redirect.getIdentifier(),
});

await this.repository.increaseVisitCount(redirect.id);
await this.visitRepository.save(visit);

return response.redirect(redirect.to);
return response.redirect(redirect.props.destination);
}
}
29 changes: 29 additions & 0 deletions apps/romainlanz.com/app/redirects/domain/visit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { hash } from 'node:crypto';
import { Entity } from '#core/domain/entity';
import type { RedirectIdentifier } from '#redirects/domain/redirect_identifier';
import type { VisitIdentifier } from '#redirects/domain/visit_identifier';
import type { DateTime } from 'luxon';

interface Properties {
id: VisitIdentifier;
createdAt: DateTime;
ipAddressRaw?: string;
ipAddress?: string;
referer: string;
redirectId: RedirectIdentifier;
}

export class Visit extends Entity<Properties> {
static create(properties: Properties) {
const instance = new this(properties);

if (properties.ipAddressRaw) {
instance.props.ipAddress = hash(
'md5',
`${properties.ipAddressRaw}${properties.createdAt.toFormat('yyyy-MM-dd')}`
);
}

return instance;
}
}
3 changes: 3 additions & 0 deletions apps/romainlanz.com/app/redirects/domain/visit_identifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Identifier } from '#core/domain/identifier';

export class VisitIdentifier extends Identifier<'VisitIdentifier'> {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { db } from '#core/services/db';
import { Redirect } from '#redirects/domain/redirect';
import { RedirectIdentifier } from '#redirects/domain/redirect_identifier';
import { sql } from 'kysely';

interface StoreRedirectDTO {
url: string;
Expand All @@ -25,16 +24,18 @@ export class RedirectRepository {
});
}

findByUrl(url: string) {
return db.selectFrom('redirects').where('url', '=', url).select(['id', 'to']).executeTakeFirst();
}
async findByUrl(url: string) {
const redirectRecord = await db
.selectFrom('redirects')
.where('url', '=', url)
.select(['id', 'to'])
.executeTakeFirstOrThrow();

increaseVisitCount(id: string) {
return db
.updateTable('redirects')
.set('visit_count', sql`visit_count + 1`)
.where('id', '=', id)
.execute();
return Redirect.create({
id: RedirectIdentifier.fromString(redirectRecord.id),
destination: redirectRecord.to,
slug: url,
});
}

create(payload: StoreRedirectDTO) {
Expand Down
26 changes: 26 additions & 0 deletions apps/romainlanz.com/app/redirects/repositories/visit_repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { assertExists } from '@adonisjs/core/helpers/assert';
import { db } from '#core/services/db';
import { sql } from 'kysely';
import type { Visit } from '#redirects/domain/visit';

export class VisitRepository {
save(visit: Visit) {
assertExists(visit.props.ipAddress, 'IP address is required');

return db
.insertInto('visits')
.values({
id: visit.getIdentifier().toString(),
created_at: visit.props.createdAt.toJSDate(),
ip_address: visit.props.ipAddress,
referer: visit.props.referer,
redirect_id: visit.props.redirectId.toString(),
})
.onConflict((builder) =>
builder.column('ip_address').doUpdateSet({
count: sql`visits.count + 1`,
})
)
.execute();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Kysely } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('visits')
.addColumn('id', 'uuid', (col) => col.primaryKey())
.addColumn('created_at', 'timestamptz', (col) => col.notNull())
.addColumn('ip_address', 'uuid', (col) => col.unique().notNull())
.addColumn('referer', 'text')
.addColumn('redirect_id', 'uuid', (col) => col.notNull())
.addColumn('count', 'int4', (col) => col.notNull().defaultTo(1))
.execute();

await db.schema.createIndex('fk_visits_redirect_id').on('visits').column('redirect_id').execute();
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('visits').execute();
}
12 changes: 11 additions & 1 deletion apps/romainlanz.com/types/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;

export type Timestamp = ColumnType<Date, Date | string>;
export type Timestamp = ColumnType<Date, Date | string, Date | string>;

export interface Articles {
category_id: string;
Expand Down Expand Up @@ -78,6 +78,15 @@ export interface Users {
updated_at: Timestamp | null;
}

export interface Visits {
count: Generated<number>;
created_at: Timestamp;
id: string;
ip_address: string;
redirect_id: string;
referer: string | null;
}

export interface DB {
articles: Articles;
categories: Categories;
Expand All @@ -87,4 +96,5 @@ export interface DB {
tag_articles: TagArticles;
tags: Tags;
users: Users;
visits: Visits;
}

0 comments on commit 6e70c2d

Please sign in to comment.