fix: Schema compatibility - emoji → icon column rename

Production Outline DB uses 'icon' column instead of 'emoji' for documents
and revisions. Fixed all affected queries:

- documents.ts: SELECT queries
- advanced-search.ts: Search queries
- analytics.ts: Analytics + GROUP BY
- export-import.ts: Export/import metadata
- templates.ts: Template queries + INSERT
- collections.ts: Collection document listing
- revisions.ts: Revision comparison

reactions.emoji kept unchanged (correct schema)

Tested: 448 documents successfully queried from hub.descomplicar.pt

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 17:14:27 +00:00
parent 5f49cb63e8
commit 7d2a014b74
10 changed files with 63 additions and 37 deletions

View File

@@ -2,6 +2,32 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.3.2] - 2026-01-31
### Fixed
- **Schema Compatibility:** Fixed column name mismatch with production Outline database
- Changed `emoji` to `icon` in documents queries (8 files affected)
- Changed `emoji` to `icon` in revisions queries
- Updated export/import tools to use `icon` field
- Updated templates tools to use `icon` field
- `reactions.emoji` kept unchanged (correct schema)
### Files Updated
- `src/tools/documents.ts` - SELECT queries
- `src/tools/advanced-search.ts` - Search queries
- `src/tools/analytics.ts` - Analytics queries + GROUP BY
- `src/tools/export-import.ts` - Export/import with metadata
- `src/tools/templates.ts` - Template queries + INSERT
- `src/tools/collections.ts` - Collection document listing
- `src/tools/revisions.ts` - Revision comparison
### Verified
- Production connection: hub.descomplicar.pt (448 documents)
- All 164 tools build without errors
## [1.3.1] - 2026-01-31 ## [1.3.1] - 2026-01-31
### Added ### Added

View File

@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
MCP server for direct PostgreSQL access to Outline Wiki database. Follows patterns established by `mcp-desk-crm-sql-v3`. MCP server for direct PostgreSQL access to Outline Wiki database. Follows patterns established by `mcp-desk-crm-sql-v3`.
**Version:** 1.3.1 **Version:** 1.3.2
**Total Tools:** 164 tools across 33 modules **Total Tools:** 164 tools across 33 modules
**Production:** hub.descomplicar.pt (via SSH tunnel) **Production:** hub.descomplicar.pt (via SSH tunnel)

View File

