Add data migration export/import endpoints
All checks were successful
Backend Build and Test / build (push) Successful in 42s
All checks were successful
Backend Build and Test / build (push) Successful in 42s
Add GET /api/export to the Go backend that dumps all users, allowances, history, and tasks (including completed) as a single JSON snapshot. Add POST /api/import to the Spring backend that accepts the same JSON, wipes existing data, inserts all records with original IDs preserved via native SQL, and resets PostgreSQL sequences to avoid future collisions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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