- 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.
195 lines
5.5 KiB
Go
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
|
|
}
|