fix: Add missing files

This commit is contained in:
Benjamin
2025-10-08 15:08:18 +02:00
parent d18f401797
commit 58382309bb
8 changed files with 877 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
package models
import "time"
// Document represents document metadata for tracking and integrity verification
type Document struct {
DocID string `json:"doc_id" db:"doc_id"`
Title string `json:"title" db:"title"`
URL string `json:"url" db:"url"`
Checksum string `json:"checksum" db:"checksum"`
ChecksumAlgorithm string `json:"checksum_algorithm" db:"checksum_algorithm"`
Description string `json:"description" db:"description"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
CreatedBy string `json:"created_by" db:"created_by"`
}
// DocumentInput represents the input for creating/updating document metadata
type DocumentInput struct {
Title string `json:"title"`
URL string `json:"url"`
Checksum string `json:"checksum"`
ChecksumAlgorithm string `json:"checksum_algorithm"`
Description string `json:"description"`
}
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
package database
import (
"context"
"database/sql"
"fmt"
"github.com/btouchard/ackify-ce/internal/domain/models"
"github.com/btouchard/ackify-ce/pkg/logger"
)
// DocumentRepository handles document metadata persistence
type DocumentRepository struct {
db *sql.DB
}
// NewDocumentRepository creates a new DocumentRepository
func NewDocumentRepository(db *sql.DB) *DocumentRepository {
return &DocumentRepository{db: db}
}
// Create creates a new document metadata entry
func (r *DocumentRepository) Create(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error) {
query := `
INSERT INTO documents (doc_id, title, url, checksum, checksum_algorithm, description, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING doc_id, title, url, checksum, checksum_algorithm, description, created_at, updated_at, created_by
`
doc := &models.Document{}
err := r.db.QueryRowContext(
ctx,
query,
docID,
input.Title,
input.URL,
input.Checksum,
input.ChecksumAlgorithm,
input.Description,
createdBy,
).Scan(
&doc.DocID,
&doc.Title,
&doc.URL,
&doc.Checksum,
&doc.ChecksumAlgorithm,
&doc.Description,
&doc.CreatedAt,
&doc.UpdatedAt,
&doc.CreatedBy,
)
if err != nil {
logger.Logger.Error("Failed to create document", "error", err.Error(), "doc_id", docID)
return nil, fmt.Errorf("failed to create document: %w", err)
}
return doc, nil
}
// GetByDocID retrieves document metadata by document ID
func (r *DocumentRepository) GetByDocID(ctx context.Context, docID string) (*models.Document, error) {
query := `
SELECT doc_id, title, url, checksum, checksum_algorithm, description, created_at, updated_at, created_by
FROM documents
WHERE doc_id = $1
`
doc := &models.Document{}
err := r.db.QueryRowContext(ctx, query, docID).Scan(
&doc.DocID,
&doc.Title,
&doc.URL,
&doc.Checksum,
&doc.ChecksumAlgorithm,
&doc.Description,
&doc.CreatedAt,
&doc.UpdatedAt,
&doc.CreatedBy,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
logger.Logger.Error("Failed to get document", "error", err.Error(), "doc_id", docID)
return nil, fmt.Errorf("failed to get document: %w", err)
}
return doc, nil
}
// Update updates document metadata
func (r *DocumentRepository) Update(ctx context.Context, docID string, input models.DocumentInput) (*models.Document, error) {
query := `
UPDATE documents
SET title = $2, url = $3, checksum = $4, checksum_algorithm = $5, description = $6
WHERE doc_id = $1
RETURNING doc_id, title, url, checksum, checksum_algorithm, description, created_at, updated_at, created_by
`
doc := &models.Document{}
err := r.db.QueryRowContext(
ctx,
query,
docID,
input.Title,
input.URL,
input.Checksum,
input.ChecksumAlgorithm,
input.Description,
).Scan(
&doc.DocID,
&doc.Title,
&doc.URL,
&doc.Checksum,
&doc.ChecksumAlgorithm,
&doc.Description,
&doc.CreatedAt,
&doc.UpdatedAt,
&doc.CreatedBy,
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("document not found")
}
if err != nil {
logger.Logger.Error("Failed to update document", "error", err.Error(), "doc_id", docID)
return nil, fmt.Errorf("failed to update document: %w", err)
}
return doc, nil
}
// CreateOrUpdate creates or updates document metadata
func (r *DocumentRepository) CreateOrUpdate(ctx context.Context, docID string, input models.DocumentInput, createdBy string) (*models.Document, error) {
query := `
INSERT INTO documents (doc_id, title, url, checksum, checksum_algorithm, description, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (doc_id) DO UPDATE SET
title = EXCLUDED.title,
url = EXCLUDED.url,
checksum = EXCLUDED.checksum,
checksum_algorithm = EXCLUDED.checksum_algorithm,
description = EXCLUDED.description
RETURNING doc_id, title, url, checksum, checksum_algorithm, description, created_at, updated_at, created_by
`
doc := &models.Document{}
err := r.db.QueryRowContext(
ctx,
query,
docID,
input.Title,
input.URL,
input.Checksum,
input.ChecksumAlgorithm,
input.Description,
createdBy,
).Scan(
&doc.DocID,
&doc.Title,
&doc.URL,
&doc.Checksum,
&doc.ChecksumAlgorithm,
&doc.Description,
&doc.CreatedAt,
&doc.UpdatedAt,
&doc.CreatedBy,
)
if err != nil {
logger.Logger.Error("Failed to create or update document", "error", err.Error(), "doc_id", docID)
return nil, fmt.Errorf("failed to create or update document: %w", err)
}
return doc, nil
}
// Delete deletes document metadata
func (r *DocumentRepository) Delete(ctx context.Context, docID string) error {
query := `DELETE FROM documents WHERE doc_id = $1`
result, err := r.db.ExecContext(ctx, query, docID)
if err != nil {
logger.Logger.Error("Failed to delete document", "error", err.Error(), "doc_id", docID)
return fmt.Errorf("failed to delete document: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("document not found")
}
return nil
}
// List retrieves all documents with pagination
func (r *DocumentRepository) List(ctx context.Context, limit, offset int) ([]*models.Document, error) {
query := `
SELECT doc_id, title, url, checksum, checksum_algorithm, description, created_at, updated_at, created_by
FROM documents
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
rows, err := r.db.QueryContext(ctx, query, limit, offset)
if err != nil {
logger.Logger.Error("Failed to list documents", "error", err.Error())
return nil, fmt.Errorf("failed to list documents: %w", err)
}
defer rows.Close()
documents := []*models.Document{}
for rows.Next() {
doc := &models.Document{}
err := rows.Scan(
&doc.DocID,
&doc.Title,
&doc.URL,
&doc.Checksum,
&doc.ChecksumAlgorithm,
&doc.Description,
&doc.CreatedAt,
&doc.UpdatedAt,
&doc.CreatedBy,
)
if err != nil {
logger.Logger.Error("Failed to scan document row", "error", err.Error())
return nil, fmt.Errorf("failed to scan document: %w", err)
}
documents = append(documents, doc)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating documents: %w", err)
}
return documents, nil
}
@@ -0,0 +1,388 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
//go:build integration
package database
import (
"context"
"testing"
"github.com/btouchard/ackify-ce/internal/domain/models"
)
func setupDocumentsTable(t *testing.T, testDB *TestDB) {
t.Helper()
schema := `
DROP TABLE IF EXISTS documents;
CREATE TABLE documents (
doc_id TEXT PRIMARY KEY,
title TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT '',
checksum TEXT NOT NULL DEFAULT '',
checksum_algorithm TEXT NOT NULL DEFAULT 'SHA-256' CHECK (checksum_algorithm IN ('SHA-256', 'SHA-512', 'MD5')),
description TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by TEXT NOT NULL DEFAULT ''
);
CREATE INDEX idx_documents_created_at ON documents(created_at DESC);
CREATE OR REPLACE FUNCTION update_documents_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_documents_updated_at
BEFORE UPDATE ON documents
FOR EACH ROW
EXECUTE FUNCTION update_documents_updated_at();
`
_, err := testDB.DB.Exec(schema)
if err != nil {
t.Fatalf("failed to setup documents table: %v", err)
}
}
func clearDocumentsTable(t *testing.T, testDB *TestDB) {
t.Helper()
_, err := testDB.DB.Exec("TRUNCATE TABLE documents RESTART IDENTITY CASCADE")
if err != nil {
t.Fatalf("failed to clear documents table: %v", err)
}
}
func TestDocumentRepository_Create(t *testing.T) {
testDB := SetupTestDB(t)
setupDocumentsTable(t, testDB)
ctx := context.Background()
repo := NewDocumentRepository(testDB.DB)
input := models.DocumentInput{
Title: "Test Document",
URL: "https://example.com/doc.pdf",
Checksum: "abc123def456",
ChecksumAlgorithm: "SHA-256",
Description: "A test document for integration testing",
}
doc, err := repo.Create(ctx, "test-doc-001", input, "admin@example.com")
if err != nil {
t.Fatalf("Failed to create document: %v", err)
}
if doc.DocID != "test-doc-001" {
t.Errorf("Expected DocID test-doc-001, got %s", doc.DocID)
}
if doc.Title != input.Title {
t.Errorf("Expected Title %s, got %s", input.Title, doc.Title)
}
if doc.URL != input.URL {
t.Errorf("Expected URL %s, got %s", input.URL, doc.URL)
}
if doc.Checksum != input.Checksum {
t.Errorf("Expected Checksum %s, got %s", input.Checksum, doc.Checksum)
}
if doc.ChecksumAlgorithm != input.ChecksumAlgorithm {
t.Errorf("Expected ChecksumAlgorithm %s, got %s", input.ChecksumAlgorithm, doc.ChecksumAlgorithm)
}
if doc.Description != input.Description {
t.Errorf("Expected Description %s, got %s", input.Description, doc.Description)
}
if doc.CreatedBy != "admin@example.com" {
t.Errorf("Expected CreatedBy admin@example.com, got %s", doc.CreatedBy)
}
if doc.CreatedAt.IsZero() {
t.Error("CreatedAt should not be zero")
}
if doc.UpdatedAt.IsZero() {
t.Error("UpdatedAt should not be zero")
}
}
func TestDocumentRepository_GetByDocID(t *testing.T) {
testDB := SetupTestDB(t)
setupDocumentsTable(t, testDB)
ctx := context.Background()
repo := NewDocumentRepository(testDB.DB)
input := models.DocumentInput{
Title: "Get Test Document",
URL: "https://example.com/get-doc.pdf",
Checksum: "get123abc456",
ChecksumAlgorithm: "SHA-512",
Description: "Document for get testing",
}
created, err := repo.Create(ctx, "get-doc-001", input, "user@example.com")
if err != nil {
t.Fatalf("Failed to create document: %v", err)
}
retrieved, err := repo.GetByDocID(ctx, "get-doc-001")
if err != nil {
t.Fatalf("Failed to get document: %v", err)
}
if retrieved == nil {
t.Fatal("Retrieved document is nil")
}
if retrieved.DocID != created.DocID {
t.Errorf("Expected DocID %s, got %s", created.DocID, retrieved.DocID)
}
if retrieved.Title != created.Title {
t.Errorf("Expected Title %s, got %s", created.Title, retrieved.Title)
}
// Test non-existent document
nonExistent, err := repo.GetByDocID(ctx, "non-existent-doc")
if err != nil {
t.Errorf("Expected no error for non-existent document, got %v", err)
}
if nonExistent != nil {
t.Error("Expected nil for non-existent document")
}
}
func TestDocumentRepository_Update(t *testing.T) {
testDB := SetupTestDB(t)
setupDocumentsTable(t, testDB)
ctx := context.Background()
repo := NewDocumentRepository(testDB.DB)
input := models.DocumentInput{
Title: "Original Title",
URL: "https://example.com/original.pdf",
Checksum: "original123",
ChecksumAlgorithm: "MD5",
Description: "Original description",
}
created, err := repo.Create(ctx, "update-doc-001", input, "creator@example.com")
if err != nil {
t.Fatalf("Failed to create document: %v", err)
}
updateInput := models.DocumentInput{
Title: "Updated Title",
URL: "https://example.com/updated.pdf",
Checksum: "updated456",
ChecksumAlgorithm: "SHA-256",
Description: "Updated description",
}
updated, err := repo.Update(ctx, "update-doc-001", updateInput)
if err != nil {
t.Fatalf("Failed to update document: %v", err)
}
if updated.Title != updateInput.Title {
t.Errorf("Expected Title %s, got %s", updateInput.Title, updated.Title)
}
if updated.URL != updateInput.URL {
t.Errorf("Expected URL %s, got %s", updateInput.URL, updated.URL)
}
if updated.Checksum != updateInput.Checksum {
t.Errorf("Expected Checksum %s, got %s", updateInput.Checksum, updated.Checksum)
}
if updated.ChecksumAlgorithm != updateInput.ChecksumAlgorithm {
t.Errorf("Expected ChecksumAlgorithm %s, got %s", updateInput.ChecksumAlgorithm, updated.ChecksumAlgorithm)
}
if updated.Description != updateInput.Description {
t.Errorf("Expected Description %s, got %s", updateInput.Description, updated.Description)
}
// CreatedBy should remain unchanged
if updated.CreatedBy != created.CreatedBy {
t.Errorf("Expected CreatedBy to remain %s, got %s", created.CreatedBy, updated.CreatedBy)
}
// UpdatedAt should be later than CreatedAt
if !updated.UpdatedAt.After(created.CreatedAt) && !updated.UpdatedAt.Equal(created.CreatedAt) {
t.Error("UpdatedAt should be after or equal to CreatedAt")
}
}
func TestDocumentRepository_CreateOrUpdate(t *testing.T) {
testDB := SetupTestDB(t)
setupDocumentsTable(t, testDB)
ctx := context.Background()
repo := NewDocumentRepository(testDB.DB)
input := models.DocumentInput{
Title: "CreateOrUpdate Test",
URL: "https://example.com/test.pdf",
Checksum: "test123",
ChecksumAlgorithm: "SHA-256",
Description: "Test description",
}
// First call should create
doc1, err := repo.CreateOrUpdate(ctx, "upsert-doc-001", input, "creator@example.com")
if err != nil {
t.Fatalf("Failed to create document: %v", err)
}
if doc1.Title != input.Title {
t.Errorf("Expected Title %s, got %s", input.Title, doc1.Title)
}
// Second call with same doc_id should update
updateInput := models.DocumentInput{
Title: "Updated via Upsert",
URL: "https://example.com/updated.pdf",
Checksum: "updated789",
ChecksumAlgorithm: "SHA-512",
Description: "Updated description",
}
doc2, err := repo.CreateOrUpdate(ctx, "upsert-doc-001", updateInput, "updater@example.com")
if err != nil {
t.Fatalf("Failed to update document: %v", err)
}
if doc2.Title != updateInput.Title {
t.Errorf("Expected Title %s, got %s", updateInput.Title, doc2.Title)
}
if doc2.URL != updateInput.URL {
t.Errorf("Expected URL %s, got %s", updateInput.URL, doc2.URL)
}
// Verify only one record exists
retrieved, err := repo.GetByDocID(ctx, "upsert-doc-001")
if err != nil {
t.Fatalf("Failed to get document: %v", err)
}
if retrieved.Title != updateInput.Title {
t.Errorf("Expected final Title %s, got %s", updateInput.Title, retrieved.Title)
}
}
func TestDocumentRepository_Delete(t *testing.T) {
testDB := SetupTestDB(t)
setupDocumentsTable(t, testDB)
ctx := context.Background()
repo := NewDocumentRepository(testDB.DB)
input := models.DocumentInput{
Title: "Delete Test",
URL: "https://example.com/delete.pdf",
Checksum: "delete123",
ChecksumAlgorithm: "SHA-256",
Description: "Document to be deleted",
}
_, err := repo.Create(ctx, "delete-doc-001", input, "admin@example.com")
if err != nil {
t.Fatalf("Failed to create document: %v", err)
}
err = repo.Delete(ctx, "delete-doc-001")
if err != nil {
t.Fatalf("Failed to delete document: %v", err)
}
// Verify deletion
retrieved, err := repo.GetByDocID(ctx, "delete-doc-001")
if err != nil {
t.Errorf("Expected no error when getting deleted document, got %v", err)
}
if retrieved != nil {
t.Error("Expected nil after deletion")
}
// Test deleting non-existent document
err = repo.Delete(ctx, "non-existent-doc")
if err == nil {
t.Error("Expected error when deleting non-existent document")
}
}
func TestDocumentRepository_List(t *testing.T) {
testDB := SetupTestDB(t)
setupDocumentsTable(t, testDB)
ctx := context.Background()
repo := NewDocumentRepository(testDB.DB)
// Create multiple documents
for i := 1; i <= 5; i++ {
input := models.DocumentInput{
Title: "Document " + string(rune('A'+i-1)),
URL: "https://example.com/doc" + string(rune('0'+i)) + ".pdf",
Checksum: "checksum" + string(rune('0'+i)),
ChecksumAlgorithm: "SHA-256",
Description: "Test document " + string(rune('0'+i)),
}
_, err := repo.Create(ctx, "list-doc-00"+string(rune('0'+i)), input, "admin@example.com")
if err != nil {
t.Fatalf("Failed to create document %d: %v", i, err)
}
}
// Test listing all
docs, err := repo.List(ctx, 10, 0)
if err != nil {
t.Fatalf("Failed to list documents: %v", err)
}
if len(docs) != 5 {
t.Errorf("Expected 5 documents, got %d", len(docs))
}
// Test pagination
page1, err := repo.List(ctx, 2, 0)
if err != nil {
t.Fatalf("Failed to get page 1: %v", err)
}
if len(page1) != 2 {
t.Errorf("Expected 2 documents in page 1, got %d", len(page1))
}
page2, err := repo.List(ctx, 2, 2)
if err != nil {
t.Fatalf("Failed to get page 2: %v", err)
}
if len(page2) != 2 {
t.Errorf("Expected 2 documents in page 2, got %d", len(page2))
}
// Verify ordering (newest first)
if len(docs) >= 2 {
if docs[0].CreatedAt.Before(docs[1].CreatedAt) {
t.Error("Documents should be ordered by created_at DESC")
}
}
}
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
package admin
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/btouchard/ackify-ce/internal/domain/models"
"github.com/btouchard/ackify-ce/internal/infrastructure/database"
"github.com/btouchard/ackify-ce/pkg/logger"
)
type DocumentHandlers struct {
documentRepo *database.DocumentRepository
userService userService
}
func NewDocumentHandlers(
documentRepo *database.DocumentRepository,
userService userService,
) *DocumentHandlers {
return &DocumentHandlers{
documentRepo: documentRepo,
userService: userService,
}
}
// HandleGetDocumentMetadata retrieves document metadata as JSON
func (h *DocumentHandlers) HandleGetDocumentMetadata(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
docID := chi.URLParam(r, "docID")
if docID == "" {
http.Error(w, "Document ID required", http.StatusBadRequest)
return
}
doc, err := h.documentRepo.GetByDocID(ctx, docID)
if err != nil {
logger.Logger.Error("Failed to get document metadata", "error", err.Error(), "doc_id", docID)
http.Error(w, "Failed to get document metadata", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(doc); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}
// HandleUpdateDocumentMetadata creates or updates document metadata
func (h *DocumentHandlers) HandleUpdateDocumentMetadata(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
docID := chi.URLParam(r, "docID")
if docID == "" {
http.Error(w, "Document ID required", http.StatusBadRequest)
return
}
user, err := h.userService.GetUser(r)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Failed to parse form", http.StatusBadRequest)
return
}
input := models.DocumentInput{
Title: r.FormValue("title"),
URL: r.FormValue("url"),
Checksum: r.FormValue("checksum"),
ChecksumAlgorithm: r.FormValue("checksum_algorithm"),
Description: r.FormValue("description"),
}
// Validate checksum algorithm
validAlgorithms := map[string]bool{
"SHA-256": true,
"SHA-512": true,
"MD5": true,
}
if input.ChecksumAlgorithm != "" && !validAlgorithms[input.ChecksumAlgorithm] {
http.Error(w, "Invalid checksum algorithm. Must be SHA-256, SHA-512, or MD5", http.StatusBadRequest)
return
}
// Default to SHA-256 if not specified
if input.ChecksumAlgorithm == "" {
input.ChecksumAlgorithm = "SHA-256"
}
doc, err := h.documentRepo.CreateOrUpdate(ctx, docID, input, user.Email)
if err != nil {
logger.Logger.Error("Failed to update document metadata", "error", err.Error(), "doc_id", docID)
http.Error(w, "Failed to update document metadata", http.StatusInternalServerError)
return
}
logger.Logger.Info("Document metadata updated", "doc_id", docID, "updated_by", user.Email)
// Return JSON response for AJAX requests
if r.Header.Get("Accept") == "application/json" {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(doc); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
return
}
// Redirect back to document page for form submissions
http.Redirect(w, r, "/admin/docs/"+docID, http.StatusSeeOther)
}
// HandleDeleteDocumentMetadata deletes document metadata
func (h *DocumentHandlers) HandleDeleteDocumentMetadata(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
docID := chi.URLParam(r, "docID")
if docID == "" {
http.Error(w, "Document ID required", http.StatusBadRequest)
return
}
user, err := h.userService.GetUser(r)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
err = h.documentRepo.Delete(ctx, docID)
if err != nil {
logger.Logger.Error("Failed to delete document metadata", "error", err.Error(), "doc_id", docID)
http.Error(w, "Failed to delete document metadata", http.StatusInternalServerError)
return
}
logger.Logger.Info("Document metadata deleted", "doc_id", docID, "deleted_by", user.Email)
// Return success for AJAX requests
if r.Header.Get("Accept") == "application/json" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return
}
// Redirect back to document page for form submissions
http.Redirect(w, r, "/admin/docs/"+docID, http.StatusSeeOther)
}
@@ -0,0 +1,2 @@
-- Remove name field from expected_signers table
ALTER TABLE expected_signers DROP COLUMN name;
@@ -0,0 +1,6 @@
-- Add name field to expected_signers table
-- This allows storing optional display names for expected readers
-- Supports formats like "Benjamin Touchard <benjamin@kolapsis.com>"
ALTER TABLE expected_signers ADD COLUMN name TEXT NOT NULL DEFAULT '';
COMMENT ON COLUMN expected_signers.name IS 'Optional display name for personalized reminder emails';
@@ -0,0 +1,8 @@
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Drop trigger and function
DROP TRIGGER IF EXISTS trigger_update_documents_updated_at ON documents;
DROP FUNCTION IF EXISTS update_documents_updated_at();
-- Drop table
DROP TABLE IF EXISTS documents;
@@ -0,0 +1,42 @@
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Create documents table for document metadata
CREATE TABLE documents (
doc_id TEXT PRIMARY KEY,
title TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT '',
checksum TEXT NOT NULL DEFAULT '',
checksum_algorithm TEXT NOT NULL DEFAULT 'SHA-256' CHECK (checksum_algorithm IN ('SHA-256', 'SHA-512', 'MD5')),
description TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by TEXT NOT NULL DEFAULT ''
);
COMMENT ON TABLE documents IS 'Stores document metadata including URL, checksum, and description';
COMMENT ON COLUMN documents.doc_id IS 'Document identifier (references signatures.doc_id)';
COMMENT ON COLUMN documents.title IS 'Optional document title';
COMMENT ON COLUMN documents.url IS 'URL or path to the document';
COMMENT ON COLUMN documents.checksum IS 'Checksum/hash of the document for integrity verification';
COMMENT ON COLUMN documents.checksum_algorithm IS 'Algorithm used for checksum (SHA-256, SHA-512, or MD5)';
COMMENT ON COLUMN documents.description IS 'Optional document description';
COMMENT ON COLUMN documents.created_at IS 'Timestamp when document metadata was created';
COMMENT ON COLUMN documents.updated_at IS 'Timestamp when document metadata was last updated';
COMMENT ON COLUMN documents.created_by IS 'Email of user who created the document metadata';
-- Create index on created_at for sorting
CREATE INDEX idx_documents_created_at ON documents(created_at DESC);
-- Create trigger to automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_documents_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_documents_updated_at
BEFORE UPDATE ON documents
FOR EACH ROW
EXECUTE FUNCTION update_documents_updated_at();