mongoku
Version:
[](https://github.com/huggingface/Mongoku/actions/workflows/ci.yml)
1,102 lines (973 loc) • 29.6 kB
text/typescript
import { command, query } from "$app/server";
import { env } from "$env/dynamic/private";
import { validateAggregationPipeline } from "$lib/server/aggregation";
import JsonEncoder from "$lib/server/JsonEncoder";
import { logger } from "$lib/server/logger";
import { getMongo } from "$lib/server/mongo";
import { isEmptyObject } from "$lib/utils/isEmptyObject";
import { parseJSON } from "$lib/utils/jsonParser";
import { auditSchemaCompliance } from "$lib/server/schema";
import { error } from "@sveltejs/kit";
import { ObjectId, ReadPreference, type Document } from "mongodb";
import { z } from "zod";
function checkReadOnly() {
if (env.MONGOKU_READ_ONLY_MODE === "true") {
error(403, "Read-only mode is enabled");
}
}
// Sanitize MongoDB connection string by removing credentials
function sanitizeMongoUrl(url: string): string {
try {
const urlObj = new URL(url.startsWith("mongodb") ? url : `mongodb://${url}`);
if (urlObj.username || urlObj.password) {
urlObj.username = "***";
urlObj.password = "***";
}
return urlObj.toString();
} catch {
// If URL parsing fails, just mask the whole thing
return "***";
}
}
// Add a new server
export const addServer = command(
z.object({
url: z.string(),
}),
async ({ url }) => {
logger.log("addServer called with payload:", { url: sanitizeMongoUrl(url) });
const mongo = await getMongo();
await mongo.addServer(url);
return { ok: true };
},
);
// Remove a server
export const removeServer = command(z.string(), async (serverName) => {
logger.log("removeServer called with payload:", { serverName });
const mongo = await getMongo();
await mongo.removeServer(serverName);
return { ok: true };
});
// Update a document
export const updateDocument = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
document: z.string(),
value: z.unknown(),
partial: z.boolean().optional().default(false),
upsert: z.boolean().optional().default(false),
}),
async ({ server, database, collection, document, value, partial, upsert }) => {
logger.log("updateDocument called with payload:", { server, database, collection, document, partial, upsert });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
const newValue = JsonEncoder.decode(value);
// TODO: For now it makes it impossible to remove fields from object with a projection
// Todo: handle multiple ID types
const _id = /^[0-9a-fA-F]{24}$/.test(document) ? new ObjectId(document) : document;
if (partial) {
await coll.updateOne(
{
_id: _id as unknown as ObjectId,
},
{ $set: newValue },
{ upsert: upsert },
);
} else {
await coll.replaceOne(
{
_id: _id as unknown as ObjectId,
},
JsonEncoder.decode(newValue),
{ upsert: upsert },
);
}
if (collection === "mongoku.mappings") {
client.clearMappingsCache(database, document);
}
return {
ok: true,
update: JsonEncoder.encode(newValue),
};
},
);
// Insert a document
export const insertDocument = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
document: z.string().nullable().optional(),
value: z.unknown(),
}),
async ({ server, database, collection, document, value }) => {
logger.log("insertDocument called with payload:", { server, database, collection, document });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
const newValue = JsonEncoder.decode(value);
// If document is provided, use it as _id; otherwise let insertOne generate it automatically
if (document !== null && document !== undefined) {
const _id = /^[0-9a-fA-F]{24}$/.test(document) ? new ObjectId(document) : document;
newValue._id = _id;
}
const res = await coll.insertOne(newValue);
if (collection === "mongoku.mappings" && typeof (res.insertedId as unknown) === "string") {
client.clearMappingsCache(database, res.insertedId as unknown as string);
}
return {
ok: true,
insert: JsonEncoder.encode(newValue),
};
},
);
// Delete a document
export const deleteDocument = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
document: z.string(),
}),
async ({ server, database, collection, document }) => {
logger.log("deleteDocument called with payload:", { server, database, collection, document });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
const _id = /^[0-9a-fA-F]{24}$/.test(document) ? new ObjectId(document) : document;
await client
.db(database)
.collection(collection)
.deleteOne({
_id: _id as unknown as ObjectId,
});
if (collection === "mongoku.mappings") {
client.clearMappingsCache(database, document);
}
return {
ok: true,
};
},
);
// Update multiple documents with an arbitrary update query
export const updateMany = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
filter: z.string(),
update: z.string(),
}),
async ({ server, database, collection, filter, update }) => {
logger.log("updateMany called with payload:", { server, database, collection, filter, update });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
const filterDoc = JsonEncoder.decode(parseJSON(filter));
const updateDoc = JsonEncoder.decode(parseJSON(update, { allowArray: true }));
const result = await coll.updateMany(filterDoc, updateDoc);
return {
ok: true,
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
};
},
);
// Count documents matching a filter - uses `command` to avoid URL length limits
export const countDocuments = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
filter: z.string(),
}),
async ({ server, database, collection, filter }) => {
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
const filterDoc = JsonEncoder.decode(parseJSON(filter));
try {
const count = await coll.countDocuments(filterDoc, {
maxTimeMS: mongo.getCountTimeout(),
});
return {
data: count,
error: null,
};
} catch (err) {
logger.error("Error counting documents:", err);
return {
data: 0,
error: `Failed to count documents: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
);
// Delete multiple documents with a filter query
export const deleteMany = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
filter: z.string(),
}),
async ({ server, database, collection, filter }) => {
logger.log("deleteMany called with payload:", { server, database, collection, filter });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
const filterDoc = JsonEncoder.decode(parseJSON(filter));
const result = await coll.deleteMany(filterDoc);
return {
ok: true,
deletedCount: result.deletedCount,
};
},
);
// Hide an index
export const hideIndex = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
index: z.string(),
}),
async ({ server, database, collection, index }) => {
logger.log("hideIndex called with payload:", { server, database, collection, index });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
await client.db(database).command({
collMod: collection,
index: {
name: index,
hidden: true,
},
});
return {
ok: true,
};
},
);
// Unhide an index
export const unhideIndex = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
index: z.string(),
}),
async ({ server, database, collection, index }) => {
logger.log("unhideIndex called with payload:", { server, database, collection, index });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
await client.db(database).command({
collMod: collection,
index: {
name: index,
hidden: false,
},
});
return {
ok: true,
};
},
);
// Create an index
export const createIndex = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
keys: z.string(),
name: z.string().optional(),
unique: z.boolean().optional(),
sparse: z.boolean().optional(),
partialFilterExpression: z.string().optional(),
expireAfterSeconds: z.number().optional(),
background: z.boolean().optional(),
}),
async ({
server,
database,
collection,
keys,
name,
unique,
sparse,
partialFilterExpression,
expireAfterSeconds,
background,
}) => {
logger.log("createIndex called with payload:", { server, database, collection, keys, name, unique, sparse });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
// Parse keys JSON
const keysDoc = JsonEncoder.decode(parseJSON(keys));
// Build options object
const options: Record<string, unknown> = {};
if (name) {
options.name = name;
}
if (unique) {
options.unique = unique;
}
if (sparse) {
options.sparse = sparse;
}
if (partialFilterExpression) {
options.partialFilterExpression = JsonEncoder.decode(parseJSON(partialFilterExpression));
}
if (expireAfterSeconds !== undefined) {
options.expireAfterSeconds = expireAfterSeconds;
}
if (background) {
options.background = background;
}
await coll.createIndex(keysDoc, options);
return {
ok: true,
};
},
);
// Drop an index
export const dropIndex = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
index: z.string(),
}),
async ({ server, database, collection, index }) => {
logger.log("dropIndex called with payload:", { server, database, collection, index });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
await client.db(database).command({
dropIndexes: collection,
index: index,
});
return {
ok: true,
};
},
);
// Drop a collection
export const dropCollection = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
}),
async ({ server, database, collection }) => {
logger.log("dropCollection called with payload:", { server, database, collection });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
const db = client.db(database);
await db.dropCollection(collection);
return {
ok: true,
};
},
);
// Drop a database
export const dropDatabase = command(
z.object({
server: z.string(),
database: z.string(),
}),
async ({ server, database }) => {
logger.log("dropDatabase called with payload:", { server, database });
checkReadOnly();
const mongo = await getMongo();
const client = mongo.getClient(server);
await client.db(database).dropDatabase();
return {
ok: true,
};
},
);
// Retry connection to a server
export const retryConnection = command(z.string(), async (serverId) => {
logger.log("retryConnection called with payload:", { serverId });
const mongo = await getMongo();
// Reconnect the client (closes old connection and creates a new one)
await mongo.reconnectClient(serverId);
return { ok: true };
});
// Load documents from a collection - uses `command` to avoid URL length limits
export const loadDocuments = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
query: z.string().default("{}"),
sort: z.string().default("{}"),
project: z.string().default("{}"),
skip: z.number().int().default(0),
limit: z.number().int().default(20),
mode: z.enum(["query", "distinct", "aggregation"]).default("query"),
field: z.string().optional(),
}),
async ({ server, database, collection, query: queryStr, sort, project, skip, limit, mode, field }) => {
// Parse JSON strings - return error if invalid
let queryDoc: unknown;
try {
queryDoc = parseJSON(queryStr, { allowArray: true });
} catch (err) {
error(400, `Invalid query: ${err}`);
}
try {
parseJSON(sort);
} catch (err) {
error(400, `Invalid sort: ${err}`);
}
try {
parseJSON(project);
} catch (err) {
error(400, `Invalid project: ${err}`);
}
const sortDoc = parseJSON(sort);
const projectDoc = parseJSON(project) as Document;
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
// Handle distinct mode
if (mode === "distinct") {
if (!field) {
error(400, "Invalid distinct query: field name is required");
}
try {
const results = await coll.distinct(field, JsonEncoder.decode(queryDoc), {
maxTimeMS: mongo.getQueryTimeout(),
});
return {
data: results.map((value) => JsonEncoder.encode({ value })),
error: null,
isAggregation: false,
isDistinct: true,
};
} catch (err) {
logger.error("Error executing distinct:", err);
error(500, `Failed to execute distinct: ${err instanceof Error ? err.message : String(err)}`);
}
}
// Handle aggregation mode
if (mode === "aggregation" && Array.isArray(queryDoc)) {
try {
validateAggregationPipeline(queryDoc);
} catch (err) {
error(400, `Invalid aggregation pipeline: ${err instanceof Error ? err.message : String(err)}`);
}
// Execute aggregation
const pipeline = JsonEncoder.decode(queryDoc);
try {
const results = await coll
.aggregate(
[
...pipeline,
...(isEmptyObject(projectDoc) ? [] : [{ $project: projectDoc }]),
...(isEmptyObject(sortDoc as object) ? [] : [{ $sort: sortDoc }]),
{ $limit: limit },
{ $skip: skip },
],
{
maxTimeMS: mongo.getQueryTimeout(),
},
)
.map((obj) => JsonEncoder.encode(obj))
.toArray();
return {
data: results,
error: null,
isAggregation: true,
isDistinct: false,
};
} catch (err) {
logger.error("Error executing aggregation:", err);
error(500, `Failed to execute aggregation: ${err instanceof Error ? err.message : String(err)}`);
}
}
// Execute regular find query
try {
const results = await coll
.find(JsonEncoder.decode(queryDoc), { maxTimeMS: mongo.getQueryTimeout() })
.project(projectDoc)
.sort(JsonEncoder.decode(sortDoc))
.limit(limit)
.skip(skip)
.map((obj) => JsonEncoder.encode(obj))
.toArray();
return {
data: results,
error: null,
isAggregation: false,
isDistinct: false,
};
} catch (err) {
logger.error("Error fetching query results:", err);
error(500, `Failed to fetch query results: ${err instanceof Error ? err.message : String(err)}`);
}
},
);
// Fetch a document by field value (for mappings)
// Tries multiple mapping targets and returns the first one that finds a document
// Only handles document mappings; URL mappings are handled client-side
export const fetchMappedDocument = query(
z.object({
server: z.string(),
database: z.string(),
mappings: z.array(
z.union([
z.object({
type: z.literal("document"),
collection: z.string(),
on: z.string(),
}),
z.object({
type: z.literal("url"),
template: z.string(),
}),
// Legacy format (backwards compatibility)
z.object({
collection: z.string(),
on: z.string(),
}),
]),
),
value: z.unknown(),
}),
async ({ server, database, mappings, value }) => {
const mongo = await getMongo();
const client = mongo.getClient(server);
const decodedValue = JsonEncoder.decode(value);
// Filter to only document mappings (URL mappings are handled client-side)
const documentMappings = mappings.filter((m) => {
if ("type" in m) {
return m.type === "document";
}
// Legacy format without type field is a document mapping
return "collection" in m && "on" in m;
});
// Try each document mapping in order and return the first match
for (const mapping of documentMappings) {
try {
// Type guard for TypeScript
if ("type" in mapping && mapping.type !== "document") {
continue;
}
const collection = "collection" in mapping ? mapping.collection : "";
const on = "on" in mapping ? mapping.on : "_id";
const coll = client.db(database).collection(collection);
const query = { [on]: decodedValue };
const document = await coll.findOne(query, { maxTimeMS: mongo.getQueryTimeout() });
if (document) {
return {
data: JsonEncoder.encode(document),
collection: collection,
error: null,
};
}
} catch (err) {
const collection = "collection" in mapping ? mapping.collection : "unknown";
const on = "on" in mapping ? mapping.on : "_id";
logger.error(`Error fetching mapped document from ${collection}.${on}:`, err);
// Continue to next mapping on error
continue;
}
}
// No mapping found a document
return {
data: null,
collection: null,
error: "Document not found in any mapped collection",
};
},
);
// Fetch index stats with read preference
export const getIndexStatsWithReadPreference = query(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
readPreferenceMode: z.string().optional(),
readPreferenceTags: z.string().optional(),
}),
async ({ server, database, collection, readPreferenceMode, readPreferenceTags }) => {
const mongo = await getMongo();
const client = mongo.getClient(server);
try {
// Parse tags if provided
let tags;
if (readPreferenceTags) {
try {
tags = parseJSON(readPreferenceTags);
// Ensure tags is an object or array of objects
if (typeof tags !== "object" || tags === null) {
error(400, "Invalid read preference tags format");
}
// If it's a single object, wrap it in an array
if (!Array.isArray(tags)) {
tags = [tags];
}
} catch (err) {
error(400, `Invalid read preference tags JSON: ${err}`);
}
}
// Build read preference
let readPreference;
if (readPreferenceMode || tags) {
const mode = (readPreferenceMode || "nearest") as
| "primary"
| "primaryPreferred"
| "secondary"
| "secondaryPreferred"
| "nearest";
readPreference = tags ? new ReadPreference(mode, tags) : new ReadPreference(mode);
}
const coll = client.db(database).collection(collection);
// Get index usage statistics with read preference
const aggregateOptions = readPreference ? { readPreference } : {};
const statsResult = await coll.aggregate([{ $indexStats: {} }], aggregateOptions).toArray();
const indexStats = Object.fromEntries(
statsResult.map((stat) => [
stat.name,
{
ops: stat.accesses?.ops || 0,
since: stat.accesses?.since || new Date(),
host: stat.host || "unknown",
},
]),
);
return {
data: JsonEncoder.encode(indexStats),
error: null,
};
} catch (err) {
logger.error("Error fetching index stats with read preference:", err);
return {
data: {},
error: `Failed to fetch index stats: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
);
// Get list of nodes from a server's connection string
export const getServerNodes = query(
z.object({
server: z.string(),
}),
async ({ server }) => {
const mongo = await getMongo();
try {
const nodes = await mongo.getServerNodes(server);
return {
data: nodes,
error: null,
};
} catch (err) {
logger.error("Error getting server nodes:", err);
return {
data: [],
error: `Failed to get server nodes: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
);
// Detect which node is the primary
export const detectPrimaryNode = query(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
}),
async ({ server, database, collection }) => {
const mongo = await getMongo();
const client = mongo.getClient(server);
try {
// Run indexStats with primary read preference
const coll = client.db(database).collection(collection);
const statsResult = await coll
.aggregate([{ $indexStats: {} }], { readPreference: new ReadPreference("primary") })
.toArray();
// Extract the host from the first result (will be the primary)
const primaryHost = statsResult.length > 0 ? statsResult[0].host : null;
return {
data: primaryHost,
error: null,
};
} catch (err) {
logger.error("Error detecting primary node:", err);
return {
data: null,
error: `Failed to detect primary: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
);
// Fetch index stats from specific nodes using direct connections
export const getIndexStatsFromNodes = query(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
nodes: z.array(z.string()),
}),
async ({ server, database, collection, nodes }) => {
const mongo = await getMongo();
try {
const results = await Promise.allSettled(
nodes.map(async (node) => {
const stats = await mongo.getIndexStatsFromNode(server, node, database, collection);
return { node, stats };
}),
);
// Merge results from all nodes
const mergedStats: Record<string, { ops: number; since: Date; host: string }> = {};
const errors: string[] = [];
let i = 0;
for (const result of results) {
if (result.status === "fulfilled") {
const { stats } = result.value;
for (const [indexName, indexStats] of Object.entries(stats)) {
const key = `${indexName}::${indexStats.host}`;
mergedStats[key] = indexStats;
}
} else {
logger.error(`Error fetching index stats from node ${nodes[i]}:`, result.reason);
errors.push(result.reason?.message || String(result.reason));
}
i++;
}
return {
data: JsonEncoder.encode(mergedStats),
error: errors.length > 0 ? errors.join("; ") : null,
};
} catch (err) {
logger.error("Error fetching index stats from nodes:", err);
return {
data: {},
error: `Failed to fetch index stats: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
);
// Explain a query and return execution statistics
export const explainQuery = query(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
query: z.string().default("{}"),
sort: z.string().default("{}"),
project: z.string().default("{}"),
skip: z.number().int().default(0),
limit: z.number().int().default(20),
mode: z.enum(["query", "aggregation"]).default("query"),
verbosity: z.enum(["queryPlanner", "executionStats", "allPlansExecution"]).default("executionStats"),
}),
async ({ server, database, collection, query: queryStr, sort, project, skip, limit, mode, verbosity }) => {
// Parse JSON strings
const queryDoc = (() => {
try {
return parseJSON(queryStr, { allowArray: true });
} catch (err) {
error(400, `Invalid query: ${err}`);
}
})();
try {
parseJSON(sort);
} catch (err) {
error(400, `Invalid sort: ${err}`);
}
try {
parseJSON(project);
} catch (err) {
error(400, `Invalid project: ${err}`);
}
const sortDoc = parseJSON(sort);
const projectDoc = parseJSON(project) as Document;
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
try {
let explainResult;
if (mode === "aggregation" && Array.isArray(queryDoc)) {
// Validate aggregation pipeline
try {
validateAggregationPipeline(queryDoc);
} catch (err) {
error(400, `Invalid aggregation pipeline: ${err instanceof Error ? err.message : String(err)}`);
}
// Build the full pipeline with project, sort, limit, skip
const pipeline = JsonEncoder.decode(queryDoc);
const fullPipeline = [
...pipeline,
...(isEmptyObject(projectDoc) ? [] : [{ $project: projectDoc }]),
...(isEmptyObject(sortDoc as object) ? [] : [{ $sort: sortDoc }]),
{ $skip: skip },
{ $limit: limit },
];
// Aggregation explain
explainResult = await coll.aggregate(fullPipeline).explain(verbosity);
} else {
// Find explain
explainResult = await coll
.find(JsonEncoder.decode(queryDoc))
.project(projectDoc)
.sort(JsonEncoder.decode(sortDoc))
.skip(skip)
.limit(limit)
.explain(verbosity);
}
// Convert to plain object to handle MongoDB special types (Long, Timestamp, etc.)
// that JsonEncoder doesn't support
const plainResult = JSON.parse(JSON.stringify(explainResult));
return {
data: plainResult,
error: null,
};
} catch (err) {
logger.error("Error explaining query:", err);
return {
data: null,
error: `Failed to explain query: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
);
// Count documents created within a time range (based on ObjectId timestamp or createdAt field)
export const countDocumentsByTimeRange = query(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
days: z.number(),
query: z.string().optional(),
}),
async ({
server,
database,
collection,
days,
query: queryStr,
}): Promise<{ count: number | null; error: string | null }> => {
const mongo = await getMongo();
const client = mongo.getClient(server);
const coll = client.db(database).collection(collection);
// Parse the base query if provided
let baseQuery: Document = {};
if (queryStr) {
try {
const parsed = parseJSON(queryStr);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
baseQuery = JsonEncoder.encode(parsed) as Document;
}
} catch {
// If parsing fails, ignore the query
}
}
// Check if _id has a creation timestamp (is ObjectId with embedded date)
// Use createdAt field ONLY if _id doesn't have a date AND there's an index on createdAt
let useCreatedAt = false;
try {
const sample = await coll.findOne({}, { projection: { _id: 1, createdAt: 1 }, maxTimeMS: 5000 });
if (sample) {
const idIsObjectId = sample._id instanceof ObjectId;
if (!idIsObjectId) {
if (sample.createdAt instanceof Date && (await client.hasIndexOnField(database, collection, "createdAt"))) {
useCreatedAt = true;
} else {
return { count: null, error: "Cannot determine document age (no ObjectId or createdAt field)" };
}
}
}
} catch {
// If sampling fails, default to ObjectId
}
const dateThreshold = new Date();
dateThreshold.setDate(dateThreshold.getDate() - days);
// Build filter based on _id type, merged with base query
let filter: Document;
if (useCreatedAt) {
filter = { ...baseQuery, createdAt: { $gte: dateThreshold } };
} else {
const objectIdThreshold = ObjectId.createFromTime(Math.floor(dateThreshold.getTime() / 1000));
filter = { ...baseQuery, _id: { $gte: objectIdThreshold } };
}
try {
const count = await coll.countDocuments(filter, { maxTimeMS: mongo.getCountTimeout() });
return { count, error: null };
} catch (err) {
logger.error(`Error counting documents for ${days} days:`, err);
const errorMsg = err instanceof Error ? err.message : String(err);
return { count: null, error: errorMsg };
}
},
);
// Audit schema compliance for a collection. Triggered manually so command instead of query
export const auditSchema = command(
z.object({
server: z.string(),
database: z.string(),
collection: z.string(),
readPreferenceMode: z.string().optional(),
readPreferenceTags: z.string().optional(),
}),
async ({ server, database, collection, readPreferenceMode, readPreferenceTags }) => {
const mongo = await getMongo();
const client = mongo.getClient(server);
try {
// Build read preference if requested
let readPreference: ReadPreference | undefined;
if (readPreferenceMode) {
let tags: Array<Record<string, string>> | undefined;
if (readPreferenceTags) {
try {
const parsed = parseJSON(readPreferenceTags, { allowArray: true });
if (Array.isArray(parsed)) {
tags = parsed as Array<Record<string, string>>;
} else if (parsed && typeof parsed === "object") {
tags = [parsed as Record<string, string>];
}
} catch {
// Ignore invalid tags
}
}
readPreference = tags
? new ReadPreference(
readPreferenceMode as "primary" | "primaryPreferred" | "secondary" | "secondaryPreferred" | "nearest",
tags,
)
: new ReadPreference(
readPreferenceMode as "primary" | "primaryPreferred" | "secondary" | "secondaryPreferred" | "nearest",
);
}
const result = await auditSchemaCompliance(client, database, collection, {
readPreference,
maxTimeMS: mongo.getQueryTimeout(),
});
return {
data: JsonEncoder.encode(result),
error: null,
};
} catch (err) {
logger.error("Error auditing schema compliance:", err);
return {
data: null,
error: `Failed to audit schema: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
);