1package history
2
3import (
4 "context"
5 "fmt"
6 "strconv"
7 "strings"
8
9 "github.com/google/uuid"
10 "github.com/kujtimiihoxha/termai/internal/db"
11 "github.com/kujtimiihoxha/termai/internal/pubsub"
12)
13
14const (
15 InitialVersion = "initial"
16)
17
18type File struct {
19 ID string
20 SessionID string
21 Path string
22 Content string
23 Version string
24 CreatedAt int64
25 UpdatedAt int64
26}
27
28type Service interface {
29 pubsub.Suscriber[File]
30 Create(sessionID, path, content string) (File, error)
31 CreateVersion(sessionID, path, content string) (File, error)
32 Get(id string) (File, error)
33 GetByPathAndSession(path, sessionID string) (File, error)
34 ListBySession(sessionID string) ([]File, error)
35 ListLatestSessionFiles(sessionID string) ([]File, error)
36 Update(file File) (File, error)
37 Delete(id string) error
38 DeleteSessionFiles(sessionID string) error
39}
40
41type service struct {
42 *pubsub.Broker[File]
43 q db.Querier
44 ctx context.Context
45}
46
47func NewService(ctx context.Context, q db.Querier) Service {
48 return &service{
49 Broker: pubsub.NewBroker[File](),
50 q: q,
51 ctx: ctx,
52 }
53}
54
55func (s *service) Create(sessionID, path, content string) (File, error) {
56 return s.createWithVersion(sessionID, path, content, InitialVersion)
57}
58
59func (s *service) CreateVersion(sessionID, path, content string) (File, error) {
60 // Get the latest version for this path
61 files, err := s.q.ListFilesByPath(s.ctx, path)
62 if err != nil {
63 return File{}, err
64 }
65
66 if len(files) == 0 {
67 // No previous versions, create initial
68 return s.Create(sessionID, path, content)
69 }
70
71 // Get the latest version
72 latestFile := files[0] // Files are ordered by created_at DESC
73 latestVersion := latestFile.Version
74
75 // Generate the next version
76 var nextVersion string
77 if latestVersion == InitialVersion {
78 nextVersion = "v1"
79 } else if strings.HasPrefix(latestVersion, "v") {
80 versionNum, err := strconv.Atoi(latestVersion[1:])
81 if err != nil {
82 // If we can't parse the version, just use a timestamp-based version
83 nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
84 } else {
85 nextVersion = fmt.Sprintf("v%d", versionNum+1)
86 }
87 } else {
88 // If the version format is unexpected, use a timestamp-based version
89 nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
90 }
91
92 return s.createWithVersion(sessionID, path, content, nextVersion)
93}
94
95func (s *service) createWithVersion(sessionID, path, content, version string) (File, error) {
96 dbFile, err := s.q.CreateFile(s.ctx, db.CreateFileParams{
97 ID: uuid.New().String(),
98 SessionID: sessionID,
99 Path: path,
100 Content: content,
101 Version: version,
102 })
103 if err != nil {
104 return File{}, err
105 }
106 file := s.fromDBItem(dbFile)
107 s.Publish(pubsub.CreatedEvent, file)
108 return file, nil
109}
110
111func (s *service) Get(id string) (File, error) {
112 dbFile, err := s.q.GetFile(s.ctx, id)
113 if err != nil {
114 return File{}, err
115 }
116 return s.fromDBItem(dbFile), nil
117}
118
119func (s *service) GetByPathAndSession(path, sessionID string) (File, error) {
120 dbFile, err := s.q.GetFileByPathAndSession(s.ctx, db.GetFileByPathAndSessionParams{
121 Path: path,
122 SessionID: sessionID,
123 })
124 if err != nil {
125 return File{}, err
126 }
127 return s.fromDBItem(dbFile), nil
128}
129
130func (s *service) ListBySession(sessionID string) ([]File, error) {
131 dbFiles, err := s.q.ListFilesBySession(s.ctx, sessionID)
132 if err != nil {
133 return nil, err
134 }
135 files := make([]File, len(dbFiles))
136 for i, dbFile := range dbFiles {
137 files[i] = s.fromDBItem(dbFile)
138 }
139 return files, nil
140}
141
142func (s *service) ListLatestSessionFiles(sessionID string) ([]File, error) {
143 dbFiles, err := s.q.ListLatestSessionFiles(s.ctx, sessionID)
144 if err != nil {
145 return nil, err
146 }
147 files := make([]File, len(dbFiles))
148 for i, dbFile := range dbFiles {
149 files[i] = s.fromDBItem(dbFile)
150 }
151 return files, nil
152}
153
154func (s *service) Update(file File) (File, error) {
155 dbFile, err := s.q.UpdateFile(s.ctx, db.UpdateFileParams{
156 ID: file.ID,
157 Content: file.Content,
158 Version: file.Version,
159 })
160 if err != nil {
161 return File{}, err
162 }
163 updatedFile := s.fromDBItem(dbFile)
164 s.Publish(pubsub.UpdatedEvent, updatedFile)
165 return updatedFile, nil
166}
167
168func (s *service) Delete(id string) error {
169 file, err := s.Get(id)
170 if err != nil {
171 return err
172 }
173 err = s.q.DeleteFile(s.ctx, id)
174 if err != nil {
175 return err
176 }
177 s.Publish(pubsub.DeletedEvent, file)
178 return nil
179}
180
181func (s *service) DeleteSessionFiles(sessionID string) error {
182 files, err := s.ListBySession(sessionID)
183 if err != nil {
184 return err
185 }
186 for _, file := range files {
187 err = s.Delete(file.ID)
188 if err != nil {
189 return err
190 }
191 }
192 return nil
193}
194
195func (s *service) fromDBItem(item db.File) File {
196 return File{
197 ID: item.ID,
198 SessionID: item.SessionID,
199 Path: item.Path,
200 Content: item.Content,
201 Version: item.Version,
202 CreatedAt: item.CreatedAt,
203 UpdatedAt: item.UpdatedAt,
204 }
205}
206