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:
26
CHANGELOG.md
26
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) + '%',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user