file.go

  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