mcp-quickbase
Version:
Work with Quickbase via Model Context Protocol
265 lines • 10.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryRecordsTool = void 0;
const base_1 = require("../base");
const logger_1 = require("../../utils/logger");
const logger = (0, logger_1.createLogger)("QueryRecordsTool");
/**
* Tool for querying records from a Quickbase table
*/
class QueryRecordsTool extends base_1.BaseTool {
/**
* Constructor
* @param client Quickbase client
*/
constructor(client) {
super(client);
this.name = "query_records";
this.description = "Executes a query against a Quickbase table with optional pagination";
/**
* Parameter schema for query_records
*/
this.paramSchema = {
type: "object",
properties: {
table_id: {
type: "string",
description: "The ID of the Quickbase table",
},
where: {
type: "string",
description: "Query criteria",
},
select: {
type: "array",
description: "Fields to select",
items: {
type: "string",
},
},
orderBy: {
type: "array",
description: "Fields to order results by",
items: {
type: "object",
properties: {
fieldId: {
type: "string",
},
order: {
type: "string",
enum: ["ASC", "DESC"],
},
},
},
},
max_records: {
type: "number",
description: "Maximum number of records to return when paginating (default: 1000)",
},
skip: {
type: "number",
description: "Number of records to skip",
},
paginate: {
type: "boolean",
description: "Whether to automatically handle pagination for large result sets",
},
options: {
type: "object",
description: "Query options for filtering, ordering, and pagination",
},
},
required: ["table_id"],
};
}
/**
* Run the query_records tool
* @param params Tool parameters
* @returns Queried records
*/
async run(params) {
const { table_id, where, select, orderBy, max_records = 1000, skip = 0, paginate = false, options, } = params;
logger.info("Querying records from Quickbase table", {
tableId: table_id,
maxRecords: max_records,
pagination: paginate ? "enabled" : "disabled",
});
// Prepare the query body
const body = {
from: table_id,
};
// Add where clause if provided
if (where) {
body.where = where;
}
// Add select clause if provided
if (select && select.length > 0) {
body.select = select;
}
// Add sorting if provided
if (orderBy && orderBy.length > 0) {
body.sortBy = orderBy;
}
// Add pagination
const limit = parseInt(max_records.toString(), 10);
body.options = {
skip,
top: Math.min(limit, 1000), // API has a limit of 1000 records per request
...(options || {}),
};
// Execute the query
const response = await this.client.request({
method: "POST",
path: "/records/query",
body,
});
if (!response.success || !response.data) {
logger.error("Failed to query records", {
error: response.error,
tableId: table_id,
});
throw new Error(response.error?.message || "Failed to query records");
}
// Safely validate response structure
if (typeof response.data !== "object" || response.data === null) {
throw new Error("Invalid API response: data is not an object");
}
const data = response.data;
// Validate records array exists
if (!Array.isArray(data.data)) {
logger.error("Query response missing data array", { data });
throw new Error("Query response does not contain records array");
}
const records = data.data;
// Validate and type-cast fields array
const fields = Array.isArray(data.fields)
? data.fields
: undefined;
const metadata = {
fields,
tableId: table_id,
numRecords: records.length,
skip,
};
// Handle pagination if enabled and there may be more records
let allRecords = [...records];
let hasMore = records.length === body.options.top;
if (paginate && hasMore && allRecords.length < limit) {
logger.info("Paginating query results", {
recordsFetched: allRecords.length,
limit,
});
let currentSkip = skip + records.length;
let iterationCount = 0;
const maxIterations = 100; // Circuit breaker: prevent infinite loops
const startTime = Date.now();
const maxTimeMs = 30000; // 30 second timeout
// Continue fetching until we reach the limit or there are no more records
while (hasMore && allRecords.length < limit) {
// Circuit breaker checks
iterationCount++;
if (iterationCount > maxIterations) {
logger.error("Pagination circuit breaker: too many iterations", {
iterationCount,
maxIterations,
totalRecords: allRecords.length,
});
break;
}
if (Date.now() - startTime > maxTimeMs) {
logger.error("Pagination circuit breaker: timeout exceeded", {
timeElapsed: Date.now() - startTime,
maxTimeMs,
totalRecords: allRecords.length,
});
break;
}
// Update pagination options
body.options.skip = currentSkip;
body.options.top = Math.min(limit - allRecords.length, 1000);
// Execute next page query
const pageResponse = await this.client.request({
method: "POST",
path: "/records/query",
body,
});
if (!pageResponse.success || !pageResponse.data) {
logger.error("Failed to query additional records", {
error: pageResponse.error,
tableId: table_id,
skip: currentSkip,
});
break;
}
// Safely validate pagination response structure
if (typeof pageResponse.data !== "object" ||
pageResponse.data === null) {
logger.error("Invalid pagination response: data is not an object");
break;
}
const pageData = pageResponse.data;
// Validate page records array exists
if (!Array.isArray(pageData.data)) {
logger.error("Pagination response missing data array", { pageData });
break;
}
const pageRecords = pageData.data;
// Zero progress detection - prevent infinite loops from bad API responses
if (pageRecords.length === 0) {
logger.debug("No more records returned, stopping pagination");
hasMore = false;
break;
}
// Capture length before appending to check limit correctly
const prevAllLen = allRecords.length;
// Check if adding all pageRecords would exceed limit - truncate if needed
if (prevAllLen + pageRecords.length > limit) {
logger.debug("Truncating final page to respect limit");
const remainingSlots = limit - prevAllLen;
allRecords = [...allRecords, ...pageRecords.slice(0, remainingSlots)];
// If we had to truncate, there are definitely more records available
hasMore = true;
break;
}
// Add the new records to our results
allRecords = [...allRecords, ...pageRecords];
// Validate pagination progress to prevent infinite loops
const previousSkip = currentSkip;
currentSkip += pageRecords.length;
// Anti-bypass check: ensure offset actually advanced
if (currentSkip <= previousSkip) {
logger.error("Pagination offset did not advance - potential infinite loop", {
previousSkip,
currentSkip,
recordsReceived: pageRecords.length,
});
break;
}
hasMore =
pageRecords.length === body.options.top && allRecords.length < limit;
logger.debug("Fetched additional records", {
newRecords: pageRecords.length,
totalRecords: allRecords.length,
limit,
currentSkip,
hasMore,
});
}
// Update metadata for the complete result set
metadata.numRecords = allRecords.length;
}
logger.info(`Retrieved ${allRecords.length} records from table`, {
tableId: table_id,
hasMore,
});
return {
records: allRecords,
totalRecords: allRecords.length,
hasMore,
metadata,
};
}
}
exports.QueryRecordsTool = QueryRecordsTool;
//# sourceMappingURL=query_records.js.map