switch-to-spring #147
@@ -2,6 +2,7 @@ package be.seeseepuff.allowanceplanner.controller;
|
||||
|
||||
import be.seeseepuff.allowanceplanner.dto.*;
|
||||
import be.seeseepuff.allowanceplanner.service.AllowanceService;
|
||||
import be.seeseepuff.allowanceplanner.service.MigrationService;
|
||||
import be.seeseepuff.allowanceplanner.service.TaskService;
|
||||
import be.seeseepuff.allowanceplanner.service.TransferService;
|
||||
import be.seeseepuff.allowanceplanner.service.UserService;
|
||||
@@ -21,16 +22,19 @@ public class ApiController
|
||||
private final AllowanceService allowanceService;
|
||||
private final TaskService taskService;
|
||||
private final TransferService transferService;
|
||||
private final MigrationService migrationService;
|
||||
|
||||
public ApiController(UserService userService,
|
||||
AllowanceService allowanceService,
|
||||
TaskService taskService,
|
||||
TransferService transferService)
|
||||
TransferService transferService,
|
||||
MigrationService migrationService)
|
||||
{
|
||||
this.userService = userService;
|
||||
this.allowanceService = allowanceService;
|
||||
this.taskService = taskService;
|
||||
this.transferService = transferService;
|
||||
this.migrationService = migrationService;
|
||||
}
|
||||
|
||||
// ---- Users ----
|
||||
@@ -503,4 +507,13 @@ public class ApiController
|
||||
ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(result.message()));
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Migration ----
|
||||
|
||||
@PostMapping("/import")
|
||||
public ResponseEntity<?> importData(@RequestBody MigrationDto data)
|
||||
{
|
||||
migrationService.importData(data);
|
||||
return ResponseEntity.ok(new MessageResponse("Import successful"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package be.seeseepuff.allowanceplanner.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record MigrationDto(
|
||||
List<MigrationUserDto> users,
|
||||
List<MigrationAllowanceDto> allowances,
|
||||
List<MigrationHistoryDto> history,
|
||||
List<MigrationTaskDto> tasks
|
||||
)
|
||||
{
|
||||
public record MigrationUserDto(int id, String name, long balance, double weight) {}
|
||||
|
||||
public record MigrationAllowanceDto(int id, int userId, String name, long target, long balance, double weight,
|
||||
Integer colour) {}
|
||||
|
||||
public record MigrationHistoryDto(int id, int userId, long timestamp, long amount, String description) {}
|
||||
|
||||
public record MigrationTaskDto(int id, String name, long reward, Integer assigned, String schedule,
|
||||
Long completed, Long nextRun) {}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package be.seeseepuff.allowanceplanner.service;
|
||||
|
||||
import be.seeseepuff.allowanceplanner.dto.MigrationDto;
|
||||
import be.seeseepuff.allowanceplanner.repository.AllowanceRepository;
|
||||
import be.seeseepuff.allowanceplanner.repository.HistoryRepository;
|
||||
import be.seeseepuff.allowanceplanner.repository.TaskRepository;
|
||||
import be.seeseepuff.allowanceplanner.repository.UserRepository;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class MigrationService
|
||||
{
|
||||
private final UserRepository userRepository;
|
||||
private final AllowanceRepository allowanceRepository;
|
||||
private final HistoryRepository historyRepository;
|
||||
private final TaskRepository taskRepository;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public MigrationService(UserRepository userRepository,
|
||||
AllowanceRepository allowanceRepository,
|
||||
HistoryRepository historyRepository,
|
||||
TaskRepository taskRepository,
|
||||
EntityManager entityManager)
|
||||
{
|
||||
this.userRepository = userRepository;
|
||||
this.allowanceRepository = allowanceRepository;
|
||||
this.historyRepository = historyRepository;
|
||||
this.taskRepository = taskRepository;
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void importData(MigrationDto data)
|
||||
{
|
||||
// Delete in dependency order
|
||||
taskRepository.deleteAll();
|
||||
historyRepository.deleteAll();
|
||||
allowanceRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
|
||||
// Insert users with original IDs using native SQL to bypass auto-increment
|
||||
for (MigrationDto.MigrationUserDto u : data.users())
|
||||
{
|
||||
entityManager.createNativeQuery(
|
||||
"INSERT INTO users (id, name, balance, weight) VALUES (:id, :name, :balance, :weight)")
|
||||
.setParameter("id", u.id())
|
||||
.setParameter("name", u.name())
|
||||
.setParameter("balance", u.balance())
|
||||
.setParameter("weight", u.weight())
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
// Insert allowances with original IDs
|
||||
for (MigrationDto.MigrationAllowanceDto a : data.allowances())
|
||||
{
|
||||
entityManager.createNativeQuery(
|
||||
"INSERT INTO allowances (id, user_id, name, target, balance, weight, colour) VALUES (:id, :userId, :name, :target, :balance, :weight, :colour)")
|
||||
.setParameter("id", a.id())
|
||||
.setParameter("userId", a.userId())
|
||||
.setParameter("name", a.name())
|
||||
.setParameter("target", a.target())
|
||||
.setParameter("balance", a.balance())
|
||||
.setParameter("weight", a.weight())
|
||||
.setParameter("colour", a.colour())
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
// Insert history with original IDs
|
||||
for (MigrationDto.MigrationHistoryDto h : data.history())
|
||||
{
|
||||
entityManager.createNativeQuery(
|
||||
"INSERT INTO history (id, user_id, timestamp, amount, description) VALUES (:id, :userId, :timestamp, :amount, :description)")
|
||||
.setParameter("id", h.id())
|
||||
.setParameter("userId", h.userId())
|
||||
.setParameter("timestamp", h.timestamp())
|
||||
.setParameter("amount", h.amount())
|
||||
.setParameter("description", h.description())
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
// Insert tasks with original IDs
|
||||
for (MigrationDto.MigrationTaskDto t : data.tasks())
|
||||
{
|
||||
entityManager.createNativeQuery(
|
||||
"INSERT INTO tasks (id, name, reward, assigned, schedule, completed, next_run) VALUES (:id, :name, :reward, :assigned, :schedule, :completed, :nextRun)")
|
||||
.setParameter("id", t.id())
|
||||
.setParameter("name", t.name())
|
||||
.setParameter("reward", t.reward())
|
||||
.setParameter("assigned", t.assigned())
|
||||
.setParameter("schedule", t.schedule())
|
||||
.setParameter("completed", t.completed())
|
||||
.setParameter("nextRun", t.nextRun())
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
// Reset sequences so new inserts don't collide with migrated IDs
|
||||
entityManager.createNativeQuery("SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 0))").getSingleResult();
|
||||
entityManager.createNativeQuery("SELECT setval('allowances_id_seq', COALESCE((SELECT MAX(id) FROM allowances), 0))").getSingleResult();
|
||||
entityManager.createNativeQuery("SELECT setval('history_id_seq', COALESCE((SELECT MAX(id) FROM history), 0))").getSingleResult();
|
||||
entityManager.createNativeQuery("SELECT setval('tasks_id_seq', COALESCE((SELECT MAX(id) FROM tasks), 0))").getSingleResult();
|
||||
}
|
||||
}
|
||||
@@ -779,3 +779,59 @@ func (db *Db) TransferAllowance(fromId int, toId int, amount float64) error {
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (db *Db) ExportAllData() (*ExportData, error) {
|
||||
var err error
|
||||
data := &ExportData{
|
||||
Users: make([]ExportUser, 0),
|
||||
Allowances: make([]ExportAllowance, 0),
|
||||
History: make([]ExportHistory, 0),
|
||||
Tasks: make([]ExportTask, 0),
|
||||
}
|
||||
|
||||
for row := range db.db.Query("select id, name, balance, weight from users").Range(&err) {
|
||||
u := ExportUser{}
|
||||
if err = row.Scan(&u.ID, &u.Name, &u.Balance, &u.Weight); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.Users = append(data.Users, u)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for row := range db.db.Query("select id, user_id, name, target, balance, weight, colour from allowances").Range(&err) {
|
||||
a := ExportAllowance{}
|
||||
if err = row.Scan(&a.ID, &a.UserID, &a.Name, &a.Target, &a.Balance, &a.Weight, &a.Colour); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.Allowances = append(data.Allowances, a)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for row := range db.db.Query("select id, user_id, timestamp, amount, description from history").Range(&err) {
|
||||
h := ExportHistory{}
|
||||
if err = row.Scan(&h.ID, &h.UserID, &h.Timestamp, &h.Amount, &h.Description); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.History = append(data.History, h)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for row := range db.db.Query("select id, name, reward, assigned, schedule, completed, next_run from tasks").Range(&err) {
|
||||
t := ExportTask{}
|
||||
if err = row.Scan(&t.ID, &t.Name, &t.Reward, &t.Assigned, &t.Schedule, &t.Completed, &t.NextRun); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.Tasks = append(data.Tasks, t)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -86,3 +86,45 @@ type TransferRequest struct {
|
||||
To int `json:"to"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type ExportUser struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Balance int64 `json:"balance"`
|
||||
Weight float64 `json:"weight"`
|
||||
}
|
||||
|
||||
type ExportAllowance struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Target int64 `json:"target"`
|
||||
Balance int64 `json:"balance"`
|
||||
Weight float64 `json:"weight"`
|
||||
Colour *int `json:"colour"`
|
||||
}
|
||||
|
||||
type ExportHistory struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"userId"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Amount int64 `json:"amount"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type ExportTask struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Reward int64 `json:"reward"`
|
||||
Assigned *int `json:"assigned"`
|
||||
Schedule *string `json:"schedule"`
|
||||
Completed *int64 `json:"completed"`
|
||||
NextRun *int64 `json:"nextRun"`
|
||||
}
|
||||
|
||||
type ExportData struct {
|
||||
Users []ExportUser `json:"users"`
|
||||
Allowances []ExportAllowance `json:"allowances"`
|
||||
History []ExportHistory `json:"history"`
|
||||
Tasks []ExportTask `json:"tasks"`
|
||||
}
|
||||
|
||||
@@ -51,6 +51,16 @@ const DefaultDomain = "localhost:8080"
|
||||
// The domain that the server is reachable at.
|
||||
var domain = DefaultDomain
|
||||
|
||||
func exportData(c *gin.Context) {
|
||||
data, err := db.ExportAllData()
|
||||
if err != nil {
|
||||
log.Printf("Error exporting data: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||
return
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func getUsers(c *gin.Context) {
|
||||
users, err := db.GetUsers()
|
||||
if err != nil {
|
||||
@@ -713,6 +723,7 @@ func start(ctx context.Context, config *ServerConfig) {
|
||||
router.DELETE("/api/task/:taskId", deleteTask)
|
||||
router.POST("/api/task/:taskId/complete", completeTask)
|
||||
router.POST("/api/transfer", transfer)
|
||||
router.GET("/api/export", exportData)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: config.Addr,
|
||||
|
||||
Reference in New Issue
Block a user