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