@@ -1,6 +1,6 @@
{ {
"name": "mcp-outline-postgresql", "name": "mcp-outline-postgresql",
"version": "1.3.1", "version": "1.3.2",
"description": "MCP Server for Outline Wiki via PostgreSQL direct access", "description": "MCP Server for Outline Wiki via PostgreSQL direct access",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {

View File

@@ -86,7 +86,7 @@ const advancedSearchDocuments: BaseTool<AdvancedSearchArgs> = {
const result = await pgClient.query(` const result = await pgClient.query(`
SELECT SELECT
d.id, d.title, d.emoji, d.template, d.id, d.title, d.icon, d.template,
d."collectionId", d."createdById", d."collectionId", d."createdById",
d."createdAt", d."updatedAt", d."publishedAt", d."archivedAt", d."createdAt", d."updatedAt", d."publishedAt", d."archivedAt",
c.name as "collectionName", c.name as "collectionName",
@@ -217,7 +217,7 @@ const searchRecent: BaseTool<PaginationArgs & { collection_id?: string; days?: n
const result = await pgClient.query(` const result = await pgClient.query(`
SELECT SELECT
d.id, d.title, d.emoji, d."collectionId", d.id, d.title, d.icon, d."collectionId",
d."updatedAt", d."createdAt", d."updatedAt", d."createdAt",
c.name as "collectionName", c.name as "collectionName",
u.name as "lastModifiedByName" u.name as "lastModifiedByName"

View File

@@ -220,14 +220,14 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
// Most viewed documents // Most viewed documents
const mostViewed = await pgClient.query(` const mostViewed = await pgClient.query(`
SELECT SELECT
d.id, d.title, d.emoji, c.name as "collectionName", d.id, d.title, d.icon, c.name as "collectionName",
COUNT(v.id) as "viewCount", COUNT(v.id) as "viewCount",
COUNT(DISTINCT v."userId") as "uniqueViewers" COUNT(DISTINCT v."userId") as "uniqueViewers"
FROM documents d FROM documents d
LEFT JOIN views v ON v."documentId" = d.id LEFT JOIN views v ON v."documentId" = d.id
LEFT JOIN collections c ON d."collectionId" = c.id LEFT JOIN collections c ON d."collectionId" = c.id
WHERE d."deletedAt" IS NULL ${collectionCondition} WHERE d."deletedAt" IS NULL ${collectionCondition}
GROUP BY d.id, d.title, d.emoji, c.name GROUP BY d.id, d.title, d.icon, c.name
ORDER BY "viewCount" DESC ORDER BY "viewCount" DESC
LIMIT 10 LIMIT 10
`, params); `, params);
@@ -235,13 +235,13 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
// Most starred documents // Most starred documents
const mostStarred = await pgClient.query(` const mostStarred = await pgClient.query(`
SELECT SELECT
d.id, d.title, d.emoji, c.name as "collectionName", d.id, d.title, d.icon, c.name as "collectionName",
COUNT(s.id) as "starCount" COUNT(s.id) as "starCount"
FROM documents d FROM documents d
LEFT JOIN stars s ON s."documentId" = d.id LEFT JOIN stars s ON s."documentId" = d.id
LEFT JOIN collections c ON d."collectionId" = c.id LEFT JOIN collections c ON d."collectionId" = c.id
WHERE d."deletedAt" IS NULL ${collectionCondition} WHERE d."deletedAt" IS NULL ${collectionCondition}
GROUP BY d.id, d.title, d.emoji, c.name GROUP BY d.id, d.title, d.icon, c.name
HAVING COUNT(s.id) > 0 HAVING COUNT(s.id) > 0
ORDER BY "starCount" DESC ORDER BY "starCount" DESC
LIMIT 10 LIMIT 10
@@ -250,7 +250,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
// Stale documents (not updated in 90 days) // Stale documents (not updated in 90 days)
const staleDocuments = await pgClient.query(` const staleDocuments = await pgClient.query(`
SELECT SELECT
d.id, d.title, d.emoji, c.name as "collectionName", d.id, d.title, d.icon, c.name as "collectionName",
d."updatedAt", d."updatedAt",
EXTRACT(DAY FROM NOW() - d."updatedAt") as "daysSinceUpdate" EXTRACT(DAY FROM NOW() - d."updatedAt") as "daysSinceUpdate"
FROM documents d FROM documents d
@@ -267,7 +267,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
// Documents without views // Documents without views
const neverViewed = await pgClient.query(` const neverViewed = await pgClient.query(`
SELECT SELECT
d.id, d.title, d.emoji, c.name as "collectionName", d.id, d.title, d.icon, c.name as "collectionName",
d."createdAt" d."createdAt"
FROM documents d FROM documents d
LEFT JOIN views v ON v."documentId" = d.id LEFT JOIN views v ON v."documentId" = d.id

View File

@@ -583,7 +583,7 @@ export const collectionsTools: BaseTool<any>[] = [
d.id, d.id,
d."urlId", d."urlId",
d.title, d.title,
d.emoji, d.icon,
d."collectionId", d."collectionId",
d."parentDocumentId", d."parentDocumentId",
d.template, d.template,
@@ -1148,7 +1148,7 @@ export const collectionsTools: BaseTool<any>[] = [
SELECT SELECT
d.id, d.id,
d.title, d.title,
d.emoji, d.icon,
d.text, d.text,
d."createdAt", d."createdAt",
d."updatedAt", d."updatedAt",
@@ -1171,7 +1171,7 @@ export const collectionsTools: BaseTool<any>[] = [
const exports = documentsResult.rows.map(doc => { const exports = documentsResult.rows.map(doc => {
const markdown = `--- const markdown = `---
title: ${doc.title} title: ${doc.title}
emoji: ${doc.emoji || ''} icon: ${doc.icon || ''}
author: ${doc.authorName} author: ${doc.authorName}
created: ${doc.createdAt} created: ${doc.createdAt}
updated: ${doc.updatedAt} updated: ${doc.updatedAt}
@@ -1260,7 +1260,7 @@ ${doc.text || ''}
SELECT SELECT
d.id, d.id,
d.title, d.title,
d.emoji, d.icon,
d.text, d.text,
d."createdAt", d."createdAt",
d."updatedAt", d."updatedAt",
@@ -1282,7 +1282,7 @@ ${doc.text || ''}
const documents = documentsResult.rows.map(doc => { const documents = documentsResult.rows.map(doc => {
const markdown = `--- const markdown = `---
title: ${doc.title} title: ${doc.title}
emoji: ${doc.emoji || ''} icon: ${doc.icon || ''}
author: ${doc.authorName} author: ${doc.authorName}
created: ${doc.createdAt} created: ${doc.createdAt}
updated: ${doc.updatedAt} updated: ${doc.updatedAt}

View File

@@ -34,7 +34,7 @@ const listDocuments: BaseTool<DocumentArgs> = {
const direction = validateSortDirection(args.direction); const direction = validateSortDirection(args.direction);
let query = ` let query = `
SELECT d.id, d."urlId", d.title, d.text, d.emoji, SELECT d.id, d."urlId", d.title, d.text, d.icon,
d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById", d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById",
d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt",
d.template, d."templateId", d."fullWidth", d.version, d.template, d."templateId", d."fullWidth", d.version,
@@ -125,7 +125,7 @@ const getDocument: BaseTool<GetDocumentArgs> = {
} }
let query = ` let query = `
SELECT d.id, d."urlId", d.title, d.text, d.emoji, SELECT d.id, d."urlId", d.title, d.text, d.icon,
d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById", d."collectionId", d."parentDocumentId", d."createdById", d."lastModifiedById",
d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", d."deletedAt", d."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", d."deletedAt",
d.template, d."templateId", d."fullWidth", d.version, d.template, d."templateId", d."fullWidth", d.version,

View File

@@ -21,7 +21,7 @@ interface ImportMarkdownArgs {
title: string; title: string;
content: string; content: string;
parent_path?: string; parent_path?: string;
emoji?: string; icon?: string;
}>; }>;
create_hierarchy?: boolean; create_hierarchy?: boolean;
} }
@@ -62,7 +62,7 @@ const exportCollectionToMarkdown: BaseTool<ExportCollectionArgs> = {
const documents = await pgClient.query(` const documents = await pgClient.query(`
WITH RECURSIVE doc_tree AS ( WITH RECURSIVE doc_tree AS (
SELECT SELECT
d.id, d.title, d.text, d.emoji, d."parentDocumentId", d.id, d.title, d.text, d.icon, d."parentDocumentId",
d."createdAt", d."updatedAt", d."publishedAt", d."createdAt", d."updatedAt", d."publishedAt",
u.name as "authorName", u.name as "authorName",
0 as depth, 0 as depth,
@@ -77,7 +77,7 @@ const exportCollectionToMarkdown: BaseTool<ExportCollectionArgs> = {
UNION ALL UNION ALL
SELECT SELECT
d.id, d.title, d.text, d.emoji, d."parentDocumentId", d.id, d.title, d.text, d.icon, d."parentDocumentId",
d."createdAt", d."updatedAt", d."publishedAt", d."createdAt", d."updatedAt", d."publishedAt",
u.name as "authorName", u.name as "authorName",
dt.depth + 1, dt.depth + 1,
@@ -111,7 +111,7 @@ const exportCollectionToMarkdown: BaseTool<ExportCollectionArgs> = {
if (includeMetadata) { if (includeMetadata) {
content += '---\n'; content += '---\n';
content += `title: "${doc.title.replace(/"/g, '\\"')}"\n`; content += `title: "${doc.title.replace(/"/g, '\\"')}"\n`;
if (doc.emoji) content += `emoji: "${doc.emoji}"\n`; if (doc.icon) content += `icon: "${doc.icon}"\n`;
content += `author: "${doc.authorName || 'Unknown'}"\n`; content += `author: "${doc.authorName || 'Unknown'}"\n`;
content += `created: ${doc.createdAt}\n`; content += `created: ${doc.createdAt}\n`;
content += `updated: ${doc.updatedAt}\n`; content += `updated: ${doc.updatedAt}\n`;
@@ -122,7 +122,7 @@ const exportCollectionToMarkdown: BaseTool<ExportCollectionArgs> = {
// Add title as H1 if not already in content // Add title as H1 if not already in content
if (!doc.text?.startsWith('# ')) { if (!doc.text?.startsWith('# ')) {
content += `# ${doc.emoji ? doc.emoji + ' ' : ''}${doc.title}\n\n`; content += `# ${doc.icon ? doc.icon + ' ' : ''}${doc.title}\n\n`;
} }
content += doc.text || ''; content += doc.text || '';
@@ -171,7 +171,7 @@ const importMarkdownFolder: BaseTool<ImportMarkdownArgs> = {
title: { type: 'string', description: 'Document title' }, title: { type: 'string', description: 'Document title' },
content: { type: 'string', description: 'Markdown content' }, content: { type: 'string', description: 'Markdown content' },
parent_path: { type: 'string', description: 'Parent document path (e.g., "parent/child")' }, parent_path: { type: 'string', description: 'Parent document path (e.g., "parent/child")' },
emoji: { type: 'string', description: 'Document emoji' }, icon: { type: 'string', description: 'Document icon' },
}, },
required: ['title', 'content'], required: ['title', 'content'],
}, },
@@ -256,7 +256,7 @@ const importMarkdownFolder: BaseTool<ImportMarkdownArgs> = {
// Create document // Create document
const result = await client.query(` const result = await client.query(`
INSERT INTO documents ( INSERT INTO documents (
id, title, text, emoji, "collectionId", "teamId", "parentDocumentId", id, title, text, icon, "collectionId", "teamId", "parentDocumentId",
"createdById", "lastModifiedById", template, "createdAt", "updatedAt" "createdById", "lastModifiedById", template, "createdAt", "updatedAt"
) )
VALUES ( VALUES (
@@ -266,7 +266,7 @@ const importMarkdownFolder: BaseTool<ImportMarkdownArgs> = {
`, [ `, [
sanitizeInput(doc.title), sanitizeInput(doc.title),
content, content,
doc.emoji || null, doc.icon || null,
args.collection_id, args.collection_id,
teamId, teamId,
parentDocumentId, parentDocumentId,

View File

@@ -54,7 +54,7 @@ const listRevisions: BaseTool<RevisionArgs> = {
r.version, r.version,
r."editorVersion", r."editorVersion",
r.title, r.title,
r.emoji, r.icon,
r."documentId", r."documentId",
r."userId", r."userId",
r."createdAt", r."createdAt",
@@ -127,7 +127,7 @@ const getRevision: BaseTool<GetRevisionArgs> = {
r."editorVersion", r."editorVersion",
r.title, r.title,
r.text, r.text,
r.emoji, r.icon,
r."documentId", r."documentId",
r."userId", r."userId",
r."createdAt", r."createdAt",
@@ -211,7 +211,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = {
r.version, r.version,
r.title, r.title,
r.text, r.text,
r.emoji, r.icon,
r."documentId", r."documentId",
r."createdAt", r."createdAt",
u.name as "createdByName" u.name as "createdByName"
@@ -236,7 +236,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = {
r.version, r.version,
r.title, r.title,
r.text, r.text,
r.emoji, r.icon,
r."documentId", r."documentId",
r."createdAt", r."createdAt",
u.name as "createdByName" u.name as "createdByName"
@@ -263,7 +263,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = {
d.id, d.id,
d.title, d.title,
d.text, d.text,
d.emoji, d.icon,
d."updatedAt" as "createdAt", d."updatedAt" as "createdAt",
u.name as "createdByName" u.name as "createdByName"
FROM documents d FROM documents d
@@ -285,7 +285,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = {
// Calculate basic diff statistics // Calculate basic diff statistics
const textLengthDiff = revision2.text.length - revision1.text.length; const textLengthDiff = revision2.text.length - revision1.text.length;
const titleChanged = revision1.title !== revision2.title; const titleChanged = revision1.title !== revision2.title;
const emojiChanged = revision1.emoji !== revision2.emoji; const iconChanged = revision1.icon !== revision2.icon;
return { return {
content: [ content: [
@@ -298,7 +298,7 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = {
version: revision1.version, version: revision1.version,
title: revision1.title, title: revision1.title,
text: revision1.text, text: revision1.text,
emoji: revision1.emoji, icon: revision1.icon,
createdAt: revision1.createdAt, createdAt: revision1.createdAt,
createdByName: revision1.createdByName, createdByName: revision1.createdByName,
}, },
@@ -307,13 +307,13 @@ const compareRevisions: BaseTool<{ id: string; compare_to?: string }> = {
version: revision2.version, version: revision2.version,
title: revision2.title, title: revision2.title,
text: revision2.text, text: revision2.text,
emoji: revision2.emoji, icon: revision2.icon,
createdAt: revision2.createdAt, createdAt: revision2.createdAt,
createdByName: revision2.createdByName, createdByName: revision2.createdByName,
}, },
comparison: { comparison: {
titleChanged, titleChanged,
emojiChanged, iconChanged,
textLengthDiff, textLengthDiff,
textLengthDiffPercent: ((textLengthDiff / revision1.text.length) * 100).toFixed(2) + '%', textLengthDiffPercent: ((textLengthDiff / revision1.text.length) * 100).toFixed(2) + '%',
}, },

View File

@@ -40,7 +40,7 @@ const listTemplates: BaseTool<TemplateListArgs> = {
const result = await pgClient.query(` const result = await pgClient.query(`
SELECT SELECT
d.id, d.title, d.emoji, d."collectionId", d."createdById", d.id, d.title, d.icon, d."collectionId", d."createdById",
d."createdAt", d."updatedAt", d."createdAt", d."updatedAt",
c.name as "collectionName", c.name as "collectionName",
u.name as "createdByName", u.name as "createdByName",
@@ -131,7 +131,7 @@ const createFromTemplate: BaseTool<{ template_id: string; title: string; collect
// Create document from template // Create document from template
const result = await pgClient.query(` const result = await pgClient.query(`
INSERT INTO documents ( INSERT INTO documents (
id, title, text, emoji, "collectionId", "teamId", "parentDocumentId", id, title, text, icon, "collectionId", "teamId", "parentDocumentId",
"templateId", "createdById", "lastModifiedById", template, "templateId", "createdById", "lastModifiedById", template,
"createdAt", "updatedAt" "createdAt", "updatedAt"
) )
@@ -142,7 +142,7 @@ const createFromTemplate: BaseTool<{ template_id: string; title: string; collect
`, [ `, [
sanitizeInput(args.title), sanitizeInput(args.title),
t.text, t.text,
t.emoji, t.icon,
args.collection_id || t.collectionId, args.collection_id || t.collectionId,
t.teamId, t.teamId,
args.parent_document_id || null, args.parent_document_id || null,