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(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}