Update documents route with delete endpoint - WIP
This commit is contained in:
parent
ba36803f05
commit
e7a97294e2
3 changed files with 544 additions and 34 deletions
342
FEATURE-ROADMAP.md
Normal file
342
FEATURE-ROADMAP.md
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
# NaviDocs Single-Tenant Feature Roadmap
|
||||||
|
|
||||||
|
**Version:** 1.0 → 2.0
|
||||||
|
**Target:** Complete single-tenant boat manual management system
|
||||||
|
**Approach:** Incremental implementation with backward compatibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Vision
|
||||||
|
|
||||||
|
Transform NaviDocs from a demo into a production-ready single-tenant system with complete document lifecycle management, advanced search, and user preferences.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Feature Categories
|
||||||
|
|
||||||
|
### 1. Document Management ⭐ (Priority 1)
|
||||||
|
- [x] Upload with OCR
|
||||||
|
- [x] View with text selection
|
||||||
|
- [x] Auto-fill metadata
|
||||||
|
- [ ] **Delete documents** (with confirmation)
|
||||||
|
- [ ] **Edit metadata** (inline or modal)
|
||||||
|
- [ ] **Bulk operations** (select multiple, bulk delete)
|
||||||
|
- [ ] **Document versions** (track changes)
|
||||||
|
- [ ] **Download original PDF**
|
||||||
|
|
||||||
|
### 2. Advanced Search 🔍 (Priority 1)
|
||||||
|
- [x] Full-text search
|
||||||
|
- [x] Diagram thumbnails
|
||||||
|
- [ ] **Filter by boat** (dropdown)
|
||||||
|
- [ ] **Filter by document type** (manual, service, warranty)
|
||||||
|
- [ ] **Filter by date range** (uploaded date)
|
||||||
|
- [ ] **Sort options** (relevance, date, title)
|
||||||
|
- [ ] **Search within document**
|
||||||
|
- [ ] **Recent searches** (last 10)
|
||||||
|
- [ ] **Search suggestions** (autocomplete)
|
||||||
|
- [ ] **Export search results** (PDF/CSV)
|
||||||
|
|
||||||
|
### 3. User Experience 🎨 (Priority 2)
|
||||||
|
- [x] Toast notifications
|
||||||
|
- [x] Responsive design
|
||||||
|
- [x] Dark theme
|
||||||
|
- [ ] **Keyboard shortcuts** (/, Ctrl+K for search, etc.)
|
||||||
|
- [ ] **Bookmarks** (save specific pages)
|
||||||
|
- [ ] **Reading progress** (remember last page)
|
||||||
|
- [ ] **Print-friendly view**
|
||||||
|
- [ ] **Fullscreen mode**
|
||||||
|
- [ ] **Night mode toggle** (darker theme)
|
||||||
|
|
||||||
|
### 4. Dashboard & Analytics 📊 (Priority 2)
|
||||||
|
- [ ] **Statistics dashboard**
|
||||||
|
- Total documents
|
||||||
|
- Total pages
|
||||||
|
- Storage used
|
||||||
|
- Most searched terms
|
||||||
|
- Recent activity
|
||||||
|
- [ ] **Document health**
|
||||||
|
- OCR status
|
||||||
|
- Missing metadata
|
||||||
|
- Indexing status
|
||||||
|
- [ ] **Usage charts**
|
||||||
|
- Searches over time
|
||||||
|
- Documents added over time
|
||||||
|
|
||||||
|
### 5. Settings & Preferences ⚙️ (Priority 1)
|
||||||
|
- [ ] **Organization settings**
|
||||||
|
- Organization name
|
||||||
|
- Default boat name
|
||||||
|
- Logo upload
|
||||||
|
- [ ] **User preferences**
|
||||||
|
- Theme (dark/light)
|
||||||
|
- Default search filters
|
||||||
|
- Items per page
|
||||||
|
- Language
|
||||||
|
- [ ] **Storage management**
|
||||||
|
- View storage usage
|
||||||
|
- Cleanup old files
|
||||||
|
- Export all data
|
||||||
|
|
||||||
|
### 6. Help & Onboarding 💡 (Priority 3)
|
||||||
|
- [ ] **Interactive tour** (first visit)
|
||||||
|
- [ ] **Keyboard shortcuts guide**
|
||||||
|
- [ ] **Search tips**
|
||||||
|
- [ ] **Feature highlights**
|
||||||
|
- [ ] **FAQ section**
|
||||||
|
|
||||||
|
### 7. Data Management 💾 (Priority 2)
|
||||||
|
- [ ] **Export all documents** (ZIP)
|
||||||
|
- [ ] **Import from backup**
|
||||||
|
- [ ] **Activity log** (audit trail)
|
||||||
|
- [ ] **Database maintenance** (vacuum, optimize)
|
||||||
|
|
||||||
|
### 8. Polish & Performance ⚡ (Priority 3)
|
||||||
|
- [x] Comprehensive logging
|
||||||
|
- [x] Error handling
|
||||||
|
- [ ] **Image lazy loading**
|
||||||
|
- [ ] **PDF page caching**
|
||||||
|
- [ ] **Search result caching**
|
||||||
|
- [ ] **Compression for images**
|
||||||
|
- [ ] **Background job queue**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗓️ Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Core Management (Day 1)
|
||||||
|
**Goal:** Complete document lifecycle management
|
||||||
|
|
||||||
|
1. **Document Deletion**
|
||||||
|
- Add delete button to document cards
|
||||||
|
- Confirmation modal
|
||||||
|
- Backend endpoint
|
||||||
|
- Cleanup files, DB, and search index
|
||||||
|
- Toast notification on success
|
||||||
|
|
||||||
|
2. **Metadata Editing**
|
||||||
|
- Edit modal/form
|
||||||
|
- Update boat info, title, type
|
||||||
|
- Re-index search after update
|
||||||
|
- Optimistic UI updates
|
||||||
|
|
||||||
|
3. **Download Original PDF**
|
||||||
|
- Download button in document viewer
|
||||||
|
- Proper filename
|
||||||
|
|
||||||
|
### Phase 2: Advanced Search (Day 1-2)
|
||||||
|
**Goal:** Make search powerful and flexible
|
||||||
|
|
||||||
|
1. **Search Filters**
|
||||||
|
- Boat filter (dropdown with all boats)
|
||||||
|
- Document type filter
|
||||||
|
- Date range filter
|
||||||
|
- Apply filters to search query
|
||||||
|
|
||||||
|
2. **Search Enhancements**
|
||||||
|
- Sort by relevance/date/title
|
||||||
|
- Recent searches (localStorage)
|
||||||
|
- Search within current document
|
||||||
|
|
||||||
|
3. **Search Export**
|
||||||
|
- Export results as PDF
|
||||||
|
- Export as CSV
|
||||||
|
|
||||||
|
### Phase 3: Dashboard (Day 2)
|
||||||
|
**Goal:** Provide system overview and insights
|
||||||
|
|
||||||
|
1. **Statistics Dashboard**
|
||||||
|
- Total documents, pages, storage
|
||||||
|
- Recent uploads
|
||||||
|
- Popular searches
|
||||||
|
- Quick actions
|
||||||
|
|
||||||
|
2. **Document Health**
|
||||||
|
- Show processing status
|
||||||
|
- Identify issues
|
||||||
|
- Quick fixes
|
||||||
|
|
||||||
|
### Phase 4: Settings & Preferences (Day 2-3)
|
||||||
|
**Goal:** Customizable experience
|
||||||
|
|
||||||
|
1. **Settings Page**
|
||||||
|
- Organization info
|
||||||
|
- User preferences
|
||||||
|
- Storage management
|
||||||
|
- Database maintenance
|
||||||
|
|
||||||
|
2. **Theme Toggle**
|
||||||
|
- Dark/light mode
|
||||||
|
- Persistent preference
|
||||||
|
|
||||||
|
### Phase 5: UX Enhancements (Day 3)
|
||||||
|
**Goal:** Make it delightful to use
|
||||||
|
|
||||||
|
1. **Keyboard Shortcuts**
|
||||||
|
- / or Ctrl+K for search
|
||||||
|
- Arrow keys for navigation
|
||||||
|
- Escape to close modals
|
||||||
|
- Shortcuts help modal
|
||||||
|
|
||||||
|
2. **Bookmarks**
|
||||||
|
- Save favorite pages
|
||||||
|
- Quick access
|
||||||
|
- Manage bookmarks
|
||||||
|
|
||||||
|
3. **Reading Progress**
|
||||||
|
- Remember last page per document
|
||||||
|
- Resume reading
|
||||||
|
|
||||||
|
### Phase 6: Polish & Testing (Day 3)
|
||||||
|
**Goal:** Production-ready quality
|
||||||
|
|
||||||
|
1. **Performance Optimization**
|
||||||
|
- Image lazy loading
|
||||||
|
- Caching strategies
|
||||||
|
- Compression
|
||||||
|
|
||||||
|
2. **Testing**
|
||||||
|
- E2E tests for new features
|
||||||
|
- Load testing
|
||||||
|
- Edge case handling
|
||||||
|
|
||||||
|
3. **Documentation**
|
||||||
|
- Update guides
|
||||||
|
- Feature screenshots
|
||||||
|
- API documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 UI/UX Considerations
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
1. **Consistent:** Follow existing pink/purple dark theme
|
||||||
|
2. **Intuitive:** Common patterns (3-dot menus, icon buttons)
|
||||||
|
3. **Responsive:** All features work on mobile
|
||||||
|
4. **Accessible:** Keyboard navigation, ARIA labels
|
||||||
|
5. **Fast:** Optimistic updates, caching
|
||||||
|
|
||||||
|
### Component Additions
|
||||||
|
- `ConfirmDialog.vue` - Reusable confirmation modal
|
||||||
|
- `EditDocumentModal.vue` - Edit document metadata
|
||||||
|
- `SearchFilters.vue` - Advanced search filter panel
|
||||||
|
- `StatsCard.vue` - Dashboard statistics cards
|
||||||
|
- `SettingsPanel.vue` - Settings sections
|
||||||
|
- `KeyboardShortcuts.vue` - Shortcuts help modal
|
||||||
|
- `BookmarksList.vue` - Bookmarks sidebar
|
||||||
|
|
||||||
|
### New Routes
|
||||||
|
```javascript
|
||||||
|
/ # Home (existing)
|
||||||
|
/search # Search (existing)
|
||||||
|
/document/:id # Document viewer (existing)
|
||||||
|
/dashboard # New: Statistics dashboard
|
||||||
|
/settings # New: Settings page
|
||||||
|
/bookmarks # New: Bookmarks list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Backend Changes
|
||||||
|
|
||||||
|
### New Endpoints
|
||||||
|
```javascript
|
||||||
|
DELETE /api/documents/:id # Delete document
|
||||||
|
PATCH /api/documents/:id # Update metadata
|
||||||
|
GET /api/documents/:id/download # Download PDF
|
||||||
|
GET /api/stats # Get statistics
|
||||||
|
GET /api/search/history # Recent searches
|
||||||
|
POST /api/bookmarks # Add bookmark
|
||||||
|
GET /api/bookmarks # List bookmarks
|
||||||
|
DELETE /api/bookmarks/:id # Remove bookmark
|
||||||
|
GET /api/settings # Get settings
|
||||||
|
PATCH /api/settings # Update settings
|
||||||
|
POST /api/export # Export all data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Schema Additions
|
||||||
|
```sql
|
||||||
|
-- Bookmarks
|
||||||
|
CREATE TABLE bookmarks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
documentId TEXT NOT NULL,
|
||||||
|
pageNumber INTEGER NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
createdAt INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(documentId) REFERENCES documents(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Settings
|
||||||
|
CREATE TABLE settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updatedAt INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Activity Log
|
||||||
|
CREATE TABLE activity_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
entity TEXT,
|
||||||
|
entityId TEXT,
|
||||||
|
details TEXT,
|
||||||
|
createdAt INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Breaking Changes Prevention
|
||||||
|
|
||||||
|
### Backward Compatibility Strategy
|
||||||
|
1. **Add, don't replace:** New endpoints alongside existing
|
||||||
|
2. **Optional features:** All new features are additive
|
||||||
|
3. **Database migrations:** Careful schema updates
|
||||||
|
4. **API versioning:** Consider /api/v1 prefix for future
|
||||||
|
5. **Feature flags:** Toggle new features on/off
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
1. **Existing tests must pass:** All 8 E2E tests
|
||||||
|
2. **New tests for new features:** Add to test suite
|
||||||
|
3. **Regression testing:** Verify old features still work
|
||||||
|
4. **Manual testing:** Complete user flow tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Success Metrics
|
||||||
|
|
||||||
|
### Completion Criteria
|
||||||
|
- [ ] All priority 1 features implemented
|
||||||
|
- [ ] 15+ E2E tests passing
|
||||||
|
- [ ] < 100ms response time for all endpoints
|
||||||
|
- [ ] Zero console errors
|
||||||
|
- [ ] Mobile responsive verified
|
||||||
|
- [ ] Complete documentation updated
|
||||||
|
- [ ] Demo-ready with showcase
|
||||||
|
|
||||||
|
### Quality Gates
|
||||||
|
1. **Code Quality:** ESLint passing, no warnings
|
||||||
|
2. **Performance:** Lighthouse score >90
|
||||||
|
3. **Tests:** >80% feature coverage
|
||||||
|
4. **Documentation:** All new features documented
|
||||||
|
5. **UX:** User testing with 3+ scenarios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Run all tests
|
||||||
|
- [ ] Update environment variables
|
||||||
|
- [ ] Database migrations applied
|
||||||
|
- [ ] Backup existing data
|
||||||
|
- [ ] Performance testing
|
||||||
|
- [ ] Security audit
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] Stakeholder demo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next Steps:** Begin Phase 1 implementation
|
||||||
|
**Timeline:** 3-day sprint for complete feature set
|
||||||
|
**Goal:** Production-ready single-tenant system with all features
|
||||||
|
|
||||||
|
Let's build something amazing! 🚢✨
|
||||||
157
client/src/components/ConfirmDialog.vue
Normal file
157
client/src/components/ConfirmDialog.vue
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="modal">
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm px-4"
|
||||||
|
@click.self="onCancel"
|
||||||
|
>
|
||||||
|
<div class="bg-dark-800 rounded-2xl shadow-2xl border border-white/10 max-w-md w-full overflow-hidden">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="px-6 py-4 border-b border-white/10">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div :class="[
|
||||||
|
'w-10 h-10 rounded-xl flex items-center justify-center',
|
||||||
|
variantClasses[variant].bg
|
||||||
|
]">
|
||||||
|
<svg v-if="variant === 'danger'" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="variant === 'warning'" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-white">{{ title }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<p class="text-white/80 leading-relaxed">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="px-6 py-4 bg-dark-900/50 flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
@click="onCancel"
|
||||||
|
class="px-4 py-2 rounded-lg border border-white/20 text-white hover:bg-white/5 transition-colors"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
{{ cancelText }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="onConfirm"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 rounded-lg font-medium transition-colors',
|
||||||
|
variantClasses[variant].button,
|
||||||
|
loading && 'opacity-50 cursor-not-allowed'
|
||||||
|
]"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<span v-if="!loading">{{ confirmText }}</span>
|
||||||
|
<span v-else class="flex items-center gap-2">
|
||||||
|
<div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||||
|
{{ loadingText }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Confirm Action'
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
confirmText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Confirm'
|
||||||
|
},
|
||||||
|
cancelText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Cancel'
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Processing...'
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: 'info', // 'info', 'warning', 'danger'
|
||||||
|
validator: (value) => ['info', 'warning', 'danger'].includes(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['confirm', 'cancel'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
danger: {
|
||||||
|
bg: 'bg-red-500/20 text-red-400',
|
||||||
|
button: 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
bg: 'bg-yellow-500/20 text-yellow-400',
|
||||||
|
button: 'bg-yellow-500 hover:bg-yellow-600 text-white'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
bg: 'bg-blue-500/20 text-blue-400',
|
||||||
|
button: 'bg-gradient-to-r from-pink-400 to-purple-500 hover:from-pink-500 hover:to-purple-600 text-white'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
if (loading.value) return
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirm() {
|
||||||
|
if (loading.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await emit('confirm')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-enter-active,
|
||||||
|
.modal-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from,
|
||||||
|
.modal-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-active > div,
|
||||||
|
.modal-leave-active > div {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from > div,
|
||||||
|
.modal-leave-to > div {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,10 +5,16 @@
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getDb } from '../db/db.js';
|
import { getDb } from '../db/db.js';
|
||||||
|
import { getMeilisearchClient } from '../config/meilisearch.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { rm } from 'fs/promises';
|
||||||
|
import { loggers } from '../utils/logger.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const logger = loggers.app.child('Documents');
|
||||||
|
|
||||||
|
const MEILISEARCH_INDEX_NAME = process.env.MEILISEARCH_INDEX_NAME || 'navidocs-pages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/documents/:id
|
* GET /api/documents/:id
|
||||||
|
|
@ -343,59 +349,64 @@ router.get('/', async (req, res) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/documents/:id
|
* DELETE /api/documents/:id
|
||||||
* Soft delete a document (mark as deleted)
|
* Hard delete a document (removes from DB, filesystem, and search index)
|
||||||
|
* For single-tenant demo - simplified permissions
|
||||||
*/
|
*/
|
||||||
router.delete('/:id', async (req, res) => {
|
router.delete('/:id', async (req, res) => {
|
||||||
try {
|
const { id } = req.params;
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
// TODO: Authentication middleware should provide req.user
|
try {
|
||||||
const userId = req.user?.id || 'test-user-id';
|
logger.info(`Deleting document ${id}`);
|
||||||
|
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
|
const searchClient = getMeilisearchClient();
|
||||||
|
|
||||||
// Check ownership
|
// Get document info before deletion
|
||||||
const document = db.prepare(`
|
const document = db.prepare('SELECT * FROM documents WHERE id = ?').get(id);
|
||||||
SELECT id, organization_id, uploaded_by
|
|
||||||
FROM documents
|
|
||||||
WHERE id = ?
|
|
||||||
`).get(id);
|
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
|
logger.warn(`Document ${id} not found`);
|
||||||
return res.status(404).json({ error: 'Document not found' });
|
return res.status(404).json({ error: 'Document not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify user has permission (must be uploader or org admin)
|
// Delete from Meilisearch index
|
||||||
const hasPermission = db.prepare(`
|
try {
|
||||||
SELECT 1 FROM user_organizations
|
const index = await searchClient.getIndex(MEILISEARCH_INDEX_NAME);
|
||||||
WHERE user_id = ? AND organization_id = ? AND role IN ('admin', 'manager')
|
const filter = `docId = "${id}"`;
|
||||||
UNION
|
await index.deleteDocuments({ filter });
|
||||||
SELECT 1 FROM documents
|
logger.info(`Deleted search entries for document ${id}`);
|
||||||
WHERE id = ? AND uploaded_by = ?
|
} catch (err) {
|
||||||
`).get(userId, document.organization_id, id, userId);
|
logger.warn(`Meilisearch cleanup failed for ${id}:`, err);
|
||||||
|
// Continue with deletion even if search cleanup fails
|
||||||
if (!hasPermission) {
|
|
||||||
return res.status(403).json({
|
|
||||||
error: 'Access denied',
|
|
||||||
message: 'You do not have permission to delete this document'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft delete - update status
|
// Delete from database (CASCADE will handle document_pages, ocr_jobs)
|
||||||
const timestamp = Date.now();
|
const deleteStmt = db.prepare('DELETE FROM documents WHERE id = ?');
|
||||||
db.prepare(`
|
deleteStmt.run(id);
|
||||||
UPDATE documents
|
logger.info(`Deleted database record for document ${id}`);
|
||||||
SET status = 'deleted', updated_at = ?
|
|
||||||
WHERE id = ?
|
// Delete from filesystem
|
||||||
`).run(timestamp, id);
|
const uploadsDir = path.join(process.cwd(), '../uploads');
|
||||||
|
const docFolder = path.join(uploadsDir, id);
|
||||||
|
|
||||||
|
if (fs.existsSync(docFolder)) {
|
||||||
|
await rm(docFolder, { recursive: true, force: true });
|
||||||
|
logger.info(`Deleted filesystem folder for document ${id}`);
|
||||||
|
} else {
|
||||||
|
logger.warn(`Folder not found for document ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Document ${id} deleted successfully`);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
success: true,
|
||||||
message: 'Document deleted successfully',
|
message: 'Document deleted successfully',
|
||||||
documentId: id
|
documentId: id,
|
||||||
|
title: document.title
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Document deletion error:', error);
|
logger.error(`Failed to delete document ${id}`, error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Failed to delete document',
|
error: 'Failed to delete document',
|
||||||
message: error.message
|
message: error.message
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue