- Replace legacy directory structure (api/, db/, functions/, logs/, pooler/) with single docker-compose.yml based self-hosted setup - Add selfhosted-supabase-mcp TypeScript MCP server for database management - Add .dockerignore for Docker build context - Update .gitignore to exclude .env files, volumes/, backups, logs
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
/**
|
|
* get_cron_job_history - Gets execution history for pg_cron jobs.
|
|
*
|
|
* Requires the pg_cron extension to be installed.
|
|
* Shows recent job runs with status and timing information.
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import { handleSqlResponse, executeSqlWithFallback } from './utils.js';
|
|
import type { ToolContext, ToolPrivilegeLevel } from './types.js';
|
|
|
|
const CronJobRunSchema = z.object({
|
|
jobid: z.number(),
|
|
jobname: z.string().nullable(),
|
|
schedule: z.string(),
|
|
runid: z.number().nullable(),
|
|
status: z.string().nullable(),
|
|
start_time: z.string().nullable(),
|
|
end_time: z.string().nullable(),
|
|
return_message: z.string().nullable(),
|
|
});
|
|
|
|
const GetCronJobHistoryOutputSchema = z.array(CronJobRunSchema);
|
|
|
|
const GetCronJobHistoryInputSchema = z.object({
|
|
jobid: z.number().optional().describe('Filter by specific job ID.'),
|
|
jobname: z.string().optional().describe('Filter by job name pattern.'),
|
|
limit: z.number().optional().default(100).describe('Maximum number of records to return.'),
|
|
status: z.enum(['succeeded', 'failed']).optional().describe('Filter by execution status.'),
|
|
});
|
|
|
|
type GetCronJobHistoryInput = z.infer<typeof GetCronJobHistoryInputSchema>;
|
|
|
|
const mcpInputSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
jobid: {
|
|
type: 'number',
|
|
description: 'Filter by specific job ID.',
|
|
},
|
|
jobname: {
|
|
type: 'string',
|
|
description: 'Filter by job name pattern.',
|
|
},
|
|
limit: {
|
|
type: 'number',
|
|
description: 'Maximum number of records to return.',
|
|
default: 100,
|
|
},
|
|
status: {
|
|
type: 'string',
|
|
enum: ['succeeded', 'failed'],
|
|
description: 'Filter by execution status.',
|
|
},
|
|
},
|
|
required: [],
|
|
};
|
|
|
|
// Safe pattern for job name search - alphanumeric, underscore, hyphen, space
|
|
const jobNamePattern = /^[a-zA-Z0-9_\-\s]+$/;
|
|
|
|
export const getCronJobHistoryTool = {
|
|
name: 'get_cron_job_history',
|
|
description: 'Gets execution history for pg_cron jobs including status and timing. Requires pg_cron to be installed.',
|
|
privilegeLevel: 'regular' as ToolPrivilegeLevel,
|
|
inputSchema: GetCronJobHistoryInputSchema,
|
|
mcpInputSchema: mcpInputSchema,
|
|
outputSchema: GetCronJobHistoryOutputSchema,
|
|
|
|
execute: async (input: GetCronJobHistoryInput, context: ToolContext) => {
|
|
const client = context.selfhostedClient;
|
|
const { jobid, jobname, limit = 100, status } = input;
|
|
|
|
// Validate jobname if provided
|
|
if (jobname && !jobNamePattern.test(jobname)) {
|
|
throw new Error(`Invalid job name pattern: ${jobname}. Use only alphanumeric, underscore, hyphen, and space characters.`);
|
|
}
|
|
|
|
// First check if pg_cron extension is installed
|
|
const checkExtensionSql = `
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_extension WHERE extname = 'pg_cron'
|
|
) as installed;
|
|
`;
|
|
|
|
const extensionCheck = await executeSqlWithFallback(client, checkExtensionSql, true);
|
|
|
|
if (!Array.isArray(extensionCheck) || extensionCheck.length === 0) {
|
|
throw new Error('Failed to check pg_cron extension status.');
|
|
}
|
|
|
|
if (!extensionCheck[0].installed) {
|
|
throw new Error('pg_cron extension is not installed. Install it with: CREATE EXTENSION pg_cron;');
|
|
}
|
|
|
|
// Build query with filters
|
|
const conditions: string[] = [];
|
|
|
|
if (jobid !== undefined) {
|
|
// jobid is a number from Zod validation, safe to use directly
|
|
conditions.push(`j.jobid = ${jobid}`);
|
|
}
|
|
|
|
if (jobname) {
|
|
conditions.push(`j.jobname ILIKE '%${jobname}%'`);
|
|
}
|
|
|
|
if (status) {
|
|
// status is validated by Zod enum, safe to use directly
|
|
conditions.push(`r.status = '${status}'`);
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
|
|
// Ensure limit is within bounds
|
|
const safeLimit = Math.min(Math.max(1, limit), 1000);
|
|
|
|
const historySql = `
|
|
SELECT
|
|
j.jobid,
|
|
j.jobname,
|
|
j.schedule,
|
|
r.runid,
|
|
r.status,
|
|
r.start_time::text,
|
|
r.end_time::text,
|
|
r.return_message
|
|
FROM cron.job j
|
|
LEFT JOIN cron.job_run_details r ON j.jobid = r.jobid
|
|
${whereClause}
|
|
ORDER BY r.start_time DESC NULLS LAST
|
|
LIMIT ${safeLimit}
|
|
`;
|
|
|
|
const result = await executeSqlWithFallback(client, historySql, true);
|
|
return handleSqlResponse(result, GetCronJobHistoryOutputSchema);
|
|
},
|
|
};
|