feat: sync — push/pull API endpoints

- POST /api/v1/sync/push — accepts ops, assigns revisions, returns accepted list

- POST /api/v1/sync/pull — returns ops since given revision with server_revision
This commit is contained in:
mirivlad 2026-06-01 22:51:30 +08:00
parent 10c6d06e38
commit ad684eb118
1 changed files with 101 additions and 2 deletions

View File

@ -358,14 +358,113 @@ func (s *Server) handleSyncPush(w http.ResponseWriter, r *http.Request) {
if !s.requireAPIKey(w, r) {
return
}
jsonOK(w, map[string]string{"status": "ok", "message": "push endpoint ready (not yet implemented)"})
if r.Method != "POST" {
jsonErr(w, 405, "POST required")
return
}
var req struct {
DeviceID string `json:"device_id"`
Ops []struct {
OpID string `json:"op_id"`
EntityType string `json:"entity_type"`
EntityID string `json:"entity_id"`
OpType string `json:"op_type"`
PayloadJSON string `json:"payload_json"`
CreatedAt string `json:"created_at"`
} `json:"ops"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonErr(w, 400, "invalid JSON: "+err.Error())
return
}
var accepted []string
for _, op := range req.Ops {
if op.OpID == "" || op.EntityType == "" || op.EntityID == "" || op.OpType == "" {
continue
}
_, err := s.db.Exec(
`INSERT OR IGNORE INTO server_ops (op_id, device_id, entity_type, entity_id, op_type, payload_json, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
op.OpID, req.DeviceID, op.EntityType, op.EntityID, op.OpType, op.PayloadJSON, op.CreatedAt,
)
if err != nil {
continue
}
// Assign revision.
res, err := s.db.Exec(
"INSERT INTO server_revisions (op_id, device_id) VALUES (?, ?)",
op.OpID, req.DeviceID,
)
if err != nil {
continue
}
rev, _ := res.LastInsertId()
_ = rev
accepted = append(accepted, op.OpID)
}
jsonOK(w, map[string]interface{}{
"accepted": accepted,
"count": len(accepted),
})
}
func (s *Server) handleSyncPull(w http.ResponseWriter, r *http.Request) {
if !s.requireAPIKey(w, r) {
return
}
jsonOK(w, map[string]string{"status": "ok", "message": "pull endpoint ready (not yet implemented)"})
if r.Method != "POST" {
jsonErr(w, 405, "POST required")
return
}
var req struct {
SinceRevision int `json:"since_revision"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonErr(w, 400, "invalid JSON")
return
}
// Get current server revision.
var serverRev int
s.db.QueryRow("SELECT COALESCE(MAX(rev), 0) FROM server_revisions").Scan(&serverRev)
// Get ops since the requested revision.
rows, err := s.db.Query(`
SELECT so.op_id, so.device_id, so.entity_type, so.entity_id, so.op_type, so.payload_json, so.created_at
FROM server_ops so
JOIN server_revisions sr ON sr.op_id = so.op_id
WHERE sr.rev > ?
ORDER BY sr.rev`, req.SinceRevision)
if err != nil {
jsonErr(w, 500, err.Error())
return
}
defer rows.Close()
type opDTO struct {
OpID string `json:"op_id"`
DeviceID string `json:"device_id"`
EntityType string `json:"entity_type"`
EntityID string `json:"entity_id"`
OpType string `json:"op_type"`
PayloadJSON string `json:"payload_json"`
CreatedAt string `json:"created_at"`
}
var ops []opDTO
for rows.Next() {
var o opDTO
if err := rows.Scan(&o.OpID, &o.DeviceID, &o.EntityType, &o.EntityID, &o.OpType, &o.PayloadJSON, &o.CreatedAt); err != nil {
continue
}
ops = append(ops, o)
}
jsonOK(w, map[string]interface{}{
"server_revision": serverRev,
"ops": ops,
})
}
func (s *Server) handleBlobs(w http.ResponseWriter, r *http.Request) {