Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ffefa93
feat: correct sticker-award to handle errors according to API spec
scottgerring Jun 17, 2025
a0b9565
feat: add AWS implementation and IaC
jeastham1993 Jun 17, 2025
f92f770
Update user-management/infra/aws/lib/network.ts
jeastham1993 Jun 17, 2025
03e01a1
feat: update connection string
jeastham1993 Jun 17, 2025
e02a6e2
Merge branch 'feat/infra-as-code' of github.com:DataDog/stickerlandia…
jeastham1993 Jun 17, 2025
5dc72ca
Merge pull request #34 from DataDog/feat/return-structured-errors
scottgerring Jun 18, 2025
9fd53b1
chore: add labels to docker image
jeastham1993 Jun 18, 2025
c784466
chore: remove ASPNET Lambda hosting
jeastham1993 Jun 18, 2025
ef2c47a
chore: add todos for future shared infra
jeastham1993 Jun 18, 2025
46378bf
feat: enable static analysis (#44)
jeastham1993 Jun 18, 2025
4e9c392
Merge pull request #45 from DataDog/feat/add-datadog-to-docker
jeastham1993 Jun 18, 2025
630ded4
feat(user-management): update docs (#37)
jeastham1993 Jun 18, 2025
a0d6c50
feat: add AWS implementation and IaC
jeastham1993 Jun 17, 2025
70df2cf
feat: update connection string
jeastham1993 Jun 17, 2025
728f589
Update user-management/infra/aws/lib/network.ts
jeastham1993 Jun 17, 2025
3f8db04
chore: add labels to docker image
jeastham1993 Jun 18, 2025
4dc35d0
chore: remove ASPNET Lambda hosting
jeastham1993 Jun 18, 2025
c94f5d1
chore: add todos for future shared infra
jeastham1993 Jun 18, 2025
da4a502
Merge branch 'feat/infra-as-code' of github.com:DataDog/stickerlandia…
jeastham1993 Jun 18, 2025
22dfdd6
chore: fix static analysis for AWS impls
jeastham1993 Jun 18, 2025
afea477
feat: add arch diagram and README for AWS infra
jeastham1993 Jun 18, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/user-management.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
shell: bash
run: |
cd user-management
dotnet restore
dotnet restore Stickerlandia.UserManagement.sln
- name: Run Integration Tests
shell: bash
env:
Expand Down Expand Up @@ -105,7 +105,7 @@ jobs:
shell: bash
run: |
cd user-management
dotnet restore
dotnet restore Stickerlandia.UserManagement.sln
- name: Run Integration Tests
shell: bash
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.datadoghq.stickerlandia.stickeraward.award.dto.GetUserStickersResponse;
import com.datadoghq.stickerlandia.stickeraward.award.dto.RemoveStickerFromUserResponse;
import com.datadoghq.stickerlandia.stickeraward.award.messaging.StickerAwardEventPublisher;
import com.datadoghq.stickerlandia.stickeraward.common.exception.ProblemDetailsResponseBuilder;
import io.smallrye.common.constraint.NotNull;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
Expand Down Expand Up @@ -58,7 +59,8 @@ public Response assignStickerToUser(
AssignStickerResponse response =
stickerAwardRepository.assignStickerToUser(userId, stickerId, request);
if (response == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
return ProblemDetailsResponseBuilder.badRequest(
"Invalid request or sticker assignment failed");
}

// Publish events - Note: We'll need to modify event publisher to work with DTOs
Expand All @@ -73,7 +75,8 @@ public Response assignStickerToUser(

return Response.status(Response.Status.CREATED).entity(response).build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.CONFLICT).build();
return ProblemDetailsResponseBuilder.conflict(
"Sticker assignment conflict: " + e.getMessage());
}
}

Expand All @@ -95,7 +98,11 @@ public Response removeStickerFromUser(
RemoveStickerFromUserResponse response =
stickerAwardRepository.removeStickerFromUser(userId, stickerId);
if (response == null) {
return Response.status(Response.Status.NOT_FOUND).build();
return ProblemDetailsResponseBuilder.notFound(
"Sticker assignment not found for user "
+ userId
+ " and sticker "
+ stickerId);
}

// Publish events - Note: We'll need to modify event publisher to work with DTOs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.datadoghq.stickerlandia.stickeraward.common.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;

/** RFC 7807 Problem Details for HTTP APIs. */
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProblemDetails {
@JsonProperty("type")
private String type;

@JsonProperty("title")
private String title;

@JsonProperty("status")
private Integer status;

@JsonProperty("detail")
private String detail;

@JsonProperty("instance")
private String instance;

private Map<String, Object> additionalProperties;

/** Default constructor. */
public ProblemDetails() {}

/**
* Constructor with status and title.
*
* @param status HTTP status code
* @param title brief, human-readable message
*/
public ProblemDetails(int status, String title) {
this.status = status;
this.title = title;
}

/**
* Constructor with status, title, and detail.
*
* @param status HTTP status code
* @param title brief, human-readable message
* @param detail human-readable explanation
*/
public ProblemDetails(int status, String title, String detail) {
this.status = status;
this.title = title;
this.detail = detail;
}

/**
* Constructor with type, status, title, and detail.
*
* @param type URI reference that identifies the problem type
* @param status HTTP status code
* @param title brief, human-readable message
* @param detail human-readable explanation
*/
public ProblemDetails(String type, int status, String title, String detail) {
this.type = type;
this.status = status;
this.title = title;
this.detail = detail;
}

/**
* Full constructor with all RFC 7807 fields.
*
* @param type URI reference that identifies the problem type
* @param status HTTP status code
* @param title brief, human-readable message
* @param detail human-readable explanation
* @param instance URI reference that identifies the specific occurrence
*/
public ProblemDetails(String type, int status, String title, String detail, String instance) {
this.type = type;
this.status = status;
this.title = title;
this.detail = detail;
this.instance = instance;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public Integer getStatus() {
return status;
}

public void setStatus(Integer status) {
this.status = status;
}

public String getDetail() {
return detail;
}

public void setDetail(String detail) {
this.detail = detail;
}

public String getInstance() {
return instance;
}

public void setInstance(String instance) {
this.instance = instance;
}

public Map<String, Object> getAdditionalProperties() {
return additionalProperties;
}

public void setAdditionalProperties(Map<String, Object> additionalProperties) {
this.additionalProperties = additionalProperties;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.datadoghq.stickerlandia.stickeraward.common.exception;

import com.datadoghq.stickerlandia.stickeraward.common.dto.ProblemDetails;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

/** JAX-RS exception mapper for WebApplicationException to RFC 7807 ProblemDetails. */
@Provider
public class ProblemDetailsExceptionMapper implements ExceptionMapper<WebApplicationException> {

private static final String PROBLEM_JSON_TYPE = "application/problem+json";

@Override
public Response toResponse(WebApplicationException exception) {
int status = exception.getResponse().getStatus();
String title = getDefaultTitle(status);
String detail = exception.getMessage();

ProblemDetails problemDetails = new ProblemDetails(status, title, detail);

return Response.status(status)
.entity(problemDetails)
.header("Content-Type", PROBLEM_JSON_TYPE)
.build();
}

private String getDefaultTitle(int status) {
return switch (status) {
case 400 -> "Bad Request";
case 401 -> "Unauthorized";
case 403 -> "Forbidden";
case 404 -> "Not Found";
case 409 -> "Conflict";
case 500 -> "Internal Server Error";
default -> "Error";
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.datadoghq.stickerlandia.stickeraward.common.exception;

import com.datadoghq.stickerlandia.stickeraward.common.dto.ProblemDetails;
import jakarta.ws.rs.core.Response;

/** Utility class for building RFC 7807 ProblemDetails HTTP responses. */
public class ProblemDetailsResponseBuilder {

private static final String PROBLEM_JSON_TYPE = "application/problem+json";

public static Response badRequest(String detail) {
return buildResponse(400, "Bad Request", detail);
}

public static Response unauthorized(String detail) {
return buildResponse(401, "Unauthorized", detail);
}

public static Response forbidden(String detail) {
return buildResponse(403, "Forbidden", detail);
}

public static Response notFound(String detail) {
return buildResponse(404, "Not Found", detail);
}

public static Response conflict(String detail) {
return buildResponse(409, "Conflict", detail);
}

public static Response internalServerError(String detail) {
return buildResponse(500, "Internal Server Error", detail);
}

private static Response buildResponse(int status, String title, String detail) {
ProblemDetails problemDetails = new ProblemDetails(status, title, detail);

return Response.status(status)
.entity(problemDetails)
.header("Content-Type", PROBLEM_JSON_TYPE)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.datadoghq.stickerlandia.stickeraward.sticker;

import com.datadoghq.stickerlandia.stickeraward.common.exception.ProblemDetailsResponseBuilder;
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.CreateStickerRequest;
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.CreateStickerResponse;
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.GetAllStickersResponse;
Expand Down Expand Up @@ -75,7 +76,8 @@ public Response createSticker(@NotNull CreateStickerRequest data) {
public Response getStickerMetadata(@PathParam("stickerId") String stickerId) {
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
if (metadata == null) {
return Response.status(Response.Status.NOT_FOUND).build();
return ProblemDetailsResponseBuilder.notFound(
"Sticker with ID " + stickerId + " not found");
}
return Response.ok(metadata).build();
}
Expand All @@ -96,7 +98,8 @@ public Response updateStickerMetadata(
@PathParam("stickerId") String stickerId, @NotNull UpdateStickerRequest data) {
StickerDTO updated = stickerRepository.updateStickerMetadata(stickerId, data);
if (updated == null) {
return Response.status(Response.Status.NOT_FOUND).build();
return ProblemDetailsResponseBuilder.notFound(
"Sticker with ID " + stickerId + " not found");
}
return Response.ok(updated).build();
}
Expand All @@ -114,13 +117,13 @@ public Response deleteSticker(@PathParam("stickerId") String stickerId) {
try {
boolean deleted = stickerRepository.deleteSticker(stickerId);
if (!deleted) {
return Response.status(Response.Status.NOT_FOUND).build();
return ProblemDetailsResponseBuilder.notFound(
"Sticker with ID " + stickerId + " not found");
}
return Response.noContent().build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Cannot delete sticker that is assigned to users")
.build();
return ProblemDetailsResponseBuilder.badRequest(
"Cannot delete sticker that is assigned to users");
}
}

Expand All @@ -137,22 +140,21 @@ public Response deleteSticker(@PathParam("stickerId") String stickerId) {
public Response getStickerImage(@PathParam("stickerId") String stickerId) {
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
if (metadata == null) {
return Response.status(Response.Status.NOT_FOUND).build();
return ProblemDetailsResponseBuilder.notFound(
"Sticker with ID " + stickerId + " not found");
}

if (metadata.getImageKey() == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("No image found for this sticker")
.build();
return ProblemDetailsResponseBuilder.notFound(
"No image found for sticker " + stickerId);
}

try {
InputStream imageStream = stickerImageService.getImage(metadata.getImageKey());
return Response.ok(imageStream).type("image/png").build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Failed to retrieve image")
.build();
return ProblemDetailsResponseBuilder.internalServerError(
"Failed to retrieve image for sticker " + stickerId);
}
}

Expand All @@ -172,7 +174,8 @@ public Response uploadStickerImage(
@PathParam("stickerId") String stickerId, @NotNull InputStream data) {
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
if (metadata == null) {
return Response.status(Response.Status.NOT_FOUND).build();
return ProblemDetailsResponseBuilder.notFound(
"Sticker with ID " + stickerId + " not found");
}

try {
Expand All @@ -195,9 +198,8 @@ public Response uploadStickerImage(
return Response.ok(response).build();
} catch (Exception e) {
System.out.println(e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Failed to upload image")
.build();
return ProblemDetailsResponseBuilder.internalServerError(
"Failed to upload image for sticker " + stickerId);
}
}
}
Loading
Loading