Skip to content

Commit

Permalink
Merge pull request #79 from neumanf/feature/click-stats
Browse files Browse the repository at this point in the history
feat: click stats
  • Loading branch information
neumanf authored Jan 6, 2025
2 parents bc24ef8 + b79d5a8 commit d4ce910
Show file tree
Hide file tree
Showing 84 changed files with 1,675 additions and 326 deletions.
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ KEYCLOAK_URL=https://keycloak.domain.com
KEYCLOAK_ADMIN=
KEYCLOAK_ADMIN_PASSWORD=

# RabbitMQ
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USER=
RABBITMQ_PASSWORD=

# InfluxDB
INFLUX_URL=http://localhost:8086
INFLUX_USER=
INFLUX_PASSWORD=
INFLUX_TOKEN=
INFLUX_ORG=

# Grafana
GRAFANA_USER=
GRAFANA_PASSWORD=
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Run backend
uses: JarvusInnovations/background-action@v1
with:
run: docker compose -f docker-compose.testing.yml up &
run: docker compose -f docker-compose.test.yml up &
wait-on: http://localhost:8080/health/
wait-for: 10m

Expand Down
18 changes: 18 additions & 0 deletions apps/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>

<!-- InfluxDB -->
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId>
<version>7.2.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/main/java/com/mally/api/Application.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.mally.api;

