chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { mongodbDeleteContract } from '@/lib/api/contracts/tools/databases/mongodb'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import {
|
||||
createMongoDBConnection,
|
||||
sanitizeCollectionName,
|
||||
validateFilter,
|
||||
} from '@/app/api/tools/mongodb/utils'
|
||||
|
||||
const logger = createLogger('MongoDBDeleteAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB delete attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseToolRequest(mongodbDeleteContract, request, { logger })
|
||||
if (!parsed.success) return parsed.response
|
||||
const params = parsed.data.body
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Deleting document(s) from ${params.host}:${params.port}/${params.database}.${params.collection} (multi: ${params.multi})`
|
||||
)
|
||||
|
||||
const sanitizedCollection = sanitizeCollectionName(params.collection)
|
||||
|
||||
const filterValidation = validateFilter(params.filter)
|
||||
if (!filterValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Filter validation failed: ${filterValidation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Filter validation failed: ${filterValidation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let filterDoc
|
||||
try {
|
||||
filterDoc = JSON.parse(params.filter)
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Invalid filter JSON: ${params.filter}`)
|
||||
return NextResponse.json({ error: 'Invalid JSON format in filter' }, { status: 400 })
|
||||
}
|
||||
|
||||
client = await createMongoDBConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
authSource: params.authSource,
|
||||
ssl: params.ssl,
|
||||
})
|
||||
|
||||
const db = client.db(params.database)
|
||||
const coll = db.collection(sanitizedCollection)
|
||||
|
||||
let result
|
||||
if (params.multi) {
|
||||
result = await coll.deleteMany(filterDoc)
|
||||
} else {
|
||||
result = await coll.deleteOne(filterDoc)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Delete completed: ${result.deletedCount} documents deleted`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `${result.deletedCount} documents deleted`,
|
||||
deletedCount: result.deletedCount,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
|
||||
logger.error(`[${requestId}] MongoDB delete failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `MongoDB delete failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { mongodbExecuteContract } from '@/lib/api/contracts/tools/databases/mongodb'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import {
|
||||
createMongoDBConnection,
|
||||
sanitizeCollectionName,
|
||||
validatePipeline,
|
||||
} from '@/app/api/tools/mongodb/utils'
|
||||
|
||||
const logger = createLogger('MongoDBExecuteAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB execute attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseToolRequest(mongodbExecuteContract, request, { logger })
|
||||
if (!parsed.success) return parsed.response
|
||||
const params = parsed.data.body
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing aggregation pipeline on ${params.host}:${params.port}/${params.database}.${params.collection}`
|
||||
)
|
||||
|
||||
const sanitizedCollection = sanitizeCollectionName(params.collection)
|
||||
|
||||
const pipelineValidation = validatePipeline(params.pipeline)
|
||||
if (!pipelineValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Pipeline validation failed: ${pipelineValidation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Pipeline validation failed: ${pipelineValidation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const pipelineDoc = JSON.parse(params.pipeline)
|
||||
|
||||
client = await createMongoDBConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
authSource: params.authSource,
|
||||
ssl: params.ssl,
|
||||
})
|
||||
|
||||
const db = client.db(params.database)
|
||||
const coll = db.collection(sanitizedCollection)
|
||||
|
||||
const cursor = coll.aggregate(pipelineDoc)
|
||||
const documents = await cursor.toArray()
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Aggregation completed successfully, returned ${documents.length} documents`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Aggregation completed, returned ${documents.length} documents`,
|
||||
documents,
|
||||
documentCount: documents.length,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
|
||||
logger.error(`[${requestId}] MongoDB aggregation failed:`, error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `MongoDB aggregation failed: ${errorMessage}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { mongodbInsertContract } from '@/lib/api/contracts/tools/databases/mongodb'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createMongoDBConnection, sanitizeCollectionName } from '@/app/api/tools/mongodb/utils'
|
||||
|
||||
const logger = createLogger('MongoDBInsertAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB insert attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseToolRequest(mongodbInsertContract, request, { logger })
|
||||
if (!parsed.success) return parsed.response
|
||||
const params = parsed.data.body
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Inserting ${params.documents.length} document(s) into ${params.host}:${params.port}/${params.database}.${params.collection}`
|
||||
)
|
||||
|
||||
const sanitizedCollection = sanitizeCollectionName(params.collection)
|
||||
client = await createMongoDBConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
authSource: params.authSource,
|
||||
ssl: params.ssl,
|
||||
})
|
||||
|
||||
const db = client.db(params.database)
|
||||
const coll = db.collection(sanitizedCollection)
|
||||
|
||||
let result
|
||||
if (params.documents.length === 1) {
|
||||
result = await coll.insertOne(params.documents[0] as Record<string, unknown>)
|
||||
logger.info(`[${requestId}] Single document inserted successfully`)
|
||||
return NextResponse.json({
|
||||
message: 'Document inserted successfully',
|
||||
insertedId: result.insertedId.toString(),
|
||||
documentCount: 1,
|
||||
})
|
||||
}
|
||||
result = await coll.insertMany(params.documents as Record<string, unknown>[])
|
||||
const insertedCount = Object.keys(result.insertedIds).length
|
||||
logger.info(`[${requestId}] ${insertedCount} documents inserted successfully`)
|
||||
return NextResponse.json({
|
||||
message: `${insertedCount} documents inserted successfully`,
|
||||
insertedIds: Object.values(result.insertedIds).map((id) => id.toString()),
|
||||
documentCount: insertedCount,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
|
||||
logger.error(`[${requestId}] MongoDB insert failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `MongoDB insert failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { mongodbIntrospectContract } from '@/lib/api/contracts/tools/databases/mongodb'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createMongoDBConnection, executeIntrospect } from '@/app/api/tools/mongodb/utils'
|
||||
|
||||
const logger = createLogger('MongoDBIntrospectAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB introspect attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseToolRequest(mongodbIntrospectContract, request, { logger })
|
||||
if (!parsed.success) return parsed.response
|
||||
const params = parsed.data.body
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Introspecting MongoDB at ${params.host}:${params.port}${params.database ? `/${params.database}` : ''}`
|
||||
)
|
||||
|
||||
client = await createMongoDBConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database || 'admin',
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
authSource: params.authSource,
|
||||
ssl: params.ssl,
|
||||
})
|
||||
|
||||
const result = await executeIntrospect(client, params.database)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Introspection completed: ${result.databases.length} databases, ${result.collections.length} collections`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: result.message,
|
||||
databases: result.databases,
|
||||
collections: result.collections,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
|
||||
logger.error(`[${requestId}] MongoDB introspect failed:`, error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `MongoDB introspect failed: ${errorMessage}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { mongodbQueryContract } from '@/lib/api/contracts/tools/databases/mongodb'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import {
|
||||
createMongoDBConnection,
|
||||
sanitizeCollectionName,
|
||||
validateFilter,
|
||||
} from '@/app/api/tools/mongodb/utils'
|
||||
|
||||
const logger = createLogger('MongoDBQueryAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB query attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseToolRequest(mongodbQueryContract, request, { logger })
|
||||
if (!parsed.success) return parsed.response
|
||||
const params = parsed.data.body
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing MongoDB query on ${params.host}:${params.port}/${params.database}.${params.collection}`
|
||||
)
|
||||
|
||||
const sanitizedCollection = sanitizeCollectionName(params.collection)
|
||||
|
||||
let filter = {}
|
||||
if (params.query?.trim()) {
|
||||
const validation = validateFilter(params.query)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Filter validation failed: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Filter validation failed: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
filter = JSON.parse(params.query)
|
||||
}
|
||||
|
||||
let sortCriteria = {}
|
||||
if (params.sort?.trim()) {
|
||||
try {
|
||||
sortCriteria = JSON.parse(params.sort)
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Invalid sort JSON: ${params.sort}`)
|
||||
return NextResponse.json({ error: 'Invalid JSON format in sort criteria' }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
client = await createMongoDBConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
authSource: params.authSource,
|
||||
ssl: params.ssl,
|
||||
})
|
||||
|
||||
const db = client.db(params.database)
|
||||
const coll = db.collection(sanitizedCollection)
|
||||
|
||||
let cursor = coll.find(filter)
|
||||
|
||||
if (Object.keys(sortCriteria).length > 0) {
|
||||
cursor = cursor.sort(sortCriteria)
|
||||
}
|
||||
|
||||
const limit = params.limit || 100
|
||||
cursor = cursor.limit(limit)
|
||||
|
||||
const documents = await cursor.toArray()
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Query executed successfully, returned ${documents.length} documents`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Found ${documents.length} documents`,
|
||||
documents,
|
||||
documentCount: documents.length,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
|
||||
logger.error(`[${requestId}] MongoDB query failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `MongoDB query failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { mongodbUpdateContract } from '@/lib/api/contracts/tools/databases/mongodb'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import {
|
||||
createMongoDBConnection,
|
||||
sanitizeCollectionName,
|
||||
validateFilter,
|
||||
} from '@/app/api/tools/mongodb/utils'
|
||||
|
||||
const logger = createLogger('MongoDBUpdateAPI')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
let client = null
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized MongoDB update attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await parseToolRequest(mongodbUpdateContract, request, { logger })
|
||||
if (!parsed.success) return parsed.response
|
||||
const params = parsed.data.body
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Updating document(s) in ${params.host}:${params.port}/${params.database}.${params.collection} (multi: ${params.multi}, upsert: ${params.upsert})`
|
||||
)
|
||||
|
||||
const sanitizedCollection = sanitizeCollectionName(params.collection)
|
||||
|
||||
const filterValidation = validateFilter(params.filter)
|
||||
if (!filterValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Filter validation failed: ${filterValidation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Filter validation failed: ${filterValidation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let filterDoc
|
||||
let updateDoc
|
||||
try {
|
||||
filterDoc = JSON.parse(params.filter)
|
||||
updateDoc = JSON.parse(params.update)
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Invalid JSON in filter or update`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid JSON format in filter or update' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
client = await createMongoDBConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
authSource: params.authSource,
|
||||
ssl: params.ssl,
|
||||
})
|
||||
|
||||
const db = client.db(params.database)
|
||||
const coll = db.collection(sanitizedCollection)
|
||||
|
||||
let result
|
||||
if (params.multi) {
|
||||
result = await coll.updateMany(filterDoc, updateDoc, { upsert: params.upsert })
|
||||
} else {
|
||||
result = await coll.updateOne(filterDoc, updateDoc, { upsert: params.upsert })
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Update completed: ${result.modifiedCount} modified, ${result.matchedCount} matched${result.upsertedCount ? `, ${result.upsertedCount} upserted` : ''}`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `${result.modifiedCount} documents updated${result.upsertedCount ? `, ${result.upsertedCount} documents upserted` : ''}`,
|
||||
matchedCount: result.matchedCount,
|
||||
modifiedCount: result.modifiedCount,
|
||||
documentCount: result.modifiedCount + (result.upsertedCount || 0),
|
||||
...(result.upsertedId && { insertedId: result.upsertedId.toString() }),
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
|
||||
logger.error(`[${requestId}] MongoDB update failed:`, error)
|
||||
|
||||
return NextResponse.json({ error: `MongoDB update failed: ${errorMessage}` }, { status: 500 })
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,197 @@
|
||||
import { MongoClient } from 'mongodb'
|
||||
import {
|
||||
createPinnedLookup,
|
||||
validateDatabaseHost,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import type { MongoDBCollectionInfo, MongoDBConnectionConfig } from '@/tools/mongodb/types'
|
||||
|
||||
export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
|
||||
const hostValidation = await validateDatabaseHost(config.host, 'host')
|
||||
if (!hostValidation.isValid) {
|
||||
throw new Error(hostValidation.error)
|
||||
}
|
||||
|
||||
const credentials =
|
||||
config.username && config.password
|
||||
? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
|
||||
: ''
|
||||
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (config.authSource) {
|
||||
queryParams.append('authSource', config.authSource)
|
||||
}
|
||||
|
||||
if (config.ssl === 'required') {
|
||||
queryParams.append('ssl', 'true')
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const uri = `mongodb://${credentials}${config.host}:${config.port}/${config.database}${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const client = new MongoClient(uri, {
|
||||
connectTimeoutMS: 10000,
|
||||
socketTimeoutMS: 10000,
|
||||
maxPoolSize: 1,
|
||||
lookup: createPinnedLookup(hostValidation.resolvedIP ?? config.host),
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
return client
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively checks an object for dangerous MongoDB operators
|
||||
* @param obj - The object to check
|
||||
* @param dangerousOperators - Array of operator names to block
|
||||
* @returns true if a dangerous operator is found
|
||||
*/
|
||||
function containsDangerousOperator(obj: unknown, dangerousOperators: string[]): boolean {
|
||||
if (typeof obj !== 'object' || obj === null) return false
|
||||
|
||||
for (const key of Object.keys(obj as Record<string, unknown>)) {
|
||||
if (dangerousOperators.includes(key)) return true
|
||||
if (
|
||||
typeof (obj as Record<string, unknown>)[key] === 'object' &&
|
||||
containsDangerousOperator((obj as Record<string, unknown>)[key], dangerousOperators)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function validateFilter(filter: string): { isValid: boolean; error?: string } {
|
||||
try {
|
||||
const parsed = JSON.parse(filter)
|
||||
|
||||
const dangerousOperators = [
|
||||
'$where', // Executes arbitrary JavaScript
|
||||
'$regex', // Can cause ReDoS attacks
|
||||
'$expr', // Expression evaluation
|
||||
'$function', // Custom JavaScript functions
|
||||
'$accumulator', // Custom JavaScript accumulators
|
||||
'$let', // Variable definitions that could be exploited
|
||||
]
|
||||
|
||||
if (containsDangerousOperator(parsed, dangerousOperators)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Filter contains potentially dangerous operators',
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Invalid JSON format in filter',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePipeline(pipeline: string): { isValid: boolean; error?: string } {
|
||||
try {
|
||||
const parsed = JSON.parse(pipeline)
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Pipeline must be an array',
|
||||
}
|
||||
}
|
||||
|
||||
const dangerousOperators = [
|
||||
'$where', // Executes arbitrary JavaScript
|
||||
'$function', // Custom JavaScript functions
|
||||
'$accumulator', // Custom JavaScript accumulators
|
||||
'$let', // Variable definitions that could be exploited
|
||||
'$merge', // Writes to external collections
|
||||
'$out', // Writes to external collections
|
||||
'$currentOp', // Exposes system operation info
|
||||
'$listSessions', // Exposes session info
|
||||
'$listLocalSessions', // Exposes local session info
|
||||
]
|
||||
|
||||
for (const stage of parsed) {
|
||||
if (containsDangerousOperator(stage, dangerousOperators)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Pipeline contains potentially dangerous operators',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Invalid JSON format in pipeline',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeCollectionName(name: string): string {
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
||||
throw new Error(
|
||||
'Invalid collection name. Must start with letter or underscore and contain only letters, numbers, and underscores.'
|
||||
)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
/**
|
||||
* Introspect MongoDB to get databases, collections, and indexes
|
||||
*/
|
||||
export async function executeIntrospect(
|
||||
client: MongoClient,
|
||||
database?: string
|
||||
): Promise<{
|
||||
message: string
|
||||
databases: string[]
|
||||
collections: MongoDBCollectionInfo[]
|
||||
}> {
|
||||
const databases: string[] = []
|
||||
const collections: MongoDBCollectionInfo[] = []
|
||||
|
||||
if (database) {
|
||||
databases.push(database)
|
||||
const db = client.db(database)
|
||||
const collectionList = await db.listCollections().toArray()
|
||||
|
||||
for (const collInfo of collectionList) {
|
||||
const coll = db.collection(collInfo.name)
|
||||
const indexes = await coll.indexes()
|
||||
const documentCount = await coll.estimatedDocumentCount()
|
||||
|
||||
collections.push({
|
||||
name: collInfo.name,
|
||||
type: collInfo.type || 'collection',
|
||||
documentCount,
|
||||
indexes: indexes.map((idx) => ({
|
||||
name: idx.name || '',
|
||||
key: idx.key as Record<string, number>,
|
||||
unique: idx.unique || false,
|
||||
sparse: idx.sparse,
|
||||
})),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const admin = client.db().admin()
|
||||
const dbList = await admin.listDatabases()
|
||||
|
||||
for (const dbInfo of dbList.databases) {
|
||||
databases.push(dbInfo.name)
|
||||
}
|
||||
}
|
||||
|
||||
const message = database
|
||||
? `Found ${collections.length} collections in database '${database}'`
|
||||
: `Found ${databases.length} databases`
|
||||
|
||||
return {
|
||||
message,
|
||||
databases,
|
||||
collections,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user