Skip to content
Merged
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
10 changes: 5 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ The project implements a **Hexagonal Architecture** backend (Java 24, Javalin, A
- [x] Add "Add to Collection" action in `LinkList` items.
- **Links Domain**:
- **Backend**:
- [ ] Implement `UpdateLinkUseCase` (Edit title, description, tags).
- [ ] Implement `DeleteLinkUseCase`.
- [x] Implement `UpdateLinkUseCase` (Edit title, description, tags).
- [x] Implement `DeleteLinkUseCase`.
- **Frontend**:
- [ ] Add Edit Link Modal/Form.
- [ ] Add Delete Link confirmation dialog.
- [ ] Update `LinkList` to reflect changes immediately.
- [x] Add Edit Link Modal/Form.
- [x] Add Delete Link confirmation dialog.
- [x] Update `LinkList` to reflect changes immediately.
- **Content Domain**:
- **Backend**:
- [ ] Review `DownloadContentService` robustness (retries).
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/it/robfrank/linklift/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,11 @@ public Javalin start(int port) {
GetContentController getContentController = new GetContentController(getContentUseCase);
AuthenticationController authenticationController = new AuthenticationController(userService, authenticationService, authenticationService);

// Initialize Link Management
UpdateLinkUseCase updateLinkUseCase = new UpdateLinkService(linkPersistenceAdapter, linkPersistenceAdapter);
DeleteLinkUseCase deleteLinkUseCase = new DeleteLinkService(linkPersistenceAdapter, linkPersistenceAdapter);
LinkController linkController = new LinkController(updateLinkUseCase, deleteLinkUseCase);

// Build and start web application
Javalin app = new WebBuilder()
.withAuthorizationService(authorizationService)
Expand All @@ -329,6 +334,7 @@ public Javalin start(int port) {
.withGetContentController(getContentController)
.withCollectionController(collectionController)
.withGetRelatedLinksController(getRelatedLinksController)
.withLinkManagementController(linkController)
.build();

app.start(port);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package it.robfrank.linklift.adapter.in.web;

import io.javalin.http.Context;
import it.robfrank.linklift.adapter.in.web.security.SecurityContext;
import it.robfrank.linklift.application.domain.model.Link;
import it.robfrank.linklift.application.port.in.DeleteLinkUseCase;
import it.robfrank.linklift.application.port.in.UpdateLinkCommand;
import it.robfrank.linklift.application.port.in.UpdateLinkUseCase;
import java.util.Objects;

public class LinkController {

private final UpdateLinkUseCase updateLinkUseCase;
private final DeleteLinkUseCase deleteLinkUseCase;

public LinkController(UpdateLinkUseCase updateLinkUseCase, DeleteLinkUseCase deleteLinkUseCase) {
this.updateLinkUseCase = updateLinkUseCase;
this.deleteLinkUseCase = deleteLinkUseCase;
}

public void updateLink(Context ctx) {
String id = Objects.requireNonNull(ctx.pathParam("id"));
String currentUserId = Objects.requireNonNull(SecurityContext.getCurrentUserId(ctx));

UpdateLinkRequest request = ctx.bodyAsClass(UpdateLinkRequest.class);

UpdateLinkCommand command = new UpdateLinkCommand(id, request.title(), request.description(), currentUserId);

Link updatedLink = updateLinkUseCase.updateLink(command);

ctx.json(new UpdateLinkResponse(updatedLink));
}

public void deleteLink(Context ctx) {
String id = Objects.requireNonNull(ctx.pathParam("id"));
String currentUserId = Objects.requireNonNull(SecurityContext.getCurrentUserId(ctx));

deleteLinkUseCase.deleteLink(id, currentUserId);

ctx.status(204);
}

public record UpdateLinkRequest(String title, String description) {}

public record UpdateLinkResponse(Link data) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ public Link saveLink(Link link) {
}
}

public Link updateLink(Link link) {
try {
database.transaction(() -> {
database.command(
"sql",
"""
UPDATE Link SET
title = ?,
description = ?
WHERE id = ?
""",
link.title(),
link.description(),
link.id()
);
});
return link;
} catch (ArcadeDBException e) {
throw new DatabaseException("Failed to update link: " + link.id(), e);
}
}

/**
* Save a link and create an OwnsLink relationship to the specified user.
* This method properly uses ArcadeDB's graph capabilities.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
import it.robfrank.linklift.application.domain.model.Link;
import it.robfrank.linklift.application.domain.model.LinkPage;
import it.robfrank.linklift.application.port.in.ListLinksQuery;
import it.robfrank.linklift.application.port.out.DeleteLinkPort;
import it.robfrank.linklift.application.port.out.LoadLinksPort;
import it.robfrank.linklift.application.port.out.SaveLinkPort;
import it.robfrank.linklift.application.port.out.UpdateLinkPort;
import java.util.List;
import java.util.Optional;
import org.jspecify.annotations.NonNull;

public class LinkPersistenceAdapter implements SaveLinkPort, LoadLinksPort {
public class LinkPersistenceAdapter implements SaveLinkPort, LoadLinksPort, UpdateLinkPort, DeleteLinkPort {

private final ArcadeLinkRepository linkRepository;

Expand Down Expand Up @@ -75,7 +78,8 @@ public Optional<String> getLinkOwner(String linkId) {
/**
* Delete a link and its relationships.
*/
public void deleteLink(String linkId) {
@Override
public void deleteLink(@NonNull String linkId) {
linkRepository.deleteLink(linkId);
}

Expand All @@ -90,4 +94,9 @@ public void transferLinkOwnership(String linkId, String fromUserId, String toUse
public List<Link> getRelatedLinks(String linkId, String userId) {
return linkRepository.getRelatedLinks(linkId, userId);
}

@Override
public @NonNull Link updateLink(@NonNull Link link) {
return java.util.Objects.requireNonNull(linkRepository.updateLink(link));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package it.robfrank.linklift.application.domain.service;

import it.robfrank.linklift.application.domain.exception.LinkNotFoundException;
import it.robfrank.linklift.application.port.in.DeleteLinkUseCase;
import it.robfrank.linklift.application.port.out.DeleteLinkPort;
import it.robfrank.linklift.application.port.out.LoadLinksPort;
import org.jspecify.annotations.NonNull;

public class DeleteLinkService implements DeleteLinkUseCase {

private final LoadLinksPort loadLinksPort;
private final DeleteLinkPort deleteLinkPort;

public DeleteLinkService(LoadLinksPort loadLinksPort, DeleteLinkPort deleteLinkPort) {
this.loadLinksPort = loadLinksPort;
this.deleteLinkPort = deleteLinkPort;
}

@Override
public void deleteLink(@NonNull String id, @NonNull String userId) {
// Check existence and ownership
if (!loadLinksPort.userOwnsLink(userId, id)) {
throw new LinkNotFoundException("Link not found or not owned by user");
}

deleteLinkPort.deleteLink(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package it.robfrank.linklift.application.domain.service;

import it.robfrank.linklift.application.domain.exception.LinkNotFoundException;
import it.robfrank.linklift.application.domain.model.Link;
import it.robfrank.linklift.application.port.in.UpdateLinkCommand;
import it.robfrank.linklift.application.port.in.UpdateLinkUseCase;
import it.robfrank.linklift.application.port.out.LoadLinksPort;
import it.robfrank.linklift.application.port.out.UpdateLinkPort;
import org.jspecify.annotations.NonNull;

public class UpdateLinkService implements UpdateLinkUseCase {

private final LoadLinksPort loadLinksPort;
private final UpdateLinkPort updateLinkPort;

public UpdateLinkService(LoadLinksPort loadLinksPort, UpdateLinkPort updateLinkPort) {
this.loadLinksPort = loadLinksPort;
this.updateLinkPort = updateLinkPort;
}

@Override
public @NonNull Link updateLink(@NonNull UpdateLinkCommand command) {
// Check existence
Link existingLink = loadLinksPort.getLinkById(command.id());

// Check ownership
if (!loadLinksPort.userOwnsLink(command.userId(), command.id())) {
throw new LinkNotFoundException("Link not found or not owned by user");
}

// Update fields
String newTitle = command.title() != null ? command.title() : existingLink.title();
String newDescription = command.description() != null ? command.description() : existingLink.description();

Link updatedLink = new Link(existingLink.id(), existingLink.url(), newTitle, newDescription, existingLink.extractedAt(), existingLink.contentType());

return updateLinkPort.updateLink(updatedLink);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package it.robfrank.linklift.application.port.in;

import org.jspecify.annotations.NonNull;

public interface DeleteLinkUseCase {
void deleteLink(@NonNull String id, @NonNull String userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package it.robfrank.linklift.application.port.in;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

public record UpdateLinkCommand(@NonNull String id, @Nullable String title, @Nullable String description, @NonNull String userId) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package it.robfrank.linklift.application.port.in;

import it.robfrank.linklift.application.domain.model.Link;
import org.jspecify.annotations.NonNull;

public interface UpdateLinkUseCase {
@NonNull
Link updateLink(@NonNull UpdateLinkCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package it.robfrank.linklift.application.port.out;

import org.jspecify.annotations.NonNull;

public interface DeleteLinkPort {
void deleteLink(@NonNull String id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ public interface LoadLinksPort {
LinkPage loadLinks(ListLinksQuery query);

List<Link> getRelatedLinks(String linkId, String userId);

Link getLinkById(String id);

boolean userOwnsLink(String userId, String linkId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package it.robfrank.linklift.application.port.out;

import it.robfrank.linklift.application.domain.model.Link;
import org.jspecify.annotations.NonNull;

public interface UpdateLinkPort {
@NonNull
Link updateLink(@NonNull Link link);
}
18 changes: 18 additions & 0 deletions src/main/java/it/robfrank/linklift/config/WebBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,24 @@ public WebBuilder withGetRelatedLinksController(GetRelatedLinksController getRel
return this;
}

public WebBuilder withLinkManagementController(LinkController linkController) {
app.before("/api/v1/links/{id}", requireAuthentication);

// Method-specific permission checks
app.before("/api/v1/links/{id}", ctx -> {
switch (ctx.method()) {
case PATCH -> RequirePermission.any(authorizationService, Role.Permissions.UPDATE_OWN_LINKS).handle(ctx);
case DELETE -> RequirePermission.any(authorizationService, Role.Permissions.DELETE_OWN_LINKS).handle(ctx);
default -> {}
}
});

app.patch("/api/v1/links/{id}", linkController::updateLink);
app.delete("/api/v1/links/{id}", linkController::deleteLink);

return this;
}

public Javalin build() {
return app;
}
Expand Down
Loading
Loading