import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableRabbit
public class Application {

public static void main(String[] args) {
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/main/java/com/mally/api/auth/UserJwt.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mally.api.auth;

import com.mally.api.auth.domain.valueobjects.UserId;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
Expand All @@ -21,7 +22,7 @@ public UserJwt(Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
super(jwt, authorities);
}

public String getId() {
return getName();
public UserId getId() {
return new UserId(getName());
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mally.api.configuration;
package com.mally.api.auth.configuration;

import com.mally.api.auth.UserJwtConverter;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.mally.api.auth.domain.valueobjects;

public record UserId(String value) {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.mally.api.pastebin.dtos.CreatePasteDTO;
import com.mally.api.pastebin.entities.Paste;
import com.mally.api.pastebin.services.PastebinService;
import com.mally.api.shared.rest.dtos.ApiResponseDTO;
import com.mally.api.shared.rest.dtos.ApiResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -33,7 +33,7 @@ public ResponseEntity<Page<Paste>> findAll(
) {
var userId = AuthenticationManager.getAuthenticatedUser().map(UserJwt::getId).orElseThrow();
var result = pastebinService.findAll(
userId,
userId.value(),
search,
PageRequest.of(pageNumber, pageSize, Sort.by(orderBy.equalsIgnoreCase("ASC") ? Sort.Direction.ASC : Sort.Direction.DESC, sortBy))
);
Expand All @@ -42,37 +42,37 @@ public ResponseEntity<Page<Paste>> findAll(
}

@GetMapping("/paste/{slug}")
public ResponseEntity<ApiResponseDTO> findPaste(@PathVariable String slug) {
public ResponseEntity<ApiResponse> findPaste(@PathVariable String slug) {
return pastebinService.findBySlug(slug)
.map(p -> ResponseEntity
.ok()
.body(ApiResponseDTO.success("Paste found successfully.", p)))
.body(ApiResponse.success("Paste found successfully.", p)))
.orElseGet(() -> ResponseEntity
.badRequest()
.body(ApiResponseDTO.error("Paste not found.", List.of())));
.body(ApiResponse.error("Paste not found.", List.of())));
}

@PostMapping("/paste")
public ResponseEntity<ApiResponseDTO> paste(@Valid @RequestBody CreatePasteDTO dto) {
public ResponseEntity<ApiResponse> paste(@Valid @RequestBody CreatePasteDTO dto) {
var userId = AuthenticationManager.getAuthenticatedUser().map(UserJwt::getId).orElse(null);
var paste = pastebinService.create(dto, userId);
var paste = pastebinService.create(dto, userId.value());

return ResponseEntity
.ok()
.body(ApiResponseDTO.success("Paste created successfully.", paste));
.body(ApiResponse.success("Paste created successfully.", paste));
}

@DeleteMapping("/{id}")
public ResponseEntity<ApiResponseDTO> delete(@PathVariable Long id) {
public ResponseEntity<ApiResponse> delete(@PathVariable Long id) {
pastebinService.delete(id);

return ResponseEntity.ok(ApiResponseDTO.success("Paste deleted successfully.", null));
return ResponseEntity.ok(ApiResponse.success("Paste deleted successfully.", null));
}

@DeleteMapping("bulk")
public ResponseEntity<ApiResponseDTO> bulkDelete(@RequestParam List<Long> id) {
public ResponseEntity<ApiResponse> bulkDelete(@RequestParam List<Long> id) {
pastebinService.deleteMany(id);

return ResponseEntity.ok(ApiResponseDTO.success("Pastes deleted successfully.", null));
return ResponseEntity.ok(ApiResponse.success("Pastes deleted successfully.", null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mally.api.shared.abstractions;

public interface UseCase<TRequest, TResponse> {
TResponse execute(TRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mally.api.shared.commands;

import com.mally.api.auth.domain.valueobjects.UserId;
import com.mally.api.shared.utils.PaginationUtils;
import jakarta.annotation.Nullable;
import org.springframework.data.domain.Pageable;

public record FindAllCommand(
UserId userId,
@Nullable String search,
Pageable pageable
) {
public FindAllCommand(UserId userId, String search, int pageNumber, int pageSize, String orderBy, String sortBy) {
this(userId, search, PaginationUtils.buildPageable(pageNumber, pageSize, orderBy, sortBy));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mally.api.shared.configuration;

import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class InfluxConfiguration {

@Value("${influx.url}")
private String connectionUrl;

@Value("${influx.token}")
private String token;

@Value("${influx.org}")
private String org;

@Bean
public InfluxDBClient influxDBClient() {
return InfluxDBClientFactory.create(connectionUrl, token.toCharArray(), org);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.mally.api.shared.configuration;

import com.influxdb.client.BucketsApi;
import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.domain.Bucket;
import com.influxdb.client.domain.Organization;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class InfluxInitializer {
private final InfluxDBClient influxDBClient;

private final String bucketNames;

private final String orgName;

public InfluxInitializer(InfluxDBClient influxDBClient,
@Value("${influx.buckets}") String buckets,
@Value("${influx.org}") String org) {
this.influxDBClient = influxDBClient;
this.bucketNames = buckets;
this.orgName = org;
}

@PostConstruct
public void init() {
createBucketsIfNotExists();
}

private void createBucketsIfNotExists() {
BucketsApi bucketsApi = influxDBClient.getBucketsApi();

Organization organization = influxDBClient.getOrganizationsApi()
.findOrganizations().stream()
.filter(org -> org.getName().equals(orgName))
.findFirst()
.orElseThrow();

for (String bucketName : bucketNames.split(",")) {
boolean bucketAlreadyCreated = bucketsApi.findBucketByName(bucketName) != null;

if (bucketAlreadyCreated) {
log.info("Bucket with name {} already created, skipping...", bucketName);
continue;
}

Bucket bucket = new Bucket();
bucket.setName(bucketName);
bucket.setOrgID(organization.getId());

bucketsApi.createBucket(bucket);
log.info("Created bucket {}", bucketName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.mally.api.shared.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfiguration {

private final ObjectMapper objectMapper;

public RabbitMQConfiguration(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Bean
MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter(objectMapper);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.mally.api.shared.repositories;

import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.domain.WritePrecision;
import com.influxdb.query.FluxRecord;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;

@Repository
public class InfluxRepository {

private final String org;

private final InfluxDBClient influxDBClient;

InfluxRepository(InfluxDBClient influxDBClient, @Value("${influx.org}") String org) {
this.influxDBClient = influxDBClient;
this.org = org;
}

public List<FluxRecord> query(String query) {
var queryApi = influxDBClient.getQueryApi();

var tables = queryApi.query(query);

var results = new ArrayList<FluxRecord>();

for (var table : tables) {
var records = table.getRecords();

results.addAll(records);
}

return results;
}

public void writeMeasurement(Object measurement, String bucket) {
influxDBClient.getWriteApiBlocking().writeMeasurement(bucket, org, WritePrecision.MS, measurement);
}

public void deleteMeasurement(Long measurementId, String bucket) {
var start = OffsetDateTime.now().minusYears(100);
var stop = OffsetDateTime.now();
var predicate = String.format("id=\"%s\"", measurementId);

influxDBClient.getDeleteApi().delete(start, stop, predicate, bucket, org);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.mally.api.shared.rest.advices;

import com.mally.api.shared.rest.dtos.ApiResponseDTO;
import com.mally.api.shared.rest.dtos.ApiResponse;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
Expand All @@ -20,28 +20,28 @@ class ErrorHandlingControllerAdvice {
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ApiResponseDTO onConstraintValidationException(ConstraintViolationException e) {
ApiResponse onConstraintValidationException(ConstraintViolationException e) {
List<String> error = new ArrayList<>();

for (ConstraintViolation violation : e.getConstraintViolations()) {
error.add(violation.getPropertyPath().toString() + ' ' + violation.getMessage());
}

return ApiResponseDTO.error("Validation error", error);
return ApiResponse.error("Validation error", error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ApiResponseDTO onMethodArgumentNotValidException(
ApiResponse onMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
List<String> error = new ArrayList<>();

for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
error.add(fieldError.getField() + ' ' + fieldError.getDefaultMessage());
}

return ApiResponseDTO.error("Validation error", error);
return ApiResponse.error("Validation error", error);
}

}
Loading

0 comments on commit d4ce910

Please sign in to comment.