From 6f5d17516b02178eea8085aa6802414d07789259 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Sat, 31 Jan 2026 13:32:41 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 16 +++++++++++++++ src/tools/attachments.ts | 36 +++++++++++++++------------------ src/tools/groups.ts | 31 +++++++++++++++-------------- src/tools/users.ts | 43 ++++++++++++++++------------------------ 4 files changed, 65 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d19458..d08a10c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. +## [1.0.1] - 2026-01-31 + +### Fixed + +- **Users:** Adapted to Outline schema - use `role` enum instead of `isAdmin`/`isViewer`/`isSuspended` booleans +- **Users:** Removed non-existent `username` column +- **Groups:** Fixed `group_users` table queries - no `deletedAt` column, composite PK +- **Groups:** Fixed ambiguous column references in subqueries +- **Attachments:** Removed non-existent `url` and `deletedAt` columns +- **Attachments:** Changed delete to hard delete (no soft delete support) + +### Changed + +- Users suspend/activate now use `suspendedAt` column instead of boolean +- Groups member count uses correct join without deletedAt filter + ## [1.0.0] - 2026-01-31 ### Added diff --git a/src/tools/attachments.ts b/src/tools/attachments.ts index b5a3e0c..70d0a6c 100644 --- a/src/tools/attachments.ts +++ b/src/tools/attachments.ts @@ -46,7 +46,7 @@ const listAttachments: BaseTool = { }, handler: async (args, pgClient): Promise => { 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 = { 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 = { 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 = { SELECT a.id, a.key, - a.url, a."contentType", a.size, a.acl, @@ -160,7 +160,8 @@ const getAttachment: BaseTool = { 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 = { // 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 = { 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 = { "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 = { */ const deleteAttachment: BaseTool = { 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 = { } 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 => { - 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( diff --git a/src/tools/groups.ts b/src/tools/groups.ts index e3ee79a..be2e725 100644 --- a/src/tools/groups.ts +++ b/src/tools/groups.ts @@ -53,14 +53,15 @@ const listGroups: BaseTool = { SELECT g.id, g.name, + g.description, g."teamId", g."createdById", g."createdAt", g."updatedAt", t.name as "teamName", u.name as "createdByName", - (SELECT COUNT(*) FROM group_users WHERE "groupId" = g.id AND "deletedAt" IS NULL) as "memberCount", - (SELECT COUNT(*) FROM groups WHERE ${whereConditions.join(' AND ')}) as total + (SELECT COUNT(*) FROM group_users gu WHERE gu."groupId" = g.id) as "memberCount", + (SELECT COUNT(*) FROM groups g2 WHERE g2."deletedAt" IS NULL) as total FROM groups g LEFT JOIN teams t ON g."teamId" = t.id LEFT JOIN users u ON g."createdById" = u.id @@ -119,13 +120,14 @@ const getGroup: BaseTool = { SELECT g.id, g.name, + g.description, g."teamId", g."createdById", g."createdAt", g."updatedAt", t.name as "teamName", u.name as "createdByName", - (SELECT COUNT(*) FROM group_users WHERE "groupId" = g.id AND "deletedAt" IS NULL) as "memberCount" + (SELECT COUNT(*) FROM group_users gu WHERE gu."groupId" = g.id) as "memberCount" FROM groups g LEFT JOIN teams t ON g."teamId" = t.id LEFT JOIN users u ON g."createdById" = u.id @@ -183,7 +185,7 @@ const createGroup: BaseTool = { // Get first admin user as creator (adjust as needed) const userResult = await pgClient.query( - `SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1` + `SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1` ); if (userResult.rows.length === 0) { throw new Error('No admin user found'); @@ -359,19 +361,19 @@ const listGroupMembers: BaseTool = { const result = await pgClient.query( ` SELECT - gu.id as "membershipId", gu."userId", gu."groupId", gu."createdById", gu."createdAt", + gu.permission, u.name as "userName", u.email as "userEmail", - u."isAdmin" as "userIsAdmin", + u.role as "userRole", creator.name as "addedByName" FROM group_users gu JOIN users u ON gu."userId" = u.id LEFT JOIN users creator ON gu."createdById" = creator.id - WHERE gu."groupId" = $1 AND gu."deletedAt" IS NULL AND u."deletedAt" IS NULL + WHERE gu."groupId" = $1 AND u."deletedAt" IS NULL ORDER BY gu."createdAt" DESC `, [args.id] @@ -445,7 +447,7 @@ const addUserToGroup: BaseTool<{ id: string; user_id: string }> = { // Check if user is already in group const existingMembership = await pgClient.query( - `SELECT id FROM group_users WHERE "groupId" = $1 AND "userId" = $2 AND "deletedAt" IS NULL`, + `SELECT "userId" FROM group_users WHERE "groupId" = $1 AND "userId" = $2`, [args.id, args.user_id] ); if (existingMembership.rows.length > 0) { @@ -454,18 +456,18 @@ const addUserToGroup: BaseTool<{ id: string; user_id: string }> = { // Get first admin user as creator (adjust as needed) const creatorResult = await pgClient.query( - `SELECT id FROM users WHERE "isAdmin" = true AND "deletedAt" IS NULL LIMIT 1` + `SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1` ); const createdById = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : args.user_id; const result = await pgClient.query( ` INSERT INTO group_users ( - id, "userId", "groupId", "createdById", + "userId", "groupId", "createdById", "createdAt", "updatedAt" ) VALUES ( - gen_random_uuid(), $1, $2, $3, + $1, $2, $3, NOW(), NOW() ) RETURNING * @@ -521,10 +523,9 @@ const removeUserFromGroup: BaseTool<{ id: string; user_id: string }> = { const result = await pgClient.query( ` - UPDATE group_users - SET "deletedAt" = NOW() - WHERE "groupId" = $1 AND "userId" = $2 AND "deletedAt" IS NULL - RETURNING id, "userId", "groupId" + DELETE FROM group_users + WHERE "groupId" = $1 AND "userId" = $2 + RETURNING "userId", "groupId" `, [args.id, args.user_id] ); diff --git a/src/tools/users.ts b/src/tools/users.ts index f66913a..37793ef 100644 --- a/src/tools/users.ts +++ b/src/tools/users.ts @@ -56,13 +56,13 @@ const listUsers: BaseTool = { // Add role/status filters switch (filter) { case 'admins': - whereConditions.push('u."isAdmin" = true'); + whereConditions.push("u.role = 'admin'"); break; case 'members': - whereConditions.push('u."isAdmin" = false AND u."isViewer" = false'); + whereConditions.push("u.role = 'member'"); break; case 'suspended': - whereConditions.push('u."isSuspended" = true'); + whereConditions.push('u."suspendedAt" IS NOT NULL'); break; case 'invited': whereConditions.push('u."lastSignedInAt" IS NULL'); @@ -76,16 +76,13 @@ const listUsers: BaseTool = { SELECT u.id, u.email, - u.username, u.name, u."avatarUrl", u.language, u.preferences, u."notificationSettings", u.timezone, - u."isAdmin", - u."isViewer", - u."isSuspended", + u.role, u."lastActiveAt", u."lastSignedInAt", u."suspendedAt", @@ -94,7 +91,7 @@ const listUsers: BaseTool = { u."createdAt", u."updatedAt", t.name as "teamName", - (SELECT COUNT(*) FROM users WHERE ${whereConditions.join(' AND ')}) as total + (SELECT COUNT(*) FROM users u2 WHERE u2."deletedAt" IS NULL) as total FROM users u LEFT JOIN teams t ON u."teamId" = t.id ${whereClause} @@ -152,16 +149,13 @@ const getUser: BaseTool = { SELECT u.id, u.email, - u.username, u.name, u."avatarUrl", u.language, u.preferences, u."notificationSettings", u.timezone, - u."isAdmin", - u."isViewer", - u."isSuspended", + u.role, u."lastActiveAt", u."lastSignedInAt", u."suspendedAt", @@ -254,22 +248,19 @@ const createUser: BaseTool = { } const teamId = teamResult.rows[0].id; - const isAdmin = role === 'admin'; - const isViewer = role === 'viewer'; - const result = await pgClient.query( ` INSERT INTO users ( - id, email, name, "teamId", "isAdmin", "isViewer", + id, email, name, "teamId", role, "createdAt", "updatedAt" ) VALUES ( - gen_random_uuid(), $1, $2, $3, $4, $5, + gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW() ) RETURNING * `, - [email, name, teamId, isAdmin, isViewer] + [email, name, teamId, role] ); return { @@ -458,9 +449,9 @@ const suspendUser: BaseTool = { const result = await pgClient.query( ` UPDATE users - SET "isSuspended" = true, "suspendedAt" = NOW() + SET "suspendedAt" = NOW() WHERE id = $1 AND "deletedAt" IS NULL - RETURNING id, email, name, "isSuspended", "suspendedAt" + RETURNING id, email, name, "suspendedAt" `, [args.id] ); @@ -511,9 +502,9 @@ const activateUser: BaseTool = { const result = await pgClient.query( ` UPDATE users - SET "isSuspended" = false, "suspendedAt" = NULL, "suspendedById" = NULL + SET "suspendedAt" = NULL, "suspendedById" = NULL WHERE id = $1 AND "deletedAt" IS NULL - RETURNING id, email, name, "isSuspended" + RETURNING id, email, name, "suspendedAt" `, [args.id] ); @@ -564,9 +555,9 @@ const promoteUser: BaseTool = { const result = await pgClient.query( ` UPDATE users - SET "isAdmin" = true, "isViewer" = false, "updatedAt" = NOW() + SET role = 'admin', "updatedAt" = NOW() WHERE id = $1 AND "deletedAt" IS NULL - RETURNING id, email, name, "isAdmin", "isViewer" + RETURNING id, email, name, role `, [args.id] ); @@ -617,9 +608,9 @@ const demoteUser: BaseTool = { const result = await pgClient.query( ` UPDATE users - SET "isAdmin" = false, "updatedAt" = NOW() + SET role = 'member', "updatedAt" = NOW() WHERE id = $1 AND "deletedAt" IS NULL - RETURNING id, email, name, "isAdmin", "isViewer" + RETURNING id, email, name, role `, [args.id] );