UNPKG

mongoku

Version:

[![CI](https://github.com/huggingface/Mongoku/actions/workflows/ci.yml/badge.svg)](https://github.com/huggingface/Mongoku/actions/workflows/ci.yml)

1,102 lines (973 loc) 29.6 kB
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)}`, }; } }, );