Files
DocNest/backend/internal/store/version.go
M1ngdaXie 0ec58ca866 feat: Add landing page and version history functionality
- Implemented ConditionalHome component to show LandingPage for guests and Home for authenticated users.
- Created LandingPage with login options for Google and GitHub.
- Added VersionHistoryPanel component for managing document versions.
- Integrated version history functionality into EditorPage.
- Updated API client to handle FormData correctly.
- Added styles for LandingPage and VersionHistoryPanel.
- Created version management API methods for creating, listing, restoring, and fetching document versions.
2026-01-19 16:14:56 -08:00

195 lines
5.5 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"github.com/M1ngdaXie/realtime-collab/internal/models"
"github.com/google/uuid"
)
// CreateDocumentVersion creates a new version snapshot
func (s *PostgresStore) CreateDocumentVersion(
ctx context.Context,
documentID, userID uuid.UUID,
snapshot []byte,
textPreview *string,
label *string,
isAuto bool,
) (*models.DocumentVersion, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
// Get next version number
var versionNumber int
err = tx.QueryRowContext(ctx,
`SELECT get_next_version_number($1)`,
documentID,
).Scan(&versionNumber)
if err != nil {
return nil, fmt.Errorf("failed to get version number: %w", err)
}
// Insert version
query := `
INSERT INTO document_versions
(document_id, yjs_snapshot, text_preview, version_number, created_by, version_label, is_auto_generated)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, document_id, text_preview, version_number, created_by, version_label, is_auto_generated, created_at
`
var version models.DocumentVersion
err = tx.QueryRowContext(ctx, query,
documentID, snapshot, textPreview, versionNumber, userID, label, isAuto,
).Scan(
&version.ID, &version.DocumentID, &version.TextPreview, &version.VersionNumber,
&version.CreatedBy, &version.VersionLabel, &version.IsAutoGenerated, &version.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to create version: %w", err)
}
// Update document metadata
_, err = tx.ExecContext(ctx, `
UPDATE documents
SET version_count = version_count + 1,
last_snapshot_at = NOW()
WHERE id = $1
`, documentID)
if err != nil {
return nil, fmt.Errorf("failed to update document metadata: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
return &version, nil
}
// ListDocumentVersions returns paginated version list with author info
func (s *PostgresStore) ListDocumentVersions(
ctx context.Context,
documentID uuid.UUID,
limit int,
offset int,
) ([]models.DocumentVersionWithAuthor, int, error) {
// Get total count
var total int
err := s.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM document_versions WHERE document_id = $1`,
documentID,
).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("failed to count versions: %w", err)
}
// Get paginated versions with author (LEFT JOIN to handle deleted users)
query := `
SELECT
v.id, v.document_id, v.text_preview, v.version_number,
v.created_by, v.version_label, v.is_auto_generated, v.created_at,
u.id, u.email, u.name, u.avatar_url
FROM document_versions v
LEFT JOIN users u ON v.created_by = u.id
WHERE v.document_id = $1
ORDER BY v.version_number DESC
LIMIT $2 OFFSET $3
`
rows, err := s.db.QueryContext(ctx, query, documentID, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list versions: %w", err)
}
defer rows.Close()
var versions []models.DocumentVersionWithAuthor
for rows.Next() {
var v models.DocumentVersionWithAuthor
var authorID, authorEmail, authorName sql.NullString
var authorAvatar *string
err := rows.Scan(
&v.ID, &v.DocumentID, &v.TextPreview, &v.VersionNumber,
&v.CreatedBy, &v.VersionLabel, &v.IsAutoGenerated, &v.CreatedAt,
&authorID, &authorEmail, &authorName, &authorAvatar,
)
if err != nil {
return nil, 0, fmt.Errorf("failed to scan version: %w", err)
}
// Populate author if exists
if authorID.Valid {
authorUUID, _ := uuid.Parse(authorID.String)
v.Author = &models.User{
ID: authorUUID,
Email: authorEmail.String,
Name: authorName.String,
AvatarURL: authorAvatar,
}
}
versions = append(versions, v)
}
return versions, total, nil
}
// GetDocumentVersion retrieves a specific version with full snapshot
func (s *PostgresStore) GetDocumentVersion(ctx context.Context, versionID uuid.UUID) (*models.DocumentVersion, error) {
query := `
SELECT id, document_id, yjs_snapshot, text_preview, version_number,
created_by, version_label, is_auto_generated, created_at
FROM document_versions
WHERE id = $1
`
var version models.DocumentVersion
err := s.db.QueryRowContext(ctx, query, versionID).Scan(
&version.ID, &version.DocumentID, &version.YjsSnapshot, &version.TextPreview,
&version.VersionNumber, &version.CreatedBy, &version.VersionLabel,
&version.IsAutoGenerated, &version.CreatedAt,
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("version not found")
}
if err != nil {
return nil, fmt.Errorf("failed to get version: %w", err)
}
return &version, nil
}
// GetLatestDocumentVersion gets the most recent version for auto-snapshot comparison
func (s *PostgresStore) GetLatestDocumentVersion(ctx context.Context, documentID uuid.UUID) (*models.DocumentVersion, error) {
query := `
SELECT id, document_id, yjs_snapshot, text_preview, version_number,
created_by, version_label, is_auto_generated, created_at
FROM document_versions
WHERE document_id = $1
ORDER BY version_number DESC
LIMIT 1
`
var version models.DocumentVersion
err := s.db.QueryRowContext(ctx, query, documentID).Scan(
&version.ID, &version.DocumentID, &version.YjsSnapshot, &version.TextPreview,
&version.VersionNumber, &version.CreatedBy, &version.VersionLabel,
&version.IsAutoGenerated, &version.CreatedAt,
)
if err == sql.ErrNoRows {
return nil, nil // No versions yet
}
if err != nil {
return nil, fmt.Errorf("failed to get latest version: %w", err)
}
return &version, nil
}