EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
documents.js
Go to the documentation of this file.
1/**
2 * @file documents.js
3 * @module routes/documents
4 * @description Knowledge base and document library endpoints with attachment support.
5 * Provides CRUD operations for technical documentation with multi-tenant isolation,
6 * file attachments (base64-encoded), and category organization.
7 * @requires express
8 * @requires services/db
9 * @requires middleware/auth
10 * @requires middleware/adminOnly
11 * @requires middleware/tenant
12 * @requires middleware/fileValidation
13 * @author RMM-PSA Development Team
14 * @copyright 2026 RMM-PSA Platform
15 * @license Proprietary
16 */
17
18/**
19 * @apiDefine Documents Knowledge Base
20 * Document library and technical documentation management
21 */
22
23const express = require('express');
24const router = express.Router();
25const pool = require('../services/db');
26const authenticateToken = require('../middleware/auth');
27const requireAdmin = require('../middleware/adminOnly');
28const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
29const { validateAttachments } = require('../middleware/fileValidation');
30
31// Apply authentication and tenant context to all routes
32router.use(authenticateToken, setTenantContext);
33
34/**
35 * @api {get} /api/documents List documents
36 * @apiName ListDocuments
37 * @apiGroup Documents
38 * @apiDescription Retrieve paginated list of documents with tenant filtering.
39 * Returns document metadata with author and tenant info, plus total count for pagination.
40 * Attachments not included in list (use GET /:id for full document).
41 * @apiParam {number} [page=1] Page number (query parameter)
42 * @apiParam {number} [limit=10] Results per page (query parameter, max: 100)
43 * @apiSuccess {object[]} documents Array of document summaries
44 * @apiSuccess {number} documents.doc_id Document ID
45 * @apiSuccess {string} documents.title Document title
46 * @apiSuccess {string} documents.category Document category/type
47 * @apiSuccess {string} documents.content Document content (full text/markdown)
48 * @apiSuccess {DateTime} documents.created_at Creation timestamp
49 * @apiSuccess {DateTime} documents.updated_at Last modified timestamp
50 * @apiSuccess {number} documents.tenant_id Tenant ID
51 * @apiSuccess {string} documents.author_name Author display name
52 * @apiSuccess {string} documents.tenant_name Tenant name
53 * @apiSuccess {number} total Total document count (for pagination)
54 * @apiSuccess {number} page Current page number
55 * @apiSuccess {number} limit Results per page
56 * @apiSuccess {number} pages Total pages available
57 * @apiError (500) {String} error="Server error" Database query failed
58 * @apiExample {curl} Example:
59 * curl -X GET "http://localhost:3000/api/documents?page=1&limit=10" \\
60 * -H "Authorization: Bearer YOUR_TOKEN"
61 * @apiSuccessExample {json} Success-Response:
62 * HTTP/1.1 200 OK
63 * {
64 * "documents": [
65 * {
66 * "doc_id": 42,
67 * "title": "How to Reset Passwords",
68 * "category": "procedure",
69 * "content": "# Password Reset...\n",
70 * "created_at": "2026-03-10T08:00:00.000Z",
71 * "updated_at": "2026-03-12T10:30:00.000Z",
72 * "tenant_id": 2,
73 * "author_name": "John Admin",
74 * "tenant_name": "Acme Corp"
75 * }
76 * ],
77 * "total": 47,
78 * "page": 1,
79 * "limit": 10,
80 * "pages": 5
81 * }
82 */
83router.get('/', async (req, res) => {
84 const page = parseInt(req.query.page) || 1;
85 const limit = parseInt(req.query.limit) || 10;
86 const offset = (page - 1) * limit;
87
88 try {
89 // āœ… enforce tenant filter (now that tenant_id exists)
90 const { clause: tenantClause, params: tenantParams, nextParamIndex } = getTenantFilter(req, 'd', true);
91 const params = [...tenantParams];
92 const whereClause = tenantClause ? `WHERE ${tenantClause}` : '';
93
94 params.push(limit, offset);
95
96 const [docsResult, countResult] = await Promise.all([
97 pool.query(`
98 SELECT
99 d.doc_id,
100 d.title,
101 d.category,
102 d.content,
103 d.created_at,
104 d.updated_at,
105 d.tenant_id,
106 u.name AS author_name,
107 t.name AS tenant_name
108 FROM documents d
109 LEFT JOIN users u ON d.author_id = u.user_id
110 LEFT JOIN tenants t ON d.tenant_id = t.tenant_id
111 ${whereClause}
112 ORDER BY d.doc_id DESC
113 LIMIT $${nextParamIndex} OFFSET $${nextParamIndex + 1}
114 `, params),
115 // āœ… ensure alias ā€œdā€ is defined here too
116 pool.query(`
117 SELECT COUNT(*)
118 FROM documents d
119 ${whereClause}
120 `, tenantParams)
121 ]);
122
123 res.json({
124 documents: docsResult.rows,
125 total: parseInt(countResult.rows[0].count, 10)
126 });
127 } catch (err) {
128 console.error('Error fetching documents:', err);
129 res.status(500).json({ error: 'Server error', details: err.message });
130 }
131});
132
133/**
134 * @api {get} /api/documents/:id Get document by ID
135 * @apiName GetDocument
136 * @apiGroup Documents
137 * @apiDescription Retrieve single document with attachment metadata (file data excluded).
138 * Includes author, tenant info, and list of attachments with size/filename.
139 * Enforces tenant isolation.
140 * @apiParam {number} id Document ID
141 * @apiSuccess {number} doc_id Document ID
142 * @apiSuccess {string} title Document title
143 * @apiSuccess {string} category Document category/type
144 * @apiSuccess {string} content Full document content
145 * @apiSuccess {DateTime} created_at Creation timestamp
146 * @apiSuccess {DateTime} updated_at Last modified timestamp
147 * @apiSuccess {number} tenant_id Tenant ID
148 * @apiSuccess {string} author_name Author display name
149 * @apiSuccess {string} tenant_name Tenant name
150 * @apiSuccess {Object[]} attachments Attachment metadata (data payload excluded)
151 * @apiSuccess {number} attachments.id Attachment ID
152 * @apiSuccess {string} attachments.filename Stored filename
153 * @apiSuccess {string} attachments.original_filename Original upload filename
154 * @apiSuccess {string} attachments.file_size_mb File size (e.g., "2.5 MB")
155 * @apiSuccess {DateTime} attachments.created_at Upload timestamp
156 * @apiError (404) {String} error="Document not found" Document ID not found or tenant mismatch
157 * @apiError (500) {String} error="Server error" Database query failed
158 * @apiExample {curl} Example:
159 * curl -X GET http://localhost:3000/api/documents/42 \\
160 * -H "Authorization: Bearer YOUR_TOKEN"
161 * @apiSuccessExample {json} Success-Response:
162 * HTTP/1.1 200 OK
163 * {
164 * "doc_id": 42,
165 * "title": "Firewall Configuration Guide",
166 * "category": "network",
167 * "content": "# Firewall Setup\n...markdown content...",
168 * "created_at": "2026-03-10T08:00:00.000Z",
169 * "updated_at": "2026-03-12T10:30:00.000Z",
170 * "tenant_id": 2,
171 * "author_name": "John Admin",
172 * "tenant_name": "Acme Corp",
173 * "attachments": [
174 * {
175 * "id": 15,
176 * "filename": "firewall-diagram.png",
177 * "original_filename": "Firewall Diagram.png",
178 * "file_size_mb": "0.8 MB",
179 * "created_at": "2026-03-10T08:30:00.000Z"
180 * }
181 * ]
182 * }
183 */
184router.get('/:id', async (req, res) => {
185 const { id } = req.params;
186
187 try {
188 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'd', false);
189 let query = `
190 SELECT d.doc_id, d.title, d.category, d.content, d.created_at, d.updated_at, d.tenant_id,
191 u.name as author_name, t.name as tenant_name
192 FROM documents d
193 LEFT JOIN users u ON d.author_id = u.user_id
194 LEFT JOIN tenants t ON d.tenant_id = t.tenant_id
195 WHERE d.doc_id = $1
196 `;
197 const params = [id];
198
199 if (tenantClause) {
200 // Shift tenant param placeholders to start at $2 because $1 is the doc_id
201 const adjustedClause = tenantClause.replace(/\$(\d+)/g, (m, n) => `$${parseInt(n, 10) + 1}`);
202 query += ` AND ${adjustedClause}`;
203 params.push(...tenantParams);
204 }
205
206 const result = await pool.query(query, params);
207
208 if (result.rows.length === 0) {
209 return res.status(404).json({ error: 'Document not found' });
210 }
211
212 const doc = result.rows[0];
213
214 // Fetch attachment metadata (exclude data payload)
215 let attachments = [];
216 try {
217 const aRes = await pool.query(
218 `SELECT id, filename, original_filename, file_size_mb, created_at
219 FROM document_attachments
220 WHERE document_id = $1
221 ORDER BY id ASC`,
222 [id]
223 );
224 attachments = aRes.rows;
225 } catch (e) {
226 // Table might not exist yet; ignore
227 console.warn('No attachment metadata available:', e.message);
228 }
229
230 res.json({ ...doc, attachments });
231 } catch (err) {
232 console.error('Error fetching document:', err);
233 res.status(500).json({ error: 'Server error', details: err.message });
234 }
235});
236
237/**
238 * @api {post} /api/documents Create document
239 * @apiName CreateDocument
240 * @apiGroup Documents
241 * @apiDescription Create new document with optional attachments (base64-encoded).
242 * Attachments validated by middleware (file type, size). Document assigned to user's tenant.
243 * Creates document_attachments table if not exists.
244 * @apiParam {string} title Document title
245 * @apiParam {string} content Document content (markdown/text)
246 * @apiParam {string} [category] Document category (e.g., "procedure", "network", "troubleshooting")
247 * @apiParam {string} [type] Legacy category field (use category instead)
248 * @apiParam {object[]} [validatedAttachments] Validated attachments from middleware
249 * @apiParam {string} validatedAttachments.filename Stored filename
250 * @apiParam {string} validatedAttachments.originalFilename Original upload filename
251 * @apiParam {string} validatedAttachments.data Base64-encoded file content
252 * @apiParam {string} validatedAttachments.size File size (e.g., "1.2 MB")
253 * @apiSuccess (201) {Number} doc_id Created document ID
254 * @apiSuccess (201) {String} title Document title
255 * @apiSuccess (201) {String} content Document content
256 * @apiSuccess (201) {String} category Document category
257 * @apiSuccess (201) {Number} tenant_id Tenant ID
258 * @apiSuccess (201) {DateTime} created_at Creation timestamp
259 * @apiSuccess (201) {DateTime} updated_at Last modified timestamp
260 * @apiError (403) {String} error="No tenant context" Tenant not set (authentication issue)
261 * @apiError (500) {String} error="Failed to create document" Database insert failed
262 * @apiExample {curl} Example:
263 * curl -X POST http://localhost:3000/api/documents \\
264 * -H "Authorization: Bearer YOUR_TOKEN" \\
265 * -H "Content-Type: application/json" \\
266 * -d '{
267 * "title": "Windows Update Procedure",
268 * "content": "# Procedure\n1. Open Windows Update...",
269 * "category": "procedure"
270 * }'
271 * @apiSuccessExample {json} Success-Response:
272 * HTTP/1.1 201 Created
273 * {
274 * "doc_id": 43,
275 * "title": "Windows Update Procedure",
276 * "content": "# Procedure\n1. Open Windows Update...",
277 * "category": "procedure",
278 * "tenant_id": 2,
279 * "created_at": "2026-03-12T11:00:00.000Z",
280 * "updated_at": "2026-03-12T11:00:00.000Z"
281 * }
282 */
283router.post('/', validateAttachments, async (req, res) => {
284 const { title, content, category: categoryFromBody, type, validatedAttachments } = req.body;
285
286 // Prefer explicit category; fall back to legacy 'type'
287 const category = categoryFromBody ?? type ?? null;
288
289 if (!req.tenant?.id) {
290 return res.status(403).json({ error: 'No tenant context' });
291 }
292
293 try {
294 console.log('POST /documents - body:', { title, hasContent: !!content, category, attachmentsCount: Array.isArray(validatedAttachments) ? validatedAttachments.length : 0 });
295 console.log('POST /documents - tenant:', req.tenant);
296 const result = await pool.query(
297 `INSERT INTO documents (title, content, category, tenant_id)
298 VALUES ($1, $2, $3, $4) RETURNING *`,
299 [title, content, category, req.tenant.id]
300 );
301
302 const doc = result.rows[0];
303
304 // If attachments are provided and validated, store them
305 if (Array.isArray(validatedAttachments) && validatedAttachments.length > 0) {
306 // ensure attachments table exists
307 await pool.query(`
308 CREATE TABLE IF NOT EXISTS document_attachments (
309 id SERIAL PRIMARY KEY,
310 document_id INTEGER REFERENCES documents(doc_id) ON DELETE CASCADE,
311 filename VARCHAR(255),
312 original_filename VARCHAR(255),
313 data_base64 TEXT,
314 file_size_mb VARCHAR(10),
315 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
316 )
317 `);
318
319 for (const a of validatedAttachments) {
320 try {
321 await pool.query(
322 `INSERT INTO document_attachments (document_id, filename, original_filename, data_base64, file_size_mb)
323 VALUES ($1, $2, $3, $4, $5)`,
324 [doc.doc_id, a.filename, a.originalFilename, a.data, a.size]
325 );
326 } catch (innerErr) {
327 console.error('Failed to save attachment', innerErr);
328 }
329 }
330 }
331
332 res.status(201).json(doc);
333 } catch (err) {
334 console.error('Error creating document:', err);
335 res.status(500).json({ error: 'Failed to create document', details: err.message });
336 }
337});
338
339/**
340 * @api {put} /api/documents/:id Update document
341 * @apiName UpdateDocument
342 * @apiGroup Documents
343 * @apiDescription Update existing document (title, content, category).
344 * Enforces tenant isolation - user can only update documents in their tenant.
345 * Does not affect attachments (use POST /:id/attachments for that).
346 * @apiParam {number} id Document ID
347 * @apiParam {string} title Updated document title
348 * @apiParam {string} content Updated document content
349 * @apiParam {string} [category] Updated document category
350 * @apiSuccess {number} doc_id Document ID
351 * @apiSuccess {string} title Updated title
352 * @apiSuccess {string} content Updated content
353 * @apiSuccess {string} category Updated category
354 * @apiSuccess {number} tenant_id Tenant ID
355 * @apiSuccess {DateTime} created_at Original creation timestamp
356 * @apiSuccess {DateTime} updated_at New update timestamp
357 * @apiError (404) {String} error="Document not found" Document ID not found or tenant mismatch
358 * @apiError (500) {String} error="Failed to update document" Database update failed
359 * @apiExample {curl} Example:
360 * curl -X PUT http://localhost:3000/api/documents/42 \\
361 * -H "Authorization: Bearer YOUR_TOKEN" \\
362 * -H "Content-Type: application/json" \\
363 * -d '{
364 * "title": "Firewall Configuration Guide (Updated)",
365 * "content": "# Updated Firewall Setup\n...",
366 * "category": "network"
367 * }'
368 * @apiSuccessExample {json} Success-Response:
369 * HTTP/1.1 200 OK
370 * {
371 * "doc_id": 42,
372 * "title": "Firewall Configuration Guide (Updated)",
373 * "content": "# Updated Firewall Setup\n...",
374 * "category": "network",
375 * "tenant_id": 2,
376 * "created_at": "2026-03-10T08:00:00.000Z",
377 * "updated_at": "2026-03-12T11:15:00.000Z"
378 * }
379 */
380router.put('/:id', async (req, res) => {
381 const { id } = req.params;
382 const { title, content, category } = req.body;
383
384 try {
385 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'd', false);
386
387 // First check if document exists and user has access
388 let checkQuery = 'SELECT doc_id FROM documents d WHERE doc_id = $1';
389 const checkParams = [id];
390
391 if (tenantClause) {
392 const adjustedClause = tenantClause.replace(/\$(\d+)/g, (m, n) => `$${parseInt(n, 10) + 1}`);
393 checkQuery += ` AND ${adjustedClause}`;
394 checkParams.push(...tenantParams);
395 }
396
397 const checkResult = await pool.query(checkQuery, checkParams);
398
399 if (checkResult.rows.length === 0) {
400 return res.status(404).json({ error: 'Document not found' });
401 }
402
403 // Update the document
404 const result = await pool.query(
405 `UPDATE documents
406 SET title = $1, content = $2, category = $3, updated_at = NOW()
407 WHERE doc_id = $4
408 RETURNING *`,
409 [title, content, category, id]
410 );
411
412 res.json(result.rows[0]);
413 } catch (err) {
414 console.error('Error updating document:', err);
415 res.status(500).json({ error: 'Failed to update document', details: err.message });
416 }
417});
418
419/**
420 * @api {post} /api/documents/:id/attachments Upload attachments
421 * @apiName UploadDocumentAttachments
422 * @apiGroup Documents
423 * @apiDescription Add attachments to existing document (base64-encoded files).
424 * Attachments validated by middleware for file type and size. Creates attachments table if not exists.
425 * Enforces tenant isolation.
426 * @apiParam {number} id Document ID
427 * @apiParam {object[]} validatedAttachments Validated attachments from middleware
428 * @apiParam {string} validatedAttachments.filename Stored filename
429 * @apiParam {string} validatedAttachments.originalFilename Original upload filename
430 * @apiParam {string} validatedAttachments.data Base64-encoded file content
431 * @apiParam {string} validatedAttachments.size File size (e.g., "1.2 MB")
432 * @apiSuccess (201) {String} message="Attachments uploaded successfully"
433 * @apiSuccess (201) {Number} count Number of attachments uploaded
434 * @apiError (400) {String} error="No valid attachments provided" No attachments in request
435 * @apiError (404) {String} error="Document not found" Document ID not found or tenant mismatch
436 * @apiError (500) {String} error="Failed to upload attachments" Database insert failed
437 * @apiExample {curl} Example:
438 * curl -X POST http://localhost:3000/api/documents/42/attachments \\
439 * -H "Authorization: Bearer YOUR_TOKEN" \\
440 * -H "Content-Type: application/json" \\
441 * -d '{
442 * "validatedAttachments": [
443 * {
444 * "filename": "network-diagram.png",
445 * "originalFilename": "Network Diagram.png",
446 * "data": "iVBORw0KGgoAAAANS...",
447 * "size": "1.2 MB"
448 * }
449 * ]
450 * }'
451 * @apiSuccessExample {json} Success-Response:
452 * HTTP/1.1 201 Created
453 * {
454 * "message": "Attachments uploaded successfully",
455 * "count": 1
456 * }
457 */
458router.post('/:id/attachments', validateAttachments, async (req, res) => {
459 const { id } = req.params;
460 const { validatedAttachments } = req.body;
461
462 if (!validatedAttachments || validatedAttachments.length === 0) {
463 return res.status(400).json({ error: 'No valid attachments provided' });
464 }
465
466 try {
467 // Verify document exists and user has tenant access
468 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'd', false);
469 let checkQuery = 'SELECT doc_id FROM documents d WHERE doc_id = $1';
470 const checkParams = [id];
471 if (tenantClause) {
472 const adjusted = tenantClause.replace(/\$(\d+)/g, (m, n) => `$${parseInt(n, 10) + 1}`);
473 checkQuery += ` AND ${adjusted}`;
474 checkParams.push(...tenantParams);
475 }
476 const check = await pool.query(checkQuery, checkParams);
477 if (check.rows.length === 0) {
478 return res.status(404).json({ error: 'Document not found' });
479 }
480
481 // Ensure attachments table exists
482 await pool.query(`
483 CREATE TABLE IF NOT EXISTS document_attachments (
484 id SERIAL PRIMARY KEY,
485 document_id INTEGER REFERENCES documents(doc_id) ON DELETE CASCADE,
486 filename VARCHAR(255),
487 original_filename VARCHAR(255),
488 data_base64 TEXT,
489 file_size_mb VARCHAR(10),
490 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
491 )
492 `);
493
494 // Insert attachments
495 for (const a of validatedAttachments) {
496 try {
497 await pool.query(
498 `INSERT INTO document_attachments (document_id, filename, original_filename, data_base64, file_size_mb)
499 VALUES ($1, $2, $3, $4, $5)`,
500 [id, a.filename, a.originalFilename, a.data, a.size]
501 );
502 } catch (innerErr) {
503 console.error('Failed to save attachment', innerErr);
504 }
505 }
506
507 res.status(201).json({ message: 'Attachments uploaded successfully', count: validatedAttachments.length });
508 } catch (err) {
509 console.error('Error uploading attachments:', err);
510 res.status(500).json({ error: 'Failed to upload attachments', details: err.message });
511 }
512});
513
514/**
515 * @api {get} /api/documents/:id/attachments/:attachmentId/download Download attachment
516 * @apiName DownloadDocumentAttachment
517 * @apiGroup Documents
518 * @apiDescription Download attachment file from document. Returns binary file with original filename.
519 * Decodes base64-encoded attachment data and streams to client. Enforces tenant isolation.
520 * @apiParam {number} id Document ID
521 * @apiParam {number} attachmentId Attachment ID
522 * @apiSuccess {File} file Binary file download with original filename
523 * @apiError (404) {String} error="Document not found" Document ID not found or tenant mismatch
524 * @apiError (404) {String} error="Attachment not found" Attachment ID not found for document
525 * @apiError (500) {String} error="Failed to download attachment" Database query or decoding failed
526 * @apiExample {curl} Example:
527 * curl -X GET http://localhost:3000/api/documents/42/attachments/15/download \\
528 * -H "Authorization: Bearer YOUR_TOKEN" \\
529 * -o firewall-diagram.png
530 * @apiSuccessExample {binary} Success-Response:
531 * HTTP/1.1 200 OK
532 * Content-Type: application/octet-stream
533 * Content-Disposition: attachment; filename="Firewall Diagram.png"
534 *
535 * [binary file content]
536 */
537router.get('/:id/attachments/:attachmentId/download', async (req, res) => {
538 const { id, attachmentId } = req.params;
539
540 console.log(`[Documents] Download request - doc_id: ${id}, attachment_id: ${attachmentId}, user: ${req.user?.user_id}, tenant: ${req.tenant?.id}`);
541
542 try {
543 // Ensure document belongs to tenant via join
544 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'd', false);
545 let checkQuery = 'SELECT 1 FROM documents d WHERE d.doc_id = $1';
546 const checkParams = [id];
547 if (tenantClause) {
548 const adjusted = tenantClause.replace(/\$(\d+)/g, (m, n) => `$${parseInt(n, 10) + 1}`);
549 checkQuery += ` AND ${adjusted}`;
550 checkParams.push(...tenantParams);
551 }
552
553 console.log('[Documents] Download check query:', checkQuery, checkParams);
554
555 const check = await pool.query(checkQuery, checkParams);
556 if (check.rows.length === 0) {
557 console.log('[Documents] Document not found or access denied for tenant');
558 return res.status(404).json({ error: 'Document not found' });
559 }
560
561 const aRes = await pool.query(
562 `SELECT original_filename, filename, data_base64
563 FROM document_attachments
564 WHERE id = $1 AND document_id = $2`,
565 [attachmentId, id]
566 );
567
568 if (aRes.rows.length === 0) {
569 console.log('[Documents] Attachment not found');
570 return res.status(404).json({ error: 'Attachment not found' });
571 }
572
573 const att = aRes.rows[0];
574 console.log('[Documents] Attachment found:', att.original_filename || att.filename);
575
576 // Decode base64 and send as file
577 let base64 = att.data_base64;
578 if (base64.includes(',')) base64 = base64.split(',')[1];
579 const buf = Buffer.from(base64, 'base64');
580
581 res.setHeader('Content-Type', 'application/octet-stream');
582 res.setHeader('Content-Disposition', `attachment; filename="${att.original_filename || att.filename}"`);
583 res.send(buf);
584 } catch (err) {
585 console.error('Error downloading attachment:', err);
586 res.status(500).json({ error: 'Failed to download attachment', details: err.message });
587 }
588});
589
590/**
591 * @api {delete} /api/documents/:id Delete document
592 * @apiName DeleteDocument
593 * @apiGroup Documents
594 * @apiDescription Delete document and associated attachments. Admin only.
595 * Enforces tenant isolation - admin can only delete documents in their tenant.
596 * Cascades to delete attachments first due to foreign key constraints.
597 * @apiParam {number} id Document ID
598 * @apiSuccess {string} message="Document deleted successfully"
599 * @apiSuccess {object} deleted_document Deleted document object
600 * @apiSuccess {number} deleted_document.doc_id Deleted document ID
601 * @apiSuccess {string} deleted_document.title Deleted document title
602 * @apiSuccess {number} deleted_document.tenant_id Tenant ID
603 * @apiError (403) {String} error="Forbidden" User not admin
604 * @apiError (404) {String} error="Document not found" Document ID not found or tenant mismatch
605 * @apiError (500) {String} error="Failed to delete document" Database delete failed
606 * @apiExample {curl} Example:
607 * curl -X DELETE http://localhost:3000/api/documents/42 \\
608 * -H "Authorization: Bearer YOUR_TOKEN"
609 * @apiSuccessExample {json} Success-Response:
610 * HTTP/1.1 200 OK
611 * {
612 * "message": "Document deleted successfully",
613 * "deleted_document": {
614 * "doc_id": 42,
615 * "title": "Firewall Configuration Guide",
616 * "category": "network",
617 * "tenant_id": 2,
618 * "created_at": "2026-03-10T08:00:00.000Z",
619 * "updated_at": "2026-03-12T10:30:00.000Z"
620 * }
621 * }
622 */
623router.delete('/:id', requireAdmin, async (req, res) => {
624 const { id } = req.params;
625
626 try {
627 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'd', false);
628
629 // First check if document exists and user has access
630 let checkQuery = 'SELECT doc_id, title FROM documents d WHERE doc_id = $1';
631 const checkParams = [id];
632
633 if (tenantClause) {
634 const adjustedClause = tenantClause.replace(/\$(\d+)/g, (m, n) => `$${parseInt(n, 10) + 1}`);
635 checkQuery += ` AND ${adjustedClause}`;
636 checkParams.push(...tenantParams);
637 }
638
639 const checkResult = await pool.query(checkQuery, checkParams);
640
641 if (checkResult.rows.length === 0) {
642 return res.status(404).json({ error: 'Document not found' });
643 }
644
645 const document = checkResult.rows[0];
646
647 // Delete associated attachments first (due to foreign key constraints)
648 try {
649 await pool.query('DELETE FROM document_attachments WHERE document_id = $1', [id]);
650 } catch (attachErr) {
651 // Table might not exist, that's ok
652 console.log('No attachments to delete or attachment table does not exist');
653 }
654
655 // Delete the document
656 const result = await pool.query(
657 'DELETE FROM documents WHERE doc_id = $1 RETURNING *',
658 [id]
659 );
660
661 console.log(`[Documents] Document deleted: ID ${id}, Title: "${document.title}", User: ${req.user?.user_id}, Tenant: ${req.tenant?.id}`);
662
663 res.json({
664 message: 'Document deleted successfully',
665 deleted_document: result.rows[0]
666 });
667 } catch (err) {
668 console.error('Error deleting document:', err);
669 res.status(500).json({ error: 'Failed to delete document', details: err.message });
670 }
671});
672
673module.exports = router;