fix: Adapt SQL queries to actual Outline database schema

- Users: Use role enum instead of isAdmin/isViewer/isSuspended booleans
- Users: Remove non-existent username column
- Groups: Fix group_users table (no deletedAt, composite PK)
- Attachments: Remove url and deletedAt columns, use hard delete

All 10/10 core queries now pass validation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 13:32:41 +00:00
parent 42fc0c6d6d
commit 6f5d17516b
4 changed files with 65 additions and 61 deletions

View File

@@ -46,7 +46,7 @@ const listAttachments: BaseTool<AttachmentListArgs> = {
},
handler: async (args, pgClient): Promise<ToolResponse> => {
const { limit, offset } = validatePagination(args.limit, args.offset);
const conditions: string[] = ['a."deletedAt" IS NULL'];
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
@@ -74,13 +74,12 @@ const listAttachments: BaseTool<AttachmentListArgs> = {
params.push(args.team_id);
}
const whereClause = `WHERE ${conditions.join(' AND ')}`;
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const query = `
SELECT
a.id,
a.key,
a.url,
a."contentType",
a.size,
a.acl,
@@ -89,6 +88,8 @@ const listAttachments: BaseTool<AttachmentListArgs> = {
a."teamId",
a."createdAt",
a."updatedAt",
a."lastAccessedAt",
a."expiresAt",
d.title as "documentTitle",
u.name as "uploadedByName",
u.email as "uploadedByEmail"
@@ -151,7 +152,6 @@ const getAttachment: BaseTool<GetAttachmentArgs> = {
SELECT
a.id,
a.key,
a.url,
a."contentType",
a.size,
a.acl,
@@ -160,7 +160,8 @@ const getAttachment: BaseTool<GetAttachmentArgs> = {
a."teamId",
a."createdAt",
a."updatedAt",
a."deletedAt",
a."lastAccessedAt",
a."expiresAt",
d.title as "documentTitle",
d."collectionId",
u.name as "uploadedByName",
@@ -243,7 +244,7 @@ const createAttachment: BaseTool<CreateAttachmentArgs> = {
// Get first admin user and team
const userQuery = await pgClient.query(
'SELECT u.id, u."teamId" FROM users u WHERE u."isAdmin" = true AND u."deletedAt" IS NULL LIMIT 1'
"SELECT u.id, u.\"teamId\" FROM users u WHERE u.role = 'admin' AND u.\"deletedAt\" IS NULL LIMIT 1"
);
if (userQuery.rows.length === 0) {
@@ -253,14 +254,13 @@ const createAttachment: BaseTool<CreateAttachmentArgs> = {
const userId = userQuery.rows[0].id;
const teamId = userQuery.rows[0].teamId;
// Generate URL and key (in real implementation, this would be S3/storage URL)
// Generate key (path in storage)
const key = `attachments/${Date.now()}-${args.name}`;
const url = `/api/attachments.redirect?id=PLACEHOLDER`;
const query = `
INSERT INTO attachments (
id,
key,
url,
"contentType",
size,
acl,
@@ -269,13 +269,12 @@ const createAttachment: BaseTool<CreateAttachmentArgs> = {
"teamId",
"createdAt",
"updatedAt"
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
) VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
RETURNING *
`;
const result = await pgClient.query(query, [
key,
url,
args.content_type,
args.size,
'private', // Default ACL
@@ -306,7 +305,7 @@ const createAttachment: BaseTool<CreateAttachmentArgs> = {
*/
const deleteAttachment: BaseTool<GetAttachmentArgs> = {
name: 'outline_attachments_delete',
description: 'Soft delete an attachment. The attachment record is marked as deleted but not removed from the database.',
description: 'Delete an attachment permanently.',
inputSchema: {
type: 'object',
properties: {
@@ -323,18 +322,15 @@ const deleteAttachment: BaseTool<GetAttachmentArgs> = {
}
const query = `
UPDATE attachments
SET
"deletedAt" = NOW(),
"updatedAt" = NOW()
WHERE id = $1 AND "deletedAt" IS NULL
DELETE FROM attachments
WHERE id = $1
RETURNING id, key, "documentId"
`;
const result = await pgClient.query(query, [args.id]);
if (result.rows.length === 0) {
throw new Error('Attachment not found or already deleted');
throw new Error('Attachment not found');
}
return {
@@ -376,7 +372,7 @@ const getAttachmentStats: BaseTool<{ team_id?: string; document_id?: string }> =
},
},
handler: async (args, pgClient): Promise<ToolResponse> => {
const conditions: string[] = ['a."deletedAt" IS NULL'];
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
@@ -396,7 +392,7 @@ const getAttachmentStats: BaseTool<{ team_id?: string; document_id?: string }> =
params.push(args.document_id);
}
const whereClause = `WHERE ${conditions.join(' AND ')}`;
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
// Overall statistics
const overallStatsQuery = await pgClient.query(