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.
## [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
### 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`.
**Version:** 1.3.1
**Version:** 1.3.2
**Total Tools:** 164 tools across 33 modules
**Production:** hub.descomplicar.pt (via SSH tunnel)

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ const listDocuments: BaseTool<DocumentArgs> = {
const direction = validateSortDirection(args.direction);
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."publishedAt", d."createdAt", d."updatedAt", d."archivedAt",
d.template, d."templateId", d."fullWidth", d.version,
@@ -125,7 +125,7 @@ const getDocument: BaseTool<GetDocumentArgs> = {
}
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."publishedAt", d."createdAt", d."updatedAt", d."archivedAt", d."deletedAt",
d.template, d."templateId", d."fullWidth", d.version,

View File

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

View File

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

View File

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