// src/services/JobService.ts - Complete implementation with type safety
import { BaseService } from "./BaseService.js";
import { JobInfo, HistoryInfo } from "../types/api.js";
import {
JobListParams, JobStatusParams, JobHistoryParams,
JobLogsParams, JobCreateParams, JobUpdateParams, JobDeleteParams,
JobExecuteParams, JobCancelParams, ExecuteQueryParams, GetJobTablesParams
} from "../types/parameters.js";
import {
DeleteResponse, JobExecutionResult, JobStatusResponse, JobLogsResponse,
TableInfo, CountResponse, ApiResponse
} from "../types/responses.js";
import { IJobService } from "../types/services.js";
import { toBooleanString, toIdString } from "../utils/typeConverters.js";
import { assertDefined } from "../utils/typeGuards.js";
export class JobService extends BaseService implements IJobService {
protected getResourceName(): string {
return 'Job';
}
async listJobs(params: JobListParams = {}): Promise<JobInfo[]> {
const queryParams = this.buildOrderedQueryParams(params);
const endpoint = `/jobs${queryParams}`;
const result = await this.syncClient.get<ApiResponse<JobInfo>>(endpoint);
return this.extractArray(result);
}
async countJobs(params: Pick<JobListParams, 'filter'> = {}): Promise<CountResponse> {
const queryParams = new URLSearchParams();
if (params.filter) queryParams.append("$filter", params.filter);
const endpoint = `/jobs/$count${queryParams.toString() ? `?${queryParams}` : ""}`;
const result = await this.syncClient.get<any>(endpoint);
return this.extractCount(result);
}
async getJob(jobName: string): Promise<JobInfo> {
// Add better error handling and encoding
if (!jobName) {
throw new Error("Job name is required for getting job details");
}
try {
const endpoint = this.buildEntityEndpoint('/jobs', jobName);
if (process.env.MCP_MODE || process.env.DEBUG_JOBS) {
console.error(`=== GET JOB DEBUG ===`);
console.error(`Original job name: ${jobName}`);
console.error(`Encoded endpoint: ${endpoint}`);
console.error(`====================`);
}
return await this.syncClient.get<JobInfo>(endpoint);
} catch (error: any) {
// Enhanced error handling
if (error.response?.status === 404) {
throw new Error(`Job '${jobName}' not found`);
} else if (error.response?.status === 400) {
throw new Error(`Invalid job name '${jobName}'. Please check the job name is correct.`);
}
if (process.env.MCP_MODE || process.env.DEBUG_JOBS) {
console.error(`Failed to get job '${jobName}':`, error.message);
if (error.response?.data) {
console.error(`API Response:`, JSON.stringify(error.response.data, null, 2));
}
}
throw error;
}
}
async createJob(params: JobCreateParams): Promise<JobInfo> {
const jobData: Record<string, any> = {
JobName: params.jobName,
Source: params.source,
Destination: params.destination,
};
// Scheduling parameters
if (params.scheduled !== undefined) jobData.Scheduled = toBooleanString(params.scheduled);
if (params.scheduledCron) jobData.ScheduledCron = params.scheduledCron;
// Notification parameters
if (params.notifyWindowsEvent !== undefined) {
jobData.NotifyWindowsEvent = toBooleanString(params.notifyWindowsEvent);
}
if (params.sendEmailNotification !== undefined) {
jobData.SendEmailNotification = toBooleanString(params.sendEmailNotification);
}
if (params.notifyEmailTo) jobData.NotifyEmailTo = params.notifyEmailTo;
if (params.notifyEmailSubject) jobData.NotifyEmailSubject = params.notifyEmailSubject;
if (params.emailErrorOnly !== undefined) {
jobData.EmailErrorOnly = toBooleanString(params.emailErrorOnly);
}
// Job behavior parameters
if (params.verbosity) jobData.Verbosity = params.verbosity;
if (params.tableNamePrefix) jobData.TableNamePrefix = params.tableNamePrefix;
if (params.useGmtDateTime !== undefined) {
jobData.UseGmtDateTime = toBooleanString(params.useGmtDateTime);
}
if (params.truncateTableData !== undefined) {
jobData.TruncateTableData = toBooleanString(params.truncateTableData);
}
if (params.dropTable !== undefined) {
jobData.DropTable = toBooleanString(params.dropTable);
}
if (params.autoTruncateStrings !== undefined) {
jobData.AutoTruncateStrings = toBooleanString(params.autoTruncateStrings);
}
if (params.continueOnError !== undefined) {
jobData.ContinueOnError = toBooleanString(params.continueOnError);
}
if (params.alterSchema !== undefined) {
jobData.AlterSchema = toBooleanString(params.alterSchema);
}
// Replication parameters
if (params.replicateInterval) jobData.ReplicateInterval = params.replicateInterval;
if (params.replicateIntervalUnit) jobData.ReplicateIntervalUnit = params.replicateIntervalUnit;
if (params.replicateStartDate) jobData.ReplicateStartDate = params.replicateStartDate;
// Performance parameters
if (params.batchSize) jobData.BatchSize = params.batchSize;
if (params.commandTimeout) jobData.CommandTimeout = params.commandTimeout;
if (params.skipDeleted !== undefined) {
jobData.SkipDeleted = toBooleanString(params.skipDeleted);
}
// Advanced parameters
if (params.otherCacheOptions) jobData.OtherCacheOptions = params.otherCacheOptions;
if (params.cacheSchema) jobData.CacheSchema = params.cacheSchema;
if (params.preJob) jobData.PreJob = params.preJob;
if (params.postJob) jobData.PostJob = params.postJob;
if (params.type) jobData.Type = params.type.toString();
// Handle queries in Query# format
if (params.queries && Array.isArray(params.queries)) {
params.queries.forEach((query: string, index: number) => {
jobData[`Query#${index + 1}`] = query;
});
}
// Add workspace to request body for POST operations
// The API might expect WorkspaceId in the body for create operations
const currentWorkspace = this.getWorkspace();
if (currentWorkspace && currentWorkspace !== 'default') {
jobData.WorkspaceId = currentWorkspace;
if (process.env.DEBUG_WORKSPACE) {
console.error(`[Job Create] Adding WorkspaceId to body: ${currentWorkspace}`);
}
}
return this.syncClient.post<JobInfo>("/jobs", jobData);
}
async updateJob(params: JobUpdateParams): Promise<JobInfo> {
const jobData: Record<string, any> = {};
// Only include parameters that are being updated
if (params.source) jobData.Source = params.source;
if (params.destination) jobData.Destination = params.destination;
// Map all other parameters similar to create
this.mapJobParameters(params, jobData);
const endpoint = this.buildEntityEndpoint('/jobs', params.jobName);
return this.syncClient.put<JobInfo>(endpoint, jobData);
}
async deleteJob(params: JobDeleteParams): Promise<DeleteResponse> {
const endpoint = this.buildEntityEndpoint('/jobs', params.jobName);
return this.handleDelete(endpoint);
}
async executeJob(params: JobExecuteParams): Promise<JobExecutionResult[]> {
if (!params.jobName && !params.jobId) {
throw new Error("Either jobName or jobId is required");
}
const requestData: Record<string, string> = {};
if (params.jobId) requestData.JobId = toIdString(params.jobId);
if (params.jobName) requestData.JobName = params.jobName;
requestData.WaitForResults = toBooleanString(params.waitForResults !== false);
requestData.Timeout = (params.timeout || 0).toString();
// Add workspace to request body for job execution
const currentWorkspace = this.getWorkspace();
if (currentWorkspace && currentWorkspace !== 'default') {
requestData.WorkspaceId = currentWorkspace;
if (process.env.DEBUG_WORKSPACE) {
console.error(`[Job Execute] Adding WorkspaceId to body: ${currentWorkspace}`);
}
}
const result = await this.syncClient.post<any>("/executeJob", requestData);
// Transform to array of execution results
if (Array.isArray(result)) {
return result;
}
return [result];
}
async cancelJob(params: JobCancelParams): Promise<DeleteResponse> {
if (!params.jobName && !params.jobId) {
throw new Error("Either jobName or jobId is required");
}
const requestData: Record<string, string> = {};
if (params.jobId) requestData.JobId = toIdString(params.jobId);
if (params.jobName) requestData.JobName = params.jobName;
// Add workspace to request body for job cancellation
const currentWorkspace = this.getWorkspace();
if (currentWorkspace && currentWorkspace !== 'default') {
requestData.WorkspaceId = currentWorkspace;
if (process.env.DEBUG_WORKSPACE) {
console.error(`[Job Cancel] Adding WorkspaceId to body: ${currentWorkspace}`);
}
}
await this.syncClient.post<any>("/cancelJob", requestData);
return {
success: true,
message: "Job cancelled successfully"
};
}
async getJobStatus(params: JobStatusParams): Promise<JobStatusResponse> {
if (!params.jobName && !params.jobId) {
throw new Error("Either jobName or jobId is required");
}
const requestData: Record<string, string> = {};
if (params.jobId) requestData.JobId = toIdString(params.jobId);
if (params.jobName) requestData.JobName = params.jobName;
requestData.PushOnQuery = toBooleanString(params.pushOnQuery !== false);
// Add workspace to request body for job status
const currentWorkspace = this.getWorkspace();
if (currentWorkspace && currentWorkspace !== 'default') {
requestData.WorkspaceId = currentWorkspace;
if (process.env.DEBUG_WORKSPACE) {
console.error(`[Job Status] Adding WorkspaceId to body: ${currentWorkspace}`);
}
}
const result = await this.syncClient.post<any>("/getJobStatus", requestData);
// Transform to typed response
return {
jobId: result.JobId || params.jobId || '',
jobName: result.JobName || params.jobName || '',
status: result.Status,
queries: result.Queries
};
}
async getJobHistory(params: JobHistoryParams = {}): Promise<HistoryInfo[]> {
const queryParams = this.buildOrderedQueryParams(params);
const endpoint = `/history${queryParams}`;
const result = await this.syncClient.get<ApiResponse<HistoryInfo>>(endpoint);
return this.extractArray(result);
}
async countHistory(params: Pick<JobHistoryParams, 'filter'> = {}): Promise<CountResponse> {
const queryParams = new URLSearchParams();
if (params.filter) queryParams.append("$filter", params.filter);
const endpoint = `/history/$count${queryParams.toString() ? `?${queryParams}` : ""}`;
const result = await this.syncClient.get<any>(endpoint);
return this.extractCount(result);
}
async getJobLogs(params: JobLogsParams): Promise<JobLogsResponse> {
if (!params.jobName && !params.jobId) {
throw new Error("Either jobName or jobId is required");
}
const requestData: Record<string, string> = {};
if (params.jobId) requestData.JobId = toIdString(params.jobId);
if (params.jobName) requestData.JobName = params.jobName;
requestData.Days = (params.days || 1).toString();
const result = await this.syncClient.post<string>("/getlogs", requestData);
return {
jobName: params.jobName || '',
jobId: params.jobId || '',
logs: result,
days: params.days || 1
};
}
async executeQuery(params: ExecuteQueryParams): Promise<JobExecutionResult[]> {
if (!params.jobName && !params.jobId) {
throw new Error("Either jobName or jobId is required");
}
const requestData: Record<string, string> = {};
if (params.jobId) requestData.JobId = toIdString(params.jobId);
if (params.jobName) requestData.JobName = params.jobName;
requestData.WaitForResults = toBooleanString(params.waitForResults !== false);
requestData.Timeout = (params.timeout || 0).toString();
// Handle queries array
if (params.queries && Array.isArray(params.queries)) {
params.queries.forEach((query: string, index: number) => {
requestData[`Query#${index + 1}`] = query;
});
}
const result = await this.syncClient.post<any>("/executeQuery", requestData);
// Transform to array of execution results
if (Array.isArray(result)) {
return result;
}
return [result];
}
async getJobTables(params: GetJobTablesParams): Promise<TableInfo[]> {
assertDefined(params.connectionName, "Connection name is required");
assertDefined(params.jobId, "Job ID is required");
const requestData: Record<string, any> = {
ConnectionName: params.connectionName,
JobId: toIdString(params.jobId),
TableOrView: params.tableOrView || "ALL",
};
if (params.schema) requestData.Schema = params.schema;
if (params.includeCatalog !== undefined) {
requestData.IncludeCatalog = toBooleanString(params.includeCatalog);
}
if (params.includeSchema !== undefined) {
requestData.IncludeSchema = toBooleanString(params.includeSchema);
}
if (params.topTable !== undefined) {
requestData.TopTable = params.topTable.toString();
}
if (params.skipTable !== undefined) {
requestData.SkipTable = params.skipTable.toString();
}
const result = await this.syncClient.post<ApiResponse<TableInfo>>(
"/getAddingTablesForJob",
requestData
);
return this.extractArray(result);
}
// Helper method to map job parameters
private mapJobParameters(params: Partial<JobCreateParams>, jobData: Record<string, any>): void {
// Scheduling parameters
if (params.scheduled !== undefined) jobData.Scheduled = toBooleanString(params.scheduled);
if (params.scheduledCron !== undefined) jobData.ScheduledCron = params.scheduledCron;
// Continue with all other parameters...
// (Implementation similar to createJob method)
}
// Diagnostic method to help debug job access issues
async diagnoseJobAccess(jobName: string): Promise<any> {
const results: any = {
jobName,
tests: []
};
// Test 1: List all jobs and check if our job exists
try {
const allJobs = await this.listJobs({ top: 100 });
const foundJob = allJobs.find(j => j.JobName === jobName);
results.tests.push({
test: "List all jobs",
success: true,
totalJobs: allJobs.length,
jobFound: !!foundJob,
exactMatch: foundJob?.JobName === jobName,
similarJobs: allJobs
.filter(j => j.JobName.toLowerCase().includes(jobName.toLowerCase()))
.map(j => j.JobName)
});
if (foundJob) {
results.foundJob = foundJob;
}
} catch (error: any) {
results.tests.push({
test: "List all jobs",
success: false,
error: error.message
});
}
// Additional tests...
return results;
}
}