feat: Add 52 new tools bringing total to 160
New modules (11): - teams.ts (5 tools): Team/workspace management - integrations.ts (6 tools): External integrations (Slack, embeds) - notifications.ts (4 tools): User notification management - subscriptions.ts (4 tools): Document subscription management - templates.ts (5 tools): Document template management - imports-tools.ts (4 tools): Import job management - emojis.ts (3 tools): Custom emoji management - user-permissions.ts (3 tools): Permission management - bulk-operations.ts (6 tools): Batch operations - advanced-search.ts (6 tools): Faceted search, recent, orphaned, duplicates - analytics.ts (6 tools): Usage statistics and insights Updated: - src/index.ts: Import and register all new tools - src/tools/index.ts: Export all new modules - CHANGELOG.md: Version 1.2.0 entry - CLAUDE.md: Updated tool count to 160 - CONTINUE.md: Updated state documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
287
src/tools/bulk-operations.ts
Normal file
287
src/tools/bulk-operations.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Bulk Operations Tools
|
||||
* Batch operations on documents, collections, etc.
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import { BaseTool, ToolResponse } from '../types/tools.js';
|
||||
import { isValidUUID } from '../utils/security.js';
|
||||
|
||||
/**
|
||||
* bulk.archive_documents - Archive multiple documents
|
||||
*/
|
||||
const bulkArchiveDocuments: BaseTool<{ document_ids: string[] }> = {
|
||||
name: 'outline_bulk_archive_documents',
|
||||
description: 'Archive multiple documents at once.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
document_ids: { type: 'array', items: { type: 'string' }, description: 'Array of document IDs (UUIDs)' },
|
||||
},
|
||||
required: ['document_ids'],
|
||||
},
|
||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||
if (!args.document_ids || args.document_ids.length === 0) throw new Error('At least one document_id required');
|
||||
if (args.document_ids.length > 100) throw new Error('Maximum 100 documents per operation');
|
||||
|
||||
// Validate all IDs
|
||||
for (const id of args.document_ids) {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "archivedAt" = NOW(), "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "archivedAt" IS NULL AND "deletedAt" IS NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids]);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
archived: result.rows,
|
||||
archivedCount: result.rows.length,
|
||||
requestedCount: args.document_ids.length,
|
||||
message: `${result.rows.length} documents archived`
|
||||
}, null, 2) }],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* bulk.delete_documents - Soft delete multiple documents
|
||||
*/
|
||||
const bulkDeleteDocuments: BaseTool<{ document_ids: string[] }> = {
|
||||
name: 'outline_bulk_delete_documents',
|
||||
description: 'Soft delete multiple documents at once.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
document_ids: { type: 'array', items: { type: 'string' }, description: 'Array of document IDs (UUIDs)' },
|
||||
},
|
||||
required: ['document_ids'],
|
||||
},
|
||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||
if (!args.document_ids || args.document_ids.length === 0) throw new Error('At least one document_id required');
|
||||
if (args.document_ids.length > 100) throw new Error('Maximum 100 documents per operation');
|
||||
|
||||
for (const id of args.document_ids) {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
const deletedById = await pgClient.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
||||
const userId = deletedById.rows.length > 0 ? deletedById.rows[0].id : null;
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "deletedAt" = NOW(), "deletedById" = $2, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids, userId]);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
deleted: result.rows,
|
||||
deletedCount: result.rows.length,
|
||||
requestedCount: args.document_ids.length,
|
||||
message: `${result.rows.length} documents deleted`
|
||||
}, null, 2) }],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* bulk.move_documents - Move multiple documents to collection
|
||||
*/
|
||||
const bulkMoveDocuments: BaseTool<{ document_ids: string[]; collection_id: string; parent_document_id?: string }> = {
|
||||
name: 'outline_bulk_move_documents',
|
||||
description: 'Move multiple documents to a collection or under a parent document.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
document_ids: { type: 'array', items: { type: 'string' }, description: 'Array of document IDs (UUIDs)' },
|
||||
collection_id: { type: 'string', description: 'Target collection ID (UUID)' },
|
||||
parent_document_id: { type: 'string', description: 'Optional parent document ID (UUID)' },
|
||||
},
|
||||
required: ['document_ids', 'collection_id'],
|
||||
},
|
||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||
if (!args.document_ids || args.document_ids.length === 0) throw new Error('At least one document_id required');
|
||||
if (args.document_ids.length > 100) throw new Error('Maximum 100 documents per operation');
|
||||
if (!isValidUUID(args.collection_id)) throw new Error('Invalid collection_id');
|
||||
if (args.parent_document_id && !isValidUUID(args.parent_document_id)) throw new Error('Invalid parent_document_id');
|
||||
|
||||
for (const id of args.document_ids) {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
// Verify collection exists
|
||||
const collectionCheck = await pgClient.query(
|
||||
`SELECT id FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
||||
[args.collection_id]
|
||||
);
|
||||
if (collectionCheck.rows.length === 0) throw new Error('Collection not found');
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "collectionId" = $2, "parentDocumentId" = $3, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||
RETURNING id, title, "collectionId"
|
||||
`, [args.document_ids, args.collection_id, args.parent_document_id || null]);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
moved: result.rows,
|
||||
movedCount: result.rows.length,
|
||||
targetCollectionId: args.collection_id,
|
||||
message: `${result.rows.length} documents moved`
|
||||
}, null, 2) }],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* bulk.restore_documents - Restore multiple deleted documents
|
||||
*/
|
||||
const bulkRestoreDocuments: BaseTool<{ document_ids: string[] }> = {
|
||||
name: 'outline_bulk_restore_documents',
|
||||
description: 'Restore multiple soft-deleted documents.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
document_ids: { type: 'array', items: { type: 'string' }, description: 'Array of document IDs (UUIDs)' },
|
||||
},
|
||||
required: ['document_ids'],
|
||||
},
|
||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||
if (!args.document_ids || args.document_ids.length === 0) throw new Error('At least one document_id required');
|
||||
if (args.document_ids.length > 100) throw new Error('Maximum 100 documents per operation');
|
||||
|
||||
for (const id of args.document_ids) {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "deletedAt" = NULL, "deletedById" = NULL, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NOT NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids]);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
restored: result.rows,
|
||||
restoredCount: result.rows.length,
|
||||
requestedCount: args.document_ids.length,
|
||||
message: `${result.rows.length} documents restored`
|
||||
}, null, 2) }],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* bulk.add_users_to_collection - Add multiple users to collection
|
||||
*/
|
||||
const bulkAddUsersToCollection: BaseTool<{ user_ids: string[]; collection_id: string; permission?: string }> = {
|
||||
name: 'outline_bulk_add_users_to_collection',
|
||||
description: 'Add multiple users to a collection with specified permission.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_ids: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs (UUIDs)' },
|
||||
collection_id: { type: 'string', description: 'Collection ID (UUID)' },
|
||||
permission: { type: 'string', description: 'Permission level: read_write, read, admin (default: read_write)' },
|
||||
},
|
||||
required: ['user_ids', 'collection_id'],
|
||||
},
|
||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||
if (!args.user_ids || args.user_ids.length === 0) throw new Error('At least one user_id required');
|
||||
if (args.user_ids.length > 50) throw new Error('Maximum 50 users per operation');
|
||||
if (!isValidUUID(args.collection_id)) throw new Error('Invalid collection_id');
|
||||
|
||||
for (const id of args.user_ids) {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid user ID: ${id}`);
|
||||
}
|
||||
|
||||
const permission = args.permission || 'read_write';
|
||||
const creatorResult = await pgClient.query(`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_ids[0];
|
||||
|
||||
const added: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const userId of args.user_ids) {
|
||||
// Check if already exists
|
||||
const existing = await pgClient.query(
|
||||
`SELECT "userId" FROM collection_users WHERE "userId" = $1 AND "collectionId" = $2`,
|
||||
[userId, args.collection_id]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
skipped.push(userId);
|
||||
} else {
|
||||
await pgClient.query(`
|
||||
INSERT INTO collection_users ("userId", "collectionId", permission, "createdById", "createdAt", "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
`, [userId, args.collection_id, permission, createdById]);
|
||||
added.push(userId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
addedUserIds: added,
|
||||
skippedUserIds: skipped,
|
||||
addedCount: added.length,
|
||||
skippedCount: skipped.length,
|
||||
permission,
|
||||
message: `${added.length} users added, ${skipped.length} already existed`
|
||||
}, null, 2) }],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* bulk.remove_users_from_collection - Remove multiple users from collection
|
||||
*/
|
||||
const bulkRemoveUsersFromCollection: BaseTool<{ user_ids: string[]; collection_id: string }> = {
|
||||
name: 'outline_bulk_remove_users_from_collection',
|
||||
description: 'Remove multiple users from a collection.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_ids: { type: 'array', items: { type: 'string' }, description: 'Array of user IDs (UUIDs)' },
|
||||
collection_id: { type: 'string', description: 'Collection ID (UUID)' },
|
||||
},
|
||||
required: ['user_ids', 'collection_id'],
|
||||
},
|
||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||
if (!args.user_ids || args.user_ids.length === 0) throw new Error('At least one user_id required');
|
||||
if (args.user_ids.length > 50) throw new Error('Maximum 50 users per operation');
|
||||
if (!isValidUUID(args.collection_id)) throw new Error('Invalid collection_id');
|
||||
|
||||
for (const id of args.user_ids) {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid user ID: ${id}`);
|
||||
}
|
||||
|
||||
const result = await pgClient.query(`
|
||||
DELETE FROM collection_users
|
||||
WHERE "userId" = ANY($1) AND "collectionId" = $2
|
||||
RETURNING "userId"
|
||||
`, [args.user_ids, args.collection_id]);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
removedUserIds: result.rows.map(r => r.userId),
|
||||
removedCount: result.rows.length,
|
||||
requestedCount: args.user_ids.length,
|
||||
message: `${result.rows.length} users removed from collection`
|
||||
}, null, 2) }],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const bulkOperationsTools: BaseTool<any>[] = [
|
||||
bulkArchiveDocuments, bulkDeleteDocuments, bulkMoveDocuments,
|
||||
bulkRestoreDocuments, bulkAddUsersToCollection, bulkRemoveUsersFromCollection
|
||||
];
|
||||
Reference in New Issue
Block a user