mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[chat] migrate to express with strict zod schema (#35742)
GitOrigin-RevId: edffacde0c069768def849f45364d519d9f76f1e
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
function asZodError(...def) {
|
||||
return {
|
||||
name: 'ZodError',
|
||||
@@ -5,4 +7,82 @@ function asZodError(...def) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { asZodError }
|
||||
/**
|
||||
* Parses (if needed) a validation-error response body and asserts that it
|
||||
* matches the given status and names the given field.
|
||||
*
|
||||
* handleValidationError (see ./handleValidationError.js) responds with
|
||||
* `{ error: fromError(err.zodError).toString(), statusCode }`, and
|
||||
* fromError's message includes the offending field's dotted schema path
|
||||
* (e.g. `at "params.projectId"`, or the failing key name for an
|
||||
* unrecognized key), so asserting the field name appears in `body.error`
|
||||
* pins the test to the intended failure rather than merely the status code.
|
||||
*
|
||||
* @param {string|{statusCode: number, error: string}} rawOrParsedBody - the
|
||||
* response body, either as raw JSON text or already parsed into an object
|
||||
* (some HTTP client wrappers pre-parse the body for the caller)
|
||||
* @param {number} status
|
||||
* @param {string} fieldSubstring
|
||||
*/
|
||||
function assertValidationErrorBody(rawOrParsedBody, status, fieldSubstring) {
|
||||
assert.ok(rawOrParsedBody != null, 'expected a response body')
|
||||
const body =
|
||||
typeof rawOrParsedBody === 'string'
|
||||
? JSON.parse(rawOrParsedBody)
|
||||
: rawOrParsedBody
|
||||
assert.equal(body.statusCode, status)
|
||||
assert.equal(typeof body.error, 'string')
|
||||
assert.ok(
|
||||
body.error.includes(fieldSubstring),
|
||||
`expected error message ${JSON.stringify(body.error)} to include ${JSON.stringify(fieldSubstring)}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a request was actually rejected by the zod validation layer
|
||||
* because of the specific field named in `fieldSubstring` -- not just any
|
||||
* error/response that happens to produce the same status code. See
|
||||
* assertValidationErrorBody() above.
|
||||
*
|
||||
* @param {any} errorOrResult - either the object thrown/rejected by a failed
|
||||
* request, e.g. a RequestFailedError from @overleaf/fetch-utils (exposes
|
||||
* `.response.status` and `.body` as raw text), or a resolved
|
||||
* `{ response, body }` pair from an HTTP client wrapper that never rejects
|
||||
* on non-2xx responses and has already parsed `body` from JSON. Either way
|
||||
* expected to expose `.response.status` and `.body`
|
||||
* @param {number} status - the expected HTTP status code
|
||||
* @param {string} fieldSubstring - a substring expected in the error
|
||||
* message identifying the rejected field (e.g. a param/body/query key)
|
||||
*/
|
||||
function expectValidationError(errorOrResult, status, fieldSubstring) {
|
||||
assert.equal(
|
||||
errorOrResult && errorOrResult.response && errorOrResult.response.status,
|
||||
status,
|
||||
`expected response.status === ${status}`
|
||||
)
|
||||
assertValidationErrorBody(errorOrResult.body, status, fieldSubstring)
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as expectValidationError, but for a response object that exposes
|
||||
* `statusCode` and `body` directly rather than nested under `.response`
|
||||
* (e.g. a raw node:http response).
|
||||
*
|
||||
* @param {any} res - expected to expose `.statusCode` and `.body` directly
|
||||
* @param {number} status
|
||||
* @param {string} fieldSubstring
|
||||
*/
|
||||
function expectValidationErrorRaw(res, status, fieldSubstring) {
|
||||
assert.equal(
|
||||
res && res.statusCode,
|
||||
status,
|
||||
`expected a response with statusCode === ${status}`
|
||||
)
|
||||
assertValidationErrorBody(res.body, status, fieldSubstring)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
asZodError,
|
||||
expectValidationError,
|
||||
expectValidationErrorRaw,
|
||||
}
|
||||
|
||||
+132
-16
@@ -1,26 +1,142 @@
|
||||
// Metrics must be initialized before importing anything else
|
||||
import '@overleaf/metrics/initialize.js'
|
||||
|
||||
import metrics from '@overleaf/metrics'
|
||||
import logger from '@overleaf/logger'
|
||||
import settings from '@overleaf/settings'
|
||||
import express from 'express'
|
||||
import { handleValidationError } from '@overleaf/validation-tools'
|
||||
import { mongoClient } from './app/js/mongodb.js'
|
||||
import { createServer } from './app/js/server.js'
|
||||
import * as MessageHttpController from './app/js/Features/Messages/MessageHttpController.js'
|
||||
import { expressify } from './app/js/util/promises.js'
|
||||
|
||||
logger.initialize('chat')
|
||||
metrics.open_sockets.monitor()
|
||||
metrics.leaked_sockets.monitor(logger)
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(metrics.http.monitor(logger))
|
||||
metrics.injectMetricsRoute(app)
|
||||
|
||||
app.use(express.json())
|
||||
|
||||
app.get(
|
||||
'/project/:projectId/messages',
|
||||
expressify(MessageHttpController.getGlobalMessages)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/messages',
|
||||
expressify(MessageHttpController.sendGlobalMessage)
|
||||
)
|
||||
app.get(
|
||||
'/project/:projectId/messages/:messageId',
|
||||
expressify(MessageHttpController.getGlobalMessage)
|
||||
)
|
||||
app.delete(
|
||||
'/project/:projectId/messages/:messageId',
|
||||
expressify(MessageHttpController.deleteGlobalMessage)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/messages/:messageId/edit',
|
||||
expressify(MessageHttpController.editGlobalMessage)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/thread/:threadId/messages',
|
||||
expressify(MessageHttpController.sendMessage)
|
||||
)
|
||||
app.get(
|
||||
'/project/:projectId/thread/:threadId/messages/:messageId',
|
||||
expressify(MessageHttpController.getThreadMessage)
|
||||
)
|
||||
app.delete(
|
||||
'/project/:projectId/thread/:threadId/messages/:messageId',
|
||||
expressify(MessageHttpController.deleteMessage)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/thread/:threadId/messages/:messageId/edit',
|
||||
expressify(MessageHttpController.editMessage)
|
||||
)
|
||||
app.delete(
|
||||
'/project/:projectId/thread/:threadId/user/:userId/messages/:messageId',
|
||||
expressify(MessageHttpController.deleteUserMessage)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/thread/:threadId/resolve',
|
||||
expressify(MessageHttpController.resolveThread)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/thread/:threadId/reopen',
|
||||
expressify(MessageHttpController.reopenThread)
|
||||
)
|
||||
app.get(
|
||||
'/project/:projectId/thread/:threadId',
|
||||
expressify(MessageHttpController.getThread)
|
||||
)
|
||||
app.delete(
|
||||
'/project/:projectId/thread/:threadId',
|
||||
expressify(MessageHttpController.deleteThread)
|
||||
)
|
||||
app.get(
|
||||
'/project/:projectId/threads',
|
||||
expressify(MessageHttpController.getThreads)
|
||||
)
|
||||
app.get(
|
||||
'/project/:projectId/resolved-thread-ids',
|
||||
expressify(MessageHttpController.getResolvedThreadIds)
|
||||
)
|
||||
app.delete(
|
||||
'/project/:projectId',
|
||||
expressify(MessageHttpController.destroyProject)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/duplicate-comment-threads',
|
||||
expressify(MessageHttpController.duplicateCommentThreads)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/generate-thread-data',
|
||||
expressify(MessageHttpController.generateThreadData)
|
||||
)
|
||||
app.post(
|
||||
'/project/:projectId/clone-comment-threads',
|
||||
expressify(MessageHttpController.cloneCommentThreads)
|
||||
)
|
||||
// express also serves HEAD /status via this GET route
|
||||
app.get('/status', expressify(MessageHttpController.getStatus))
|
||||
|
||||
// Return a 404 for unknown routes
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ message: `Not found` })
|
||||
})
|
||||
|
||||
app.use(handleValidationError)
|
||||
|
||||
// Handle any unexpected errors
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error({ err, req }, 'request errored')
|
||||
res.status(500).json({ message: `Internal error: ${err.message}` })
|
||||
})
|
||||
|
||||
const port = settings.internal.chat.port
|
||||
const host = settings.internal.chat.host
|
||||
mongoClient
|
||||
.connect()
|
||||
.then(async () => {
|
||||
const { server } = await createServer()
|
||||
server.listen(port, host, function (err) {
|
||||
if (err) {
|
||||
logger.fatal({ err }, `Cannot bind to ${host}:${port}. Exiting.`)
|
||||
process.exit(1)
|
||||
}
|
||||
logger.debug(`Chat starting up, listening on ${host}:${port}`)
|
||||
|
||||
if (import.meta.main) {
|
||||
// Called directly
|
||||
mongoClient
|
||||
.connect()
|
||||
.then(() => {
|
||||
app.listen(port, host, function (err) {
|
||||
if (err) {
|
||||
logger.fatal({ err }, `Cannot bind to ${host}:${port}. Exiting.`)
|
||||
process.exit(1)
|
||||
}
|
||||
logger.debug(`Chat starting up, listening on ${host}:${port}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
logger.fatal({ err }, 'Cannot connect to mongo. Exiting.')
|
||||
process.exit(1)
|
||||
})
|
||||
.catch(err => {
|
||||
logger.fatal({ err }, 'Cannot connect to mongo. Exiting.')
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
export default app
|
||||
|
||||
@@ -1,136 +1,28 @@
|
||||
import logger from '@overleaf/logger'
|
||||
import { parseReq } from '@overleaf/validation-tools'
|
||||
import { promiseMapWithLimit } from '@overleaf/promise-utils'
|
||||
import * as MessageManager from './MessageManager.js'
|
||||
import * as MessageFormatter from './MessageFormatter.js'
|
||||
import * as ThreadManager from '../Threads/ThreadManager.js'
|
||||
import { ObjectId } from '../../mongodb.js'
|
||||
import { promiseMapWithLimit } from '@overleaf/promise-utils'
|
||||
import * as Schemas from './MessageHttpSchemas.js'
|
||||
|
||||
const DEFAULT_MESSAGE_LIMIT = 50
|
||||
const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words
|
||||
|
||||
function readContext(context, req) {
|
||||
req.body = context.requestBody
|
||||
req.params = context.params.path
|
||||
req.query = context.params.query
|
||||
if (typeof req.params.projectId !== 'undefined') {
|
||||
if (!ObjectId.isValid(req.params.projectId)) {
|
||||
context.res.status(400).setBody('Invalid projectId')
|
||||
}
|
||||
}
|
||||
if (typeof req.params.threadId !== 'undefined') {
|
||||
if (!ObjectId.isValid(req.params.threadId)) {
|
||||
context.res.status(400).setBody('Invalid threadId')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @param {(req: unknown, res: unknown) => Promise<unknown>} ControllerMethod
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function callMessageHttpController(context, ControllerMethod) {
|
||||
const req = {}
|
||||
readContext(context, req)
|
||||
if (context.res.statusCode !== 400) {
|
||||
return await ControllerMethod(req, context.res)
|
||||
} else {
|
||||
return context.res.body
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGlobalMessages(context) {
|
||||
return await callMessageHttpController(context, _getGlobalMessages)
|
||||
}
|
||||
|
||||
export async function getGlobalMessage(context) {
|
||||
return await callMessageHttpController(context, _getGlobalMessage)
|
||||
}
|
||||
|
||||
export async function sendGlobalMessage(context) {
|
||||
return await callMessageHttpController(context, _sendGlobalMessage)
|
||||
}
|
||||
|
||||
export async function sendMessage(context) {
|
||||
return await callMessageHttpController(context, _sendThreadMessage)
|
||||
}
|
||||
|
||||
export async function getThreads(context) {
|
||||
return await callMessageHttpController(context, _getAllThreads)
|
||||
}
|
||||
|
||||
export async function getThread(context) {
|
||||
return await callMessageHttpController(context, _getThread)
|
||||
}
|
||||
|
||||
export async function getThreadMessage(context) {
|
||||
return await callMessageHttpController(context, _getThreadMessage)
|
||||
}
|
||||
|
||||
export async function resolveThread(context) {
|
||||
return await callMessageHttpController(context, _resolveThread)
|
||||
}
|
||||
|
||||
export async function reopenThread(context) {
|
||||
return await callMessageHttpController(context, _reopenThread)
|
||||
}
|
||||
|
||||
export async function deleteThread(context) {
|
||||
return await callMessageHttpController(context, _deleteThread)
|
||||
}
|
||||
|
||||
export async function editMessage(context) {
|
||||
return await callMessageHttpController(context, _editMessage)
|
||||
}
|
||||
|
||||
export async function editGlobalMessage(context) {
|
||||
return await callMessageHttpController(context, _editGlobalMessage)
|
||||
}
|
||||
|
||||
export async function deleteMessage(context) {
|
||||
return await callMessageHttpController(context, _deleteMessage)
|
||||
}
|
||||
|
||||
export async function deleteUserMessage(context) {
|
||||
return await callMessageHttpController(context, _deleteUserMessage)
|
||||
}
|
||||
|
||||
export async function deleteGlobalMessage(context) {
|
||||
return await callMessageHttpController(context, _deleteGlobalMessage)
|
||||
}
|
||||
|
||||
export async function getResolvedThreadIds(context) {
|
||||
return await callMessageHttpController(context, _getResolvedThreadIds)
|
||||
}
|
||||
|
||||
export async function destroyProject(context) {
|
||||
return await callMessageHttpController(context, _destroyProject)
|
||||
}
|
||||
|
||||
export async function duplicateCommentThreads(context) {
|
||||
return await callMessageHttpController(context, _duplicateCommentThreads)
|
||||
}
|
||||
|
||||
export async function generateThreadData(context) {
|
||||
return await callMessageHttpController(context, _generateThreadData)
|
||||
}
|
||||
|
||||
export async function cloneCommentThreads(context) {
|
||||
return await callMessageHttpController(context, _cloneCommentThreads)
|
||||
}
|
||||
|
||||
export async function getStatus(context) {
|
||||
export async function getStatus(req, res) {
|
||||
const message = 'chat is alive'
|
||||
context.res.status(200).setBody(message)
|
||||
return message
|
||||
res.send(message)
|
||||
}
|
||||
|
||||
const _getGlobalMessages = async (req, res) => {
|
||||
await _getMessages(ThreadManager.GLOBAL_THREAD, req, res)
|
||||
export async function getGlobalMessages(req, res) {
|
||||
const {
|
||||
params: { projectId },
|
||||
query: { before, limit },
|
||||
} = parseReq(req, Schemas.getGlobalMessages)
|
||||
await _getMessages(projectId, ThreadManager.GLOBAL_THREAD, before, limit, res)
|
||||
}
|
||||
|
||||
const _getGlobalMessage = async (req, res) => {
|
||||
const { projectId, messageId } = req.params
|
||||
export async function getGlobalMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, messageId },
|
||||
} = parseReq(req, Schemas.getGlobalMessage)
|
||||
logger.debug({ projectId, messageId }, 'getting single global message')
|
||||
try {
|
||||
const room = await ThreadManager.findThread(
|
||||
@@ -141,23 +33,25 @@ const _getGlobalMessage = async (req, res) => {
|
||||
const message = await MessageManager.getMessage(room._id, messageId)
|
||||
const formattedMsg = MessageFormatter.formatMessageForClientSide(message)
|
||||
|
||||
res.status(200).setBody(formattedMsg)
|
||||
res.json(formattedMsg)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ThreadManager.MissingThreadError ||
|
||||
error instanceof MessageManager.MissingMessageError
|
||||
) {
|
||||
res.status(404)
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function _sendGlobalMessage(req, res) {
|
||||
const { user_id: userId, content } = req.body
|
||||
const { projectId } = req.params
|
||||
return await _sendMessage(
|
||||
export async function sendGlobalMessage(req, res) {
|
||||
const {
|
||||
params: { projectId },
|
||||
body: { user_id: userId, content },
|
||||
} = parseReq(req, Schemas.sendGlobalMessage)
|
||||
await _sendMessage(
|
||||
userId,
|
||||
projectId,
|
||||
content,
|
||||
@@ -166,14 +60,18 @@ async function _sendGlobalMessage(req, res) {
|
||||
)
|
||||
}
|
||||
|
||||
async function _sendThreadMessage(req, res) {
|
||||
const { user_id: userId, content } = req.body
|
||||
const { projectId, threadId } = req.params
|
||||
return await _sendMessage(userId, projectId, content, threadId, res)
|
||||
export async function sendMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId },
|
||||
body: { user_id: userId, content },
|
||||
} = parseReq(req, Schemas.sendMessage)
|
||||
await _sendMessage(userId, projectId, content, threadId, res)
|
||||
}
|
||||
|
||||
const _getAllThreads = async (req, res) => {
|
||||
const { projectId } = req.params
|
||||
export async function getThreads(req, res) {
|
||||
const {
|
||||
params: { projectId },
|
||||
} = parseReq(req, Schemas.getThreads)
|
||||
logger.debug({ projectId }, 'getting all threads')
|
||||
const rooms = await ThreadManager.findAllThreadRooms(projectId)
|
||||
const roomIds = rooms.map(r => r._id)
|
||||
@@ -182,9 +80,11 @@ const _getAllThreads = async (req, res) => {
|
||||
res.json(threads)
|
||||
}
|
||||
|
||||
const _generateThreadData = async (req, res) => {
|
||||
const { projectId } = req.params
|
||||
const { threads } = req.body
|
||||
export async function generateThreadData(req, res) {
|
||||
const {
|
||||
params: { projectId },
|
||||
body: { threads },
|
||||
} = parseReq(req, Schemas.generateThreadData)
|
||||
logger.debug({ projectId }, 'getting all threads')
|
||||
const rooms = await ThreadManager.findThreadsById(projectId, threads)
|
||||
const roomIds = rooms.map(r => r._id)
|
||||
@@ -194,8 +94,10 @@ const _generateThreadData = async (req, res) => {
|
||||
res.json(threadData)
|
||||
}
|
||||
|
||||
const _getThread = async (req, res) => {
|
||||
const { projectId, threadId } = req.params
|
||||
export async function getThread(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId },
|
||||
} = parseReq(req, Schemas.getThread)
|
||||
logger.debug({ projectId, threadId }, 'getting specific thread')
|
||||
try {
|
||||
const room = await ThreadManager.findThread(projectId, threadId)
|
||||
@@ -204,21 +106,23 @@ const _getThread = async (req, res) => {
|
||||
|
||||
const thread = threads[threadId] || null
|
||||
if (!thread) {
|
||||
res.status(404)
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
res.json(thread)
|
||||
} catch (error) {
|
||||
if (error instanceof ThreadManager.MissingThreadError) {
|
||||
res.status(404)
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const _getThreadMessage = async (req, res) => {
|
||||
const { projectId, threadId, messageId } = req.params
|
||||
export async function getThreadMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId, messageId },
|
||||
} = parseReq(req, Schemas.getThreadMessage)
|
||||
logger.debug(
|
||||
{ projectId, threadId, messageId },
|
||||
'getting single thread message'
|
||||
@@ -228,45 +132,53 @@ const _getThreadMessage = async (req, res) => {
|
||||
const message = await MessageManager.getMessage(room._id, messageId)
|
||||
const formattedMsg = MessageFormatter.formatMessageForClientSide(message)
|
||||
|
||||
res.status(200).setBody(formattedMsg)
|
||||
res.json(formattedMsg)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ThreadManager.MissingThreadError ||
|
||||
error instanceof MessageManager.MissingMessageError
|
||||
) {
|
||||
res.status(404)
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const _resolveThread = async (req, res) => {
|
||||
const { projectId, threadId } = req.params
|
||||
const { user_id: userId } = req.body
|
||||
export async function resolveThread(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId },
|
||||
body: { user_id: userId },
|
||||
} = parseReq(req, Schemas.resolveThread)
|
||||
logger.debug({ userId, projectId, threadId }, 'marking thread as resolved')
|
||||
await ThreadManager.resolveThread(projectId, threadId, userId)
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _reopenThread = async (req, res) => {
|
||||
const { projectId, threadId } = req.params
|
||||
export async function reopenThread(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId },
|
||||
} = parseReq(req, Schemas.reopenThread)
|
||||
logger.debug({ projectId, threadId }, 'reopening thread')
|
||||
await ThreadManager.reopenThread(projectId, threadId)
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _deleteThread = async (req, res) => {
|
||||
const { projectId, threadId } = req.params
|
||||
export async function deleteThread(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId },
|
||||
} = parseReq(req, Schemas.deleteThread)
|
||||
logger.debug({ projectId, threadId }, 'deleting thread')
|
||||
const roomId = await ThreadManager.deleteThread(projectId, threadId)
|
||||
await MessageManager.deleteAllMessagesInRoom(roomId)
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _editMessage = async (req, res) => {
|
||||
const { content, userId } = req.body
|
||||
const { projectId, threadId, messageId } = req.params
|
||||
export async function editMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId, messageId },
|
||||
body: { content, userId },
|
||||
} = parseReq(req, Schemas.editMessage)
|
||||
logger.debug({ projectId, threadId, messageId, content }, 'editing message')
|
||||
const room = await ThreadManager.findOrCreateThread(projectId, threadId)
|
||||
const found = await MessageManager.updateMessage(
|
||||
@@ -277,15 +189,17 @@ const _editMessage = async (req, res) => {
|
||||
Date.now()
|
||||
)
|
||||
if (!found) {
|
||||
res.status(404)
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _editGlobalMessage = async (req, res) => {
|
||||
const { content, userId } = req.body
|
||||
const { projectId, messageId } = req.params
|
||||
export async function editGlobalMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, messageId },
|
||||
body: { content, userId },
|
||||
} = parseReq(req, Schemas.editGlobalMessage)
|
||||
logger.debug({ projectId, messageId, content }, 'editing global message')
|
||||
const room = await ThreadManager.findOrCreateThread(
|
||||
projectId,
|
||||
@@ -299,45 +213,55 @@ const _editGlobalMessage = async (req, res) => {
|
||||
Date.now()
|
||||
)
|
||||
if (!found) {
|
||||
res.status(404)
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _deleteMessage = async (req, res) => {
|
||||
const { projectId, threadId, messageId } = req.params
|
||||
export async function deleteMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId, messageId },
|
||||
} = parseReq(req, Schemas.deleteMessage)
|
||||
logger.debug({ projectId, threadId, messageId }, 'deleting message')
|
||||
const room = await ThreadManager.findOrCreateThread(projectId, threadId)
|
||||
await MessageManager.deleteMessage(room._id, messageId)
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _deleteUserMessage = async (req, res) => {
|
||||
const { projectId, threadId, userId, messageId } = req.params
|
||||
export async function deleteUserMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, threadId, userId, messageId },
|
||||
} = parseReq(req, Schemas.deleteUserMessage)
|
||||
const room = await ThreadManager.findOrCreateThread(projectId, threadId)
|
||||
await MessageManager.deleteUserMessage(userId, room._id, messageId)
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _deleteGlobalMessage = async (req, res) => {
|
||||
const { projectId, messageId } = req.params
|
||||
export async function deleteGlobalMessage(req, res) {
|
||||
const {
|
||||
params: { projectId, messageId },
|
||||
} = parseReq(req, Schemas.deleteGlobalMessage)
|
||||
const room = await ThreadManager.findOrCreateThread(
|
||||
projectId,
|
||||
ThreadManager.GLOBAL_THREAD
|
||||
)
|
||||
await MessageManager.deleteMessage(room._id, messageId)
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const _getResolvedThreadIds = async (req, res) => {
|
||||
const { projectId } = req.params
|
||||
export async function getResolvedThreadIds(req, res) {
|
||||
const {
|
||||
params: { projectId },
|
||||
} = parseReq(req, Schemas.getResolvedThreadIds)
|
||||
const resolvedThreadIds = await ThreadManager.getResolvedThreadIds(projectId)
|
||||
res.json({ resolvedThreadIds })
|
||||
}
|
||||
|
||||
const _destroyProject = async (req, res) => {
|
||||
const { projectId } = req.params
|
||||
export async function destroyProject(req, res) {
|
||||
const {
|
||||
params: { projectId },
|
||||
} = parseReq(req, Schemas.destroyProject)
|
||||
logger.debug({ projectId }, 'destroying project')
|
||||
const rooms = await ThreadManager.findAllThreadRoomsAndGlobalThread(projectId)
|
||||
const roomIds = rooms.map(r => r._id)
|
||||
@@ -345,25 +269,10 @@ const _destroyProject = async (req, res) => {
|
||||
await MessageManager.deleteAllMessagesInRooms(roomIds)
|
||||
logger.debug({ projectId }, 'deleting all threads in project')
|
||||
await ThreadManager.deleteAllThreadsInProject(projectId)
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
async function _sendMessage(userId, projectId, content, clientThreadId, res) {
|
||||
if (!ObjectId.isValid(userId)) {
|
||||
const message = 'Invalid userId'
|
||||
res.status(400).setBody(message)
|
||||
return message
|
||||
}
|
||||
if (!content) {
|
||||
const message = 'No content provided'
|
||||
res.status(400).setBody(message)
|
||||
return message
|
||||
}
|
||||
if (content.length > MAX_MESSAGE_LENGTH) {
|
||||
const message = `Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
|
||||
res.status(400).setBody(message)
|
||||
return message
|
||||
}
|
||||
logger.debug(
|
||||
{ clientThreadId, projectId, userId, content },
|
||||
'new message received'
|
||||
@@ -381,22 +290,10 @@ async function _sendMessage(userId, projectId, content, clientThreadId, res) {
|
||||
message = MessageFormatter.formatMessageForClientSide(message)
|
||||
message.room_id = projectId
|
||||
|
||||
res.status(201).setBody(message)
|
||||
res.status(201).json(message)
|
||||
}
|
||||
|
||||
async function _getMessages(clientThreadId, req, res) {
|
||||
let before, limit
|
||||
const { projectId } = req.params
|
||||
if (req.query.before) {
|
||||
before = parseInt(req.query.before, 10)
|
||||
} else {
|
||||
before = null
|
||||
}
|
||||
if (req.query.limit) {
|
||||
limit = parseInt(req.query.limit, 10)
|
||||
} else {
|
||||
limit = DEFAULT_MESSAGE_LIMIT
|
||||
}
|
||||
async function _getMessages(projectId, clientThreadId, before, limit, res) {
|
||||
logger.debug(
|
||||
{ limit, before, projectId, clientThreadId },
|
||||
'get message request received'
|
||||
@@ -413,12 +310,14 @@ async function _getMessages(clientThreadId, req, res) {
|
||||
let messages = await MessageManager.getMessages(threadObjectId, limit, before)
|
||||
messages = MessageFormatter.formatMessagesForClientSide(messages)
|
||||
logger.debug({ projectId, messages }, 'got messages')
|
||||
res.status(200).setBody(messages)
|
||||
res.json(messages)
|
||||
}
|
||||
|
||||
async function _duplicateCommentThreads(req, res) {
|
||||
const { projectId } = req.params
|
||||
const { threads } = req.body
|
||||
export async function duplicateCommentThreads(req, res) {
|
||||
const {
|
||||
params: { projectId },
|
||||
body: { threads },
|
||||
} = parseReq(req, Schemas.duplicateCommentThreads)
|
||||
const result = {}
|
||||
for (const id of threads) {
|
||||
logger.debug({ projectId, thread: id }, 'duplicating thread')
|
||||
@@ -442,9 +341,11 @@ async function _duplicateCommentThreads(req, res) {
|
||||
res.json({ newThreads: result })
|
||||
}
|
||||
|
||||
async function _cloneCommentThreads(req, res) {
|
||||
const { projectId: sourceProjectId } = req.params
|
||||
const { targetProjectId } = req.body
|
||||
export async function cloneCommentThreads(req, res) {
|
||||
const {
|
||||
params: { projectId: sourceProjectId },
|
||||
body: { targetProjectId },
|
||||
} = parseReq(req, Schemas.cloneCommentThreads)
|
||||
const rooms = await ThreadManager.cloneThreads(
|
||||
sourceProjectId,
|
||||
targetProjectId
|
||||
@@ -452,5 +353,5 @@ async function _cloneCommentThreads(req, res) {
|
||||
await promiseMapWithLimit(10, rooms, async ({ from, to }) => {
|
||||
await MessageManager.duplicateRoomToOtherRoom(from, to)
|
||||
})
|
||||
res.status(204)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { z, zz } from '@overleaf/validation-tools'
|
||||
|
||||
export const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words
|
||||
const DEFAULT_MESSAGE_LIMIT = 50
|
||||
|
||||
const projectParams = z.object({ projectId: zz.objectId() })
|
||||
const threadParams = projectParams.extend({ threadId: zz.objectId() })
|
||||
const messageParams = projectParams.extend({ messageId: zz.objectId() })
|
||||
const threadMessageParams = threadParams.extend({ messageId: zz.objectId() })
|
||||
const userMessageParams = threadMessageParams.extend({ userId: zz.objectId() })
|
||||
|
||||
const messageContent = z
|
||||
.string({ error: 'No content provided' })
|
||||
.min(1, 'No content provided')
|
||||
.max(MAX_MESSAGE_LENGTH, `Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`)
|
||||
|
||||
const sendMessageBody = z.strictObject({
|
||||
user_id: zz.objectId(),
|
||||
content: messageContent,
|
||||
})
|
||||
|
||||
// web sends `userId` (camelCase) when editing, unlike `user_id` when sending.
|
||||
const editMessageBody = z.strictObject({
|
||||
content: messageContent,
|
||||
userId: zz.objectId().optional(),
|
||||
})
|
||||
|
||||
const threadsBody = z.strictObject({ threads: z.array(zz.objectId()) })
|
||||
|
||||
export const getGlobalMessages = z.object({
|
||||
params: projectParams,
|
||||
query: z.strictObject({
|
||||
before: z.coerce.number().int().optional(),
|
||||
limit: z.coerce.number().int().default(DEFAULT_MESSAGE_LIMIT),
|
||||
}),
|
||||
})
|
||||
|
||||
export const sendGlobalMessage = z.object({
|
||||
params: projectParams,
|
||||
body: sendMessageBody,
|
||||
})
|
||||
|
||||
export const getGlobalMessage = z.object({ params: messageParams })
|
||||
|
||||
export const deleteGlobalMessage = z.object({ params: messageParams })
|
||||
|
||||
export const editGlobalMessage = z.object({
|
||||
params: messageParams,
|
||||
body: editMessageBody,
|
||||
})
|
||||
|
||||
export const sendMessage = z.object({
|
||||
params: threadParams,
|
||||
body: sendMessageBody,
|
||||
})
|
||||
|
||||
export const getThreadMessage = z.object({ params: threadMessageParams })
|
||||
|
||||
export const deleteMessage = z.object({ params: threadMessageParams })
|
||||
|
||||
export const editMessage = z.object({
|
||||
params: threadMessageParams,
|
||||
body: editMessageBody,
|
||||
})
|
||||
|
||||
export const deleteUserMessage = z.object({ params: userMessageParams })
|
||||
|
||||
export const resolveThread = z.object({
|
||||
params: threadParams,
|
||||
body: z.strictObject({ user_id: zz.objectId() }),
|
||||
})
|
||||
|
||||
export const reopenThread = z.object({ params: threadParams })
|
||||
|
||||
export const getThread = z.object({ params: threadParams })
|
||||
|
||||
export const deleteThread = z.object({ params: threadParams })
|
||||
|
||||
export const getThreads = z.object({ params: projectParams })
|
||||
|
||||
export const getResolvedThreadIds = z.object({ params: projectParams })
|
||||
|
||||
export const destroyProject = z.object({ params: projectParams })
|
||||
|
||||
export const duplicateCommentThreads = z.object({
|
||||
params: projectParams,
|
||||
body: threadsBody,
|
||||
})
|
||||
|
||||
export const generateThreadData = z.object({
|
||||
params: projectParams,
|
||||
body: threadsBody,
|
||||
})
|
||||
|
||||
export const cloneCommentThreads = z.object({
|
||||
params: projectParams,
|
||||
body: z.strictObject({ targetProjectId: zz.objectId() }),
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
import http from 'node:http'
|
||||
import metrics from '@overleaf/metrics'
|
||||
import logger from '@overleaf/logger'
|
||||
import express from 'express'
|
||||
import exegesisExpress from 'exegesis-express'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as messagesController from './Features/Messages/MessageHttpController.js'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
logger.initialize('chat')
|
||||
metrics.open_sockets.monitor()
|
||||
|
||||
metrics.leaked_sockets.monitor(logger)
|
||||
|
||||
export async function createServer() {
|
||||
const app = express()
|
||||
|
||||
app.use(metrics.http.monitor(logger))
|
||||
metrics.injectMetricsRoute(app)
|
||||
|
||||
// See https://github.com/exegesis-js/exegesis/blob/master/docs/Options.md
|
||||
const options = {
|
||||
controllers: { messagesController },
|
||||
ignoreServers: true,
|
||||
allowMissingControllers: false,
|
||||
}
|
||||
|
||||
// const exegesisMiddleware = await exegesisExpress.middleware(
|
||||
const exegesisMiddleware = await exegesisExpress.middleware(
|
||||
path.resolve(__dirname, '../../chat.yaml'),
|
||||
options
|
||||
)
|
||||
|
||||
// If you have any body parsers, this should go before them.
|
||||
app.use(exegesisMiddleware)
|
||||
|
||||
// Return a 404
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ message: `Not found` })
|
||||
})
|
||||
|
||||
// Handle any unexpected errors
|
||||
app.use((err, req, res, next) => {
|
||||
res.status(500).json({ message: `Internal error: ${err.message}` })
|
||||
})
|
||||
|
||||
const server = http.createServer(app)
|
||||
return { app, server }
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
openapi: 3.1.0
|
||||
x-stoplight:
|
||||
id: okoe8mh50pjec
|
||||
info:
|
||||
title: chat
|
||||
version: "1.0"
|
||||
servers:
|
||||
- url: "http://chat:3010"
|
||||
x-exegesis-controller: messagesController
|
||||
paths:
|
||||
"/project/{projectId}/messages":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: Get Global messages
|
||||
tags: []
|
||||
responses:
|
||||
"201":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Message"
|
||||
operationId: getGlobalMessages
|
||||
description: Get global messages for the project with Project ID provided
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
in: query
|
||||
name: before
|
||||
- schema:
|
||||
type: string
|
||||
in: query
|
||||
name: limit
|
||||
post:
|
||||
summary: Send Global message
|
||||
operationId: sendGlobalMessage
|
||||
responses:
|
||||
"201":
|
||||
description: OK
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Message"
|
||||
examples:
|
||||
example-1:
|
||||
value:
|
||||
user_id: string
|
||||
content: string
|
||||
description: "UserID and Content of the message to be posted. "
|
||||
description: Send global message for the project with Project ID provided
|
||||
"/project/{projectId}/messages/{messageId}":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: messageId
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: Get single global message
|
||||
tags: []
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Message"
|
||||
"404":
|
||||
description: Message not found
|
||||
operationId: getGlobalMessage
|
||||
description: Get a single global message by message ID for the project with Project ID provided
|
||||
delete:
|
||||
summary: Delete global message
|
||||
operationId: deleteGlobalMessage
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
description: "Delete global message"
|
||||
"/project/{projectId}/thread/{threadId}/messages":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: threadId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Send message
|
||||
operationId: sendMessage
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
description: Add a message to the thread with thread ID provided from the Project with Project ID provided.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Message"
|
||||
description: |-
|
||||
JSON object with :
|
||||
- user_id: Id of the user
|
||||
- content: Content of the message
|
||||
"/project/{projectId}/threads":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: Get Threads
|
||||
tags: []
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Thread"
|
||||
examples: {}
|
||||
"404":
|
||||
description: Not Found
|
||||
operationId: getThreads
|
||||
description: Get the list of threads for the project with Project ID provided
|
||||
"/project/{projectId}/thread/{threadId}/messages/{messageId}/edit":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: threadId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: messageId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Edit message
|
||||
operationId: editMessage
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"404":
|
||||
description: Not Found
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
user_id:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- content
|
||||
examples: {}
|
||||
description: |-
|
||||
JSON object with :
|
||||
- content: Content of the message to edit
|
||||
- user_id: Id of the user (optional)
|
||||
description: |
|
||||
Update message with Message ID provided from the Thread ID and Project ID provided
|
||||
"/project/{projectId}/messages/{messageId}/edit":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: messageId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Edit global message
|
||||
operationId: editGlobalMessage
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"404":
|
||||
description: Not Found
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
user_id:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- content
|
||||
examples: {}
|
||||
description: |-
|
||||
JSON object with :
|
||||
- content: Content of the message to edit
|
||||
- user_id: Id of the user (optional)
|
||||
description: |
|
||||
Update global message with Message ID provided from the Project ID provided
|
||||
"/project/{projectId}/thread/{threadId}/messages/{messageId}":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: threadId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: messageId
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: Get thread message
|
||||
tags: []
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Message"
|
||||
"404":
|
||||
description: Message not found
|
||||
operationId: getThreadMessage
|
||||
description: Get a specific message by message ID from the thread with Thread ID and Project ID provided
|
||||
delete:
|
||||
summary: Delete message
|
||||
operationId: deleteMessage
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
description: "Delete message with Message ID provided, from the Thread with ThreadID and ProjectID provided"
|
||||
"/project/{projectId}/thread/{threadId}/user/{userId}/messages/{messageId}":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: threadId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: userId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: messageId
|
||||
in: path
|
||||
required: true
|
||||
delete:
|
||||
summary: Delete message written by a given user
|
||||
operationId: deleteUserMessage
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"/project/{projectId}/thread/{threadId}/resolve":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: threadId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Resolve Thread
|
||||
operationId: resolveThread
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
required:
|
||||
- user_id
|
||||
description: |-
|
||||
JSON object with :
|
||||
- user_id: Id of the user.
|
||||
description: Mark Thread with ThreadID and ProjectID provided owned by the user with UserID provided as resolved.
|
||||
"/project/{projectId}/thread/{threadId}/reopen":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: threadId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Reopen Thread
|
||||
operationId: reopenThread
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
description: |-
|
||||
Reopen Thread with ThreadID and ProjectID provided.
|
||||
i.e unmark it as resolved.
|
||||
"/project/{projectId}/thread/{threadId}":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: threadId
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: Get Thread
|
||||
tags: []
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Thread"
|
||||
"404":
|
||||
description: Thread not found
|
||||
operationId: getThread
|
||||
description: Get a thread with ThreadID and ProjectID provided
|
||||
delete:
|
||||
summary: Delete thread
|
||||
operationId: deleteThread
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
description: Delete thread with ThreadID and ProjectID provided
|
||||
"/project/{projectId}/resolved-thread-ids":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
get:
|
||||
summary: Get resolved thread ids
|
||||
operationId: getResolvedThreadIds
|
||||
responses:
|
||||
"200":
|
||||
description: Resolved thread ids
|
||||
"/project/{projectId}":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
delete:
|
||||
summary: Destroy project
|
||||
operationId: destroyProject
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
description: "Delete all threads from Project with Project ID provided, and all messages in those threads."
|
||||
/status:
|
||||
get:
|
||||
summary: Check status
|
||||
tags: []
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: chat is alive
|
||||
operationId: getStatus
|
||||
description: Check that the Chat service is alive
|
||||
head:
|
||||
summary: Check status
|
||||
tags: []
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: chat is alive
|
||||
operationId: getStatus
|
||||
description: Check that the Chat service is alive
|
||||
"/project/{projectId}/duplicate-comment-threads":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Duplicate comment threads
|
||||
operationId: duplicateCommentThreads
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
threads:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
newThreads:
|
||||
type: object
|
||||
description: Mapping of old thread ids to their duplicated thread ids
|
||||
description: Duplicate a list of comment threads
|
||||
"/project/{projectId}/generate-thread-data":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Generate thread data to load into the frontend
|
||||
operationId: generateThreadData
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
threads:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
description: Load threads and generate a json blob containing all messages in all the threads
|
||||
"/project/{projectId}/clone-comment-threads":
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: projectId
|
||||
in: path
|
||||
required: true
|
||||
post:
|
||||
summary: Clone a projects comment threads
|
||||
operationId: cloneCommentThreads
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
targetProjectId:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: OK
|
||||
description: Clone a projects comment threads
|
||||
components:
|
||||
schemas:
|
||||
Message:
|
||||
title: Message
|
||||
x-stoplight:
|
||||
id: ue9n1vvezlutw
|
||||
type: object
|
||||
examples:
|
||||
- user_id: string
|
||||
- content: string
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
required:
|
||||
- user_id
|
||||
- content
|
||||
Thread:
|
||||
title: Thread
|
||||
x-stoplight:
|
||||
id: 0ppt3jw4h5bua
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Message"
|
||||
@@ -21,8 +21,8 @@
|
||||
"@overleaf/mongo-utils": "*",
|
||||
"@overleaf/promise-utils": "*",
|
||||
"@overleaf/settings": "*",
|
||||
"@overleaf/validation-tools": "workspace:*",
|
||||
"async": "^3.2.5",
|
||||
"exegesis-express": "4.0.0",
|
||||
"express": "4.22.1",
|
||||
"mongodb": "6.12.0"
|
||||
},
|
||||
@@ -30,7 +30,6 @@
|
||||
"@overleaf/migrations": "workspace:*",
|
||||
"@types/node": "^24.5.2",
|
||||
"acorn": "^7.1.1",
|
||||
"ajv": "^6.12.0",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^10.4.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
@@ -90,4 +91,102 @@ describe('Cloning comment threads', async function () {
|
||||
expect(threads[this.newThreadId].messages.length).to.equal(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an orphaned (non-existent) thread', async function () {
|
||||
it('should report an error for that thread without failing the request', async function () {
|
||||
const missingThreadId = new ObjectId().toString()
|
||||
const { response, body: result } =
|
||||
await ChatClient.duplicateCommentThreads(projectId, [missingThreadId])
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(result.newThreads[missingThreadId]).to.deep.equal({
|
||||
error: 'not found',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed thread id in the body', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response } = await ChatClient.duplicateCommentThreads(projectId, [
|
||||
'malformed-thread-id',
|
||||
])
|
||||
expectValidationErrorRaw(response, 400, 'threads')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', async function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.duplicateCommentThreads(
|
||||
'malformed-project',
|
||||
[this.thread1Id]
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cloning comment threads to another project', async function () {
|
||||
const sourceProjectId = new ObjectId().toString()
|
||||
const targetProjectId = new ObjectId().toString()
|
||||
const userId = new ObjectId().toString()
|
||||
const threadId = new ObjectId().toString()
|
||||
|
||||
before(async function () {
|
||||
await ChatApp.ensureRunning()
|
||||
const { response } = await ChatClient.sendMessage(
|
||||
sourceProjectId,
|
||||
threadId,
|
||||
userId,
|
||||
'source message'
|
||||
)
|
||||
expect(response.statusCode).to.equal(201)
|
||||
})
|
||||
|
||||
describe('with a valid source and target project', function () {
|
||||
before(async function () {
|
||||
const { response } = await ChatClient.cloneCommentThreads(
|
||||
sourceProjectId,
|
||||
targetProjectId
|
||||
)
|
||||
expect(response.statusCode).to.equal(204)
|
||||
})
|
||||
|
||||
it('should copy the threads and messages into the target project', async function () {
|
||||
const { response, body: threads } =
|
||||
await ChatClient.getThreads(targetProjectId)
|
||||
expect(response.statusCode).to.equal(200)
|
||||
const threadIds = Object.keys(threads)
|
||||
expect(threadIds.length).to.equal(1)
|
||||
expect(threads[threadIds[0]].messages.length).to.equal(1)
|
||||
expect(threads[threadIds[0]].messages[0].content).to.equal(
|
||||
'source message'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not remove the threads from the source project', async function () {
|
||||
const { response, body: threads } =
|
||||
await ChatClient.getThreads(sourceProjectId)
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(threads[threadId].messages.length).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed source projectId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.cloneCommentThreads(
|
||||
'malformed-project',
|
||||
targetProjectId
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed target projectId', function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response } = await ChatClient.cloneCommentThreads(
|
||||
sourceProjectId,
|
||||
'malformed-target-project'
|
||||
)
|
||||
expectValidationErrorRaw(response, 400, 'targetProjectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
@@ -44,4 +45,114 @@ describe('Deleting a message', async function () {
|
||||
expect(threads[threadId].messages.length).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('in a thread, with a malformed messageId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.deleteMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
'malformed-message-id'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'messageId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('in a thread, with a malformed threadId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.deleteMessage(
|
||||
projectId,
|
||||
'malformed-thread-id',
|
||||
new ObjectId().toString()
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('globally', async function () {
|
||||
let messageId
|
||||
before(async function () {
|
||||
const { response, body: message } = await ChatClient.sendGlobalMessage(
|
||||
projectId,
|
||||
userId,
|
||||
'deleted global message'
|
||||
)
|
||||
expect(response.statusCode).to.equal(201)
|
||||
messageId = message.id
|
||||
const { response: response2 } = await ChatClient.deleteGlobalMessage(
|
||||
projectId,
|
||||
messageId
|
||||
)
|
||||
expect(response2.statusCode).to.equal(204)
|
||||
})
|
||||
|
||||
it('should then remove the message from the global messages', async function () {
|
||||
const { response, body: messages } =
|
||||
await ChatClient.getGlobalMessages(projectId)
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(messages.map(m => m.id)).to.not.include(messageId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('globally, with a malformed messageId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.deleteGlobalMessage(
|
||||
projectId,
|
||||
'malformed-message-id'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'messageId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('as a specific user', async function () {
|
||||
let messageId
|
||||
const otherUserId = new ObjectId().toString()
|
||||
before(async function () {
|
||||
const { response, body: message } = await ChatClient.sendMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
userId,
|
||||
'message owned by userId'
|
||||
)
|
||||
expect(response.statusCode).to.equal(201)
|
||||
messageId = message.id
|
||||
})
|
||||
|
||||
it('should not remove the message when deleted by a different user', async function () {
|
||||
const { response } = await ChatClient.deleteUserMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
otherUserId,
|
||||
messageId
|
||||
)
|
||||
expect(response.statusCode).to.equal(204)
|
||||
const { body: threads } = await ChatClient.getThreads(projectId)
|
||||
expect(threads[threadId].messages.map(m => m.id)).to.include(messageId)
|
||||
})
|
||||
|
||||
it('should remove the message when deleted by its owner', async function () {
|
||||
const { response } = await ChatClient.deleteUserMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
userId,
|
||||
messageId
|
||||
)
|
||||
expect(response.statusCode).to.equal(204)
|
||||
const { body: threads } = await ChatClient.getThreads(projectId)
|
||||
expect(threads[threadId].messages.map(m => m.id)).to.not.include(
|
||||
messageId
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('as a specific user, with a malformed userId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.deleteUserMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
'malformed-user',
|
||||
new ObjectId().toString()
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'userId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
@@ -35,4 +36,24 @@ describe('Deleting a thread', async function () {
|
||||
expect(Object.keys(threads).length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed threadId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.deleteThread(
|
||||
projectId,
|
||||
'malformed-thread-id'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.deleteThread(
|
||||
'malformed-project',
|
||||
new ObjectId().toString()
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
@@ -63,4 +64,11 @@ describe('Destroying a project', async function () {
|
||||
expect(globalThreadMessage).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.destroyProject('malformed-project')
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
import { MAX_MESSAGE_LENGTH } from '../../../app/js/Features/Messages/MessageHttpSchemas.js'
|
||||
|
||||
describe('Editing a message', async function () {
|
||||
let projectId, userId, threadId
|
||||
@@ -31,6 +33,71 @@ describe('Editing a message', async function () {
|
||||
messageId = message.id
|
||||
})
|
||||
|
||||
describe('with a malformed messageId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.editMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
'malformed-message-id',
|
||||
newContent
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'messageId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.editMessage(
|
||||
'malformed-project',
|
||||
threadId,
|
||||
messageId,
|
||||
newContent
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed threadId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.editMessage(
|
||||
projectId,
|
||||
'malformed-thread-id',
|
||||
messageId,
|
||||
newContent
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with no content', function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response } = await ChatClient.editMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
messageId,
|
||||
''
|
||||
)
|
||||
expectValidationErrorRaw(response, 400, 'No content provided')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with very long content', function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const tooLongContent = '-'.repeat(MAX_MESSAGE_LENGTH + 1)
|
||||
const { response } = await ChatClient.editMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
messageId,
|
||||
tooLongContent
|
||||
)
|
||||
expectValidationErrorRaw(
|
||||
response,
|
||||
400,
|
||||
`Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without user', function () {
|
||||
beforeEach(async function () {
|
||||
const { response } = await ChatClient.editMessage(
|
||||
@@ -93,4 +160,100 @@ describe('Editing a message', async function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('globally', async function () {
|
||||
const content = 'global message'
|
||||
const newContent = 'updated global message'
|
||||
let messageId
|
||||
beforeEach(async function () {
|
||||
projectId = new ObjectId().toString()
|
||||
userId = new ObjectId().toString()
|
||||
|
||||
const { response, body: message } = await ChatClient.sendGlobalMessage(
|
||||
projectId,
|
||||
userId,
|
||||
content
|
||||
)
|
||||
expect(response.statusCode).to.equal(201)
|
||||
expect(message.id).to.exist
|
||||
messageId = message.id
|
||||
})
|
||||
|
||||
describe('with a malformed messageId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.editGlobalMessage(
|
||||
projectId,
|
||||
'malformed-message-id',
|
||||
newContent
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'messageId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.editGlobalMessage(
|
||||
'malformed-project',
|
||||
messageId,
|
||||
newContent
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with very long content', function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const tooLongContent = '-'.repeat(MAX_MESSAGE_LENGTH + 1)
|
||||
const { response } = await ChatClient.editGlobalMessage(
|
||||
projectId,
|
||||
messageId,
|
||||
tooLongContent
|
||||
)
|
||||
expectValidationErrorRaw(
|
||||
response,
|
||||
400,
|
||||
`Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without user', function () {
|
||||
beforeEach(async function () {
|
||||
const { response } = await ChatClient.editGlobalMessage(
|
||||
projectId,
|
||||
messageId,
|
||||
newContent
|
||||
)
|
||||
expect(response.statusCode).to.equal(204)
|
||||
})
|
||||
|
||||
it('should then list the updated message in the global messages', async function () {
|
||||
const { response, body: messages } =
|
||||
await ChatClient.getGlobalMessages(projectId)
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(messages.length).to.equal(1)
|
||||
expect(messages[0].content).to.equal(newContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with another user', function () {
|
||||
beforeEach(async function () {
|
||||
const { response } = await ChatClient.editGlobalMessageWithUser(
|
||||
projectId,
|
||||
messageId,
|
||||
new ObjectId(),
|
||||
newContent
|
||||
)
|
||||
expect(response.statusCode).to.equal(404)
|
||||
})
|
||||
|
||||
it('should then list the old message in the global messages', async function () {
|
||||
const { response, body: messages } =
|
||||
await ChatClient.getGlobalMessages(projectId)
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(messages.length).to.equal(1)
|
||||
expect(messages[0].content).to.equal(content)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
|
||||
describe('Getting a message', async function () {
|
||||
before(async function () {
|
||||
await ChatApp.ensureRunning()
|
||||
})
|
||||
|
||||
describe('globally', async function () {
|
||||
const projectId = new ObjectId().toString()
|
||||
const userId = new ObjectId().toString()
|
||||
const content = 'global message'
|
||||
let messageId
|
||||
|
||||
before(async function () {
|
||||
const { response, body: message } = await ChatClient.sendGlobalMessage(
|
||||
projectId,
|
||||
userId,
|
||||
content
|
||||
)
|
||||
expect(response.statusCode).to.equal(201)
|
||||
messageId = message.id
|
||||
})
|
||||
|
||||
it('should return the message', async function () {
|
||||
const { response, body: message } = await ChatClient.getGlobalMessage(
|
||||
projectId,
|
||||
messageId
|
||||
)
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(message.content).to.equal(content)
|
||||
expect(message.user_id).to.equal(userId)
|
||||
})
|
||||
|
||||
describe('when the message does not exist', function () {
|
||||
it('should return 404', async function () {
|
||||
const { response } = await ChatClient.getGlobalMessage(
|
||||
projectId,
|
||||
new ObjectId().toString()
|
||||
)
|
||||
expect(response.statusCode).to.equal(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.getGlobalMessage(
|
||||
'malformed-project',
|
||||
messageId
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed messageId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.getGlobalMessage(
|
||||
projectId,
|
||||
'malformed-message-id'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'messageId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('in a thread', async function () {
|
||||
const projectId = new ObjectId().toString()
|
||||
const threadId = new ObjectId().toString()
|
||||
const userId = new ObjectId().toString()
|
||||
const content = 'thread message'
|
||||
let messageId
|
||||
|
||||
before(async function () {
|
||||
const { response, body: message } = await ChatClient.sendMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
userId,
|
||||
content
|
||||
)
|
||||
expect(response.statusCode).to.equal(201)
|
||||
messageId = message.id
|
||||
})
|
||||
|
||||
it('should return the message', async function () {
|
||||
const { response, body: message } = await ChatClient.getThreadMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
messageId
|
||||
)
|
||||
expect(response.statusCode).to.equal(200)
|
||||
expect(message.content).to.equal(content)
|
||||
expect(message.user_id).to.equal(userId)
|
||||
})
|
||||
|
||||
describe('when the message does not exist', function () {
|
||||
it('should return 404', async function () {
|
||||
const { response } = await ChatClient.getThreadMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
new ObjectId().toString()
|
||||
)
|
||||
expect(response.statusCode).to.equal(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed threadId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.getThreadMessage(
|
||||
projectId,
|
||||
'malformed-thread-id',
|
||||
messageId
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed messageId', function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.getThreadMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
'malformed-message-id'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'messageId')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
@@ -64,4 +65,24 @@ describe('Getting a thread', async function () {
|
||||
expect(response.statusCode).to.equal(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed threadId', function () {
|
||||
it('should return a not found error naming the field', async function () {
|
||||
const { response } = await ChatClient.getThread(
|
||||
projectId,
|
||||
'malformed-thread-id'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error naming the field', async function () {
|
||||
const { response } = await ChatClient.getThread(
|
||||
'malformed-project',
|
||||
threadId
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
@@ -8,7 +9,7 @@ async function getCount() {
|
||||
return await ChatClient.getMetric(line => {
|
||||
return (
|
||||
line.includes('timer_http_request_count') &&
|
||||
line.includes('path="project_{projectId}_messages"') &&
|
||||
line.includes('path="project_projectId_messages"') &&
|
||||
line.includes('method="POST"')
|
||||
)
|
||||
})
|
||||
@@ -161,4 +162,31 @@ describe('Getting messages', async function () {
|
||||
expect(thread1.messages[1].user_id).to.equal(userId1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed thread id in the list', function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const projectId = new ObjectId().toString()
|
||||
const { response } = await ChatClient.generateThreadData(projectId, [
|
||||
'malformed-thread-id',
|
||||
])
|
||||
expectValidationErrorRaw(response, 400, 'threads')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error for threads', async function () {
|
||||
const { response } = await ChatClient.asyncRequest({
|
||||
method: 'get',
|
||||
url: `/project/malformed-project/threads`,
|
||||
json: true,
|
||||
})
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
|
||||
it('should return a not found error for global messages', async function () {
|
||||
const { response } =
|
||||
await ChatClient.getGlobalMessages('malformed-project')
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
@@ -111,4 +112,43 @@ describe('Resolving a thread', async function () {
|
||||
expect(body.resolvedThreadIds).not.to.include(threadId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed threadId', function () {
|
||||
it('should return a not found error when resolving', async function () {
|
||||
const { response } = await ChatClient.resolveThread(
|
||||
projectId,
|
||||
'malformed-thread-id',
|
||||
userId
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
|
||||
it('should return a not found error when reopening', async function () {
|
||||
const { response } = await ChatClient.reopenThread(
|
||||
projectId,
|
||||
'malformed-thread-id'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed userId when resolving', function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const threadId = new ObjectId().toString()
|
||||
const { response } = await ChatClient.resolveThread(
|
||||
projectId,
|
||||
threadId,
|
||||
'malformed-user'
|
||||
)
|
||||
expectValidationErrorRaw(response, 400, 'user_id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', function () {
|
||||
it('should return a not found error for resolved-thread-ids', async function () {
|
||||
const { response } =
|
||||
await ChatClient.getResolvedThreadIds('malformed-project')
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ObjectId } from '../../../app/js/mongodb.js'
|
||||
import { expect } from 'chai'
|
||||
import { expectValidationErrorRaw } from '@overleaf/validation-tools/testUtils.js'
|
||||
|
||||
import * as ChatClient from './helpers/ChatClient.js'
|
||||
import * as ChatApp from './helpers/ChatApp.js'
|
||||
import { MAX_MESSAGE_LENGTH } from '../../../app/js/Features/Messages/MessageHttpSchemas.js'
|
||||
|
||||
describe('Sending a message', async function () {
|
||||
before(async function () {
|
||||
@@ -80,63 +82,113 @@ describe('Sending a message', async function () {
|
||||
'malformed-user',
|
||||
'content'
|
||||
)
|
||||
expect(response.statusCode).to.equal(400)
|
||||
expect(body).to.equal('Invalid userId')
|
||||
expectValidationErrorRaw(response, 400, 'user_id')
|
||||
expect(body.error).to.include('Invalid Mongo ObjectId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response, body } = await ChatClient.sendMessage(
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.sendMessage(
|
||||
'malformed-project',
|
||||
threadId,
|
||||
userId,
|
||||
'content'
|
||||
)
|
||||
expect(response.statusCode).to.equal(400)
|
||||
expect(body).to.equal('Invalid projectId')
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed threadId', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response, body } = await ChatClient.sendMessage(
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.sendMessage(
|
||||
projectId,
|
||||
'malformed-thread-id',
|
||||
userId,
|
||||
'content'
|
||||
)
|
||||
expect(response.statusCode).to.equal(400)
|
||||
expect(body).to.equal('Invalid threadId')
|
||||
expectValidationErrorRaw(response, 404, 'threadId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with no content', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response, body } = await ChatClient.sendMessage(
|
||||
const { response } = await ChatClient.sendMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
userId,
|
||||
null
|
||||
)
|
||||
expect(response.statusCode).to.equal(400)
|
||||
// Exegesis is responding with validation errors. I can´t find a way to choose the validation error yet.
|
||||
// expect(body).to.equal('No content provided')
|
||||
expect(body.message).to.equal('Validation errors')
|
||||
expectValidationErrorRaw(response, 400, 'No content provided')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with very long content', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const content = '-'.repeat(10 * 1024 + 1)
|
||||
const { response, body } = await ChatClient.sendMessage(
|
||||
const content = '-'.repeat(MAX_MESSAGE_LENGTH + 1)
|
||||
const { response } = await ChatClient.sendMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
userId,
|
||||
content
|
||||
)
|
||||
expect(response.statusCode).to.equal(400)
|
||||
expect(body).to.equal('Content too long (> 10240 bytes)')
|
||||
expectValidationErrorRaw(
|
||||
response,
|
||||
400,
|
||||
`Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an unknown field in the body', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response } = await ChatClient.asyncRequest({
|
||||
method: 'post',
|
||||
url: `/project/${projectId}/thread/${threadId}/messages`,
|
||||
json: {
|
||||
user_id: userId,
|
||||
content: 'content',
|
||||
unknownField: 'x',
|
||||
},
|
||||
})
|
||||
expectValidationErrorRaw(response, 400, 'unknownField')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed limit', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response } = await ChatClient.asyncRequest({
|
||||
method: 'get',
|
||||
url: `/project/${projectId}/messages?limit=nope`,
|
||||
json: true,
|
||||
})
|
||||
expectValidationErrorRaw(response, 400, 'limit')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('globally: failure cases', async function () {
|
||||
const projectId = new ObjectId().toString()
|
||||
|
||||
describe('with a malformed userId', async function () {
|
||||
it('should return a graceful error', async function () {
|
||||
const { response } = await ChatClient.sendGlobalMessage(
|
||||
projectId,
|
||||
'malformed-user',
|
||||
'content'
|
||||
)
|
||||
expectValidationErrorRaw(response, 400, 'user_id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a malformed projectId', async function () {
|
||||
it('should return a not found error', async function () {
|
||||
const { response } = await ChatClient.sendGlobalMessage(
|
||||
'malformed-project',
|
||||
new ObjectId().toString(),
|
||||
'content'
|
||||
)
|
||||
expectValidationErrorRaw(response, 404, 'projectId')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createServer } from '../../../../app/js/server.js'
|
||||
import app from '../../../../app.js'
|
||||
import { promisify } from 'node:util'
|
||||
import './MongoHelper.js'
|
||||
import testLogRecorder from '@overleaf/logger/test-log-recorder.js'
|
||||
@@ -9,7 +9,6 @@ let serverPromise = null
|
||||
|
||||
export async function ensureRunning() {
|
||||
if (!serverPromise) {
|
||||
const { app } = await createServer()
|
||||
const startServer = promisify(app.listen.bind(app))
|
||||
serverPromise = startServer(3010, '127.0.0.1')
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const request = Request.defaults({
|
||||
baseUrl: 'http://127.0.0.1:3010',
|
||||
})
|
||||
|
||||
async function asyncRequest(options) {
|
||||
export async function asyncRequest(options) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
request(options, (err, response, body) => {
|
||||
if (err) {
|
||||
@@ -35,6 +35,47 @@ export async function getGlobalMessages(projectId) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getGlobalMessage(projectId, messageId) {
|
||||
return await asyncRequest({
|
||||
method: 'get',
|
||||
url: `/project/${projectId}/messages/${messageId}`,
|
||||
json: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteGlobalMessage(projectId, messageId) {
|
||||
return await asyncRequest({
|
||||
method: 'delete',
|
||||
url: `/project/${projectId}/messages/${messageId}`,
|
||||
})
|
||||
}
|
||||
|
||||
export async function editGlobalMessage(projectId, messageId, content) {
|
||||
return await asyncRequest({
|
||||
method: 'post',
|
||||
url: `/project/${projectId}/messages/${messageId}/edit`,
|
||||
json: {
|
||||
content,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function editGlobalMessageWithUser(
|
||||
projectId,
|
||||
messageId,
|
||||
userId,
|
||||
content
|
||||
) {
|
||||
return await asyncRequest({
|
||||
method: 'post',
|
||||
url: `/project/${projectId}/messages/${messageId}/edit`,
|
||||
json: {
|
||||
content,
|
||||
userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendMessage(projectId, threadId, userId, content) {
|
||||
return await asyncRequest({
|
||||
method: 'post',
|
||||
@@ -54,6 +95,14 @@ export async function getThread(projectId, threadId) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getThreadMessage(projectId, threadId, messageId) {
|
||||
return await asyncRequest({
|
||||
method: 'get',
|
||||
url: `/project/${projectId}/thread/${threadId}/messages/${messageId}`,
|
||||
json: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getThreads(projectId) {
|
||||
return await asyncRequest({
|
||||
method: 'get',
|
||||
@@ -146,6 +195,18 @@ export async function deleteMessage(projectId, threadId, messageId) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteUserMessage(
|
||||
projectId,
|
||||
threadId,
|
||||
userId,
|
||||
messageId
|
||||
) {
|
||||
return await asyncRequest({
|
||||
method: 'delete',
|
||||
url: `/project/${projectId}/thread/${threadId}/user/${userId}/messages/${messageId}`,
|
||||
})
|
||||
}
|
||||
|
||||
export async function destroyProject(projectId) {
|
||||
return await asyncRequest({
|
||||
method: 'delete',
|
||||
@@ -172,3 +233,13 @@ export async function generateThreadData(projectId, threads) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function cloneCommentThreads(sourceProjectId, targetProjectId) {
|
||||
return await asyncRequest({
|
||||
method: 'post',
|
||||
url: `/project/${sourceProjectId}/clone-comment-threads`,
|
||||
json: {
|
||||
targetProjectId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -297,18 +297,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@apidevtools/json-schema-ref-parser@npm:^9.0.3":
|
||||
version: 9.1.2
|
||||
resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.2"
|
||||
dependencies:
|
||||
"@jsdevtools/ono": "npm:^7.1.3"
|
||||
"@types/json-schema": "npm:^7.0.6"
|
||||
call-me-maybe: "npm:^1.0.1"
|
||||
js-yaml: "npm:^4.1.0"
|
||||
checksum: 10c0/ebf952eb2e00bf0919f024e72897e047fd5012f0a9e47ac361873f6de0a733b9334513cdbc73205a6b43ac4a652b8c87f55e489c39b2d60bd0bc1cb2b411e218
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@arrows/array@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "@arrows/array@npm:1.4.1"
|
||||
@@ -4816,13 +4804,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jsdevtools/ono@npm:^7.1.3":
|
||||
version: 7.1.3
|
||||
resolution: "@jsdevtools/ono@npm:7.1.3"
|
||||
checksum: 10c0/a9f7e3e8e3bc315a34959934a5e2f874c423cf4eae64377d3fc9de0400ed9f36cb5fd5ebce3300d2e8f4085f557c4a8b591427a583729a87841fda46e6c216b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jsonjoy.com/base64@npm:17.67.0":
|
||||
version: 17.67.0
|
||||
resolution: "@jsonjoy.com/base64@npm:17.67.0"
|
||||
@@ -6928,14 +6909,13 @@ __metadata:
|
||||
"@overleaf/mongo-utils": "npm:*"
|
||||
"@overleaf/promise-utils": "npm:*"
|
||||
"@overleaf/settings": "npm:*"
|
||||
"@overleaf/validation-tools": "workspace:*"
|
||||
"@types/node": "npm:^24.5.2"
|
||||
acorn: "npm:^7.1.1"
|
||||
ajv: "npm:^6.12.0"
|
||||
async: "npm:^3.2.5"
|
||||
chai: "npm:^4.3.6"
|
||||
chai-as-promised: "npm:^7.1.1"
|
||||
eslint: "npm:^10.4.0"
|
||||
exegesis-express: "npm:4.0.0"
|
||||
express: "npm:4.22.1"
|
||||
mocha: "npm:^11.1.0"
|
||||
mocha-junit-reporter: "npm:^2.2.1"
|
||||
@@ -10971,7 +10951,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
|
||||
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
|
||||
version: 7.0.15
|
||||
resolution: "@types/json-schema@npm:7.0.15"
|
||||
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
|
||||
@@ -12793,7 +12773,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ajv-formats@npm:^2.1.0, ajv-formats@npm:^2.1.1":
|
||||
"ajv-formats@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "ajv-formats@npm:2.1.1"
|
||||
dependencies:
|
||||
@@ -12827,7 +12807,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ajv@npm:^6.12.0, ajv@npm:^6.12.3, ajv@npm:^6.12.5":
|
||||
"ajv@npm:^6.12.3, ajv@npm:^6.12.5":
|
||||
version: 6.14.0
|
||||
resolution: "ajv@npm:6.14.0"
|
||||
dependencies:
|
||||
@@ -12851,7 +12831,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.12.0, ajv@npm:^8.3.0, ajv@npm:^8.9.0":
|
||||
"ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.12.0, ajv@npm:^8.9.0":
|
||||
version: 8.18.0
|
||||
resolution: "ajv@npm:8.18.0"
|
||||
dependencies:
|
||||
@@ -14493,13 +14473,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"call-me-maybe@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "call-me-maybe@npm:1.0.2"
|
||||
checksum: 10c0/8eff5dbb61141ebb236ed71b4e9549e488bcb5451c48c11e5667d5c75b0532303788a1101e6978cafa2d0c8c1a727805599c2741e3e0982855c9f1d78cd06c9f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"callsites@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "callsites@npm:3.1.0"
|
||||
@@ -15437,7 +15410,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
|
||||
"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "content-type@npm:1.0.5"
|
||||
checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af
|
||||
@@ -16514,13 +16487,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deep-freeze@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "deep-freeze@npm:0.0.1"
|
||||
checksum: 10c0/b32c878395df6ca0e07d065750e14bc1651ec76e99490bca25e5844f7321676d7045d4eb4123892a0d4f08c38e4b7fa46d6e834782c095723447c0ee2ad0340b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deep-is@npm:^0.1.3":
|
||||
version: 0.1.4
|
||||
resolution: "deep-is@npm:0.1.4"
|
||||
@@ -18258,13 +18224,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"events-listener@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "events-listener@npm:1.1.0"
|
||||
checksum: 10c0/85a2be3d27cef22edb6e8bccfd2d87c9f9f6854a834129958c7a0410284b6c9c69efff40554ba8ae3f4c1dc9b036c5a07c5d5df8b31041b3b93722c91dc3b140
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"events-universal@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "events-universal@npm:1.0.1"
|
||||
@@ -18344,39 +18303,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exegesis-express@npm:4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "exegesis-express@npm:4.0.0"
|
||||
dependencies:
|
||||
exegesis: "npm:^4.1.0"
|
||||
checksum: 10c0/09e32a03f0bbafc09cc19d213fdc61584cda52f7f465b1cd4faba55a8c87eb956c00bb0a72f53770799669e95c4d6af525983c639ab920333d3b0397592e8241
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exegesis@npm:^4.1.0":
|
||||
version: 4.3.0
|
||||
resolution: "exegesis@npm:4.3.0"
|
||||
dependencies:
|
||||
"@apidevtools/json-schema-ref-parser": "npm:^9.0.3"
|
||||
ajv: "npm:^8.3.0"
|
||||
ajv-formats: "npm:^2.1.0"
|
||||
body-parser: "npm:^1.18.3"
|
||||
content-type: "npm:^1.0.4"
|
||||
deep-freeze: "npm:0.0.1"
|
||||
events-listener: "npm:^1.1.0"
|
||||
glob: "npm:^10.3.10"
|
||||
json-ptr: "npm:^3.0.1"
|
||||
json-schema-traverse: "npm:^1.0.0"
|
||||
lodash: "npm:^4.17.11"
|
||||
openapi3-ts: "npm:^3.1.1"
|
||||
promise-breaker: "npm:^6.0.0"
|
||||
qs: "npm:^6.6.0"
|
||||
raw-body: "npm:^2.3.3"
|
||||
semver: "npm:^7.0.0"
|
||||
checksum: 10c0/944ec9df00378ef19eb37a52aff60244e3bb98878e5979d7efb98b8454b144d9bb7ec08ea8d23e54430d97e2d0bf9e59341f85a471e1288a607cf13ceabfdbe1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exifr@npm:^7.0.0":
|
||||
version: 7.1.3
|
||||
resolution: "exifr@npm:7.1.3"
|
||||
@@ -19681,7 +19607,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.3.10, glob@npm:^10.4.2, glob@npm:^10.4.5":
|
||||
"glob@npm:^10.4.2, glob@npm:^10.4.5":
|
||||
version: 10.5.0
|
||||
resolution: "glob@npm:10.5.0"
|
||||
dependencies:
|
||||
@@ -22289,13 +22215,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json-ptr@npm:^3.0.1":
|
||||
version: 3.1.1
|
||||
resolution: "json-ptr@npm:3.1.1"
|
||||
checksum: 10c0/f5fd7eb60a8ad52e3531eea8285d22a67bf67a16249da032cd42fabcac386135adbe8c64a5588f5b755e77b9e646ca5cc97782fe09c5cef0b6d24ebdd1e1c277
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"json-schema-traverse@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "json-schema-traverse@npm:0.4.1"
|
||||
@@ -25776,15 +25695,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"openapi3-ts@npm:^3.1.1":
|
||||
version: 3.2.0
|
||||
resolution: "openapi3-ts@npm:3.2.0"
|
||||
dependencies:
|
||||
yaml: "npm:^2.2.1"
|
||||
checksum: 10c0/3b9a663bf71f9292880c970a80f6f1a8db0ee475451c03b4fd336da957a24372349594d7868ce0a60b3a0875844a1f0e906e8fec8ef4220c06aa70670bfa3148
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"optionator@npm:^0.9.3":
|
||||
version: 0.9.4
|
||||
resolution: "optionator@npm:0.9.4"
|
||||
@@ -28070,13 +27980,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise-breaker@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "promise-breaker@npm:6.0.0"
|
||||
checksum: 10c0/fcd00836dd96d34d3376964bc3a9482251248cef00f0dcf1f2485e7d9af44f9c48b0431cf4dc780fd5a7137b8e0861e3c6557667811522e93978f6bffab66cfe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise.prototype.finally@npm:^3.1.2":
|
||||
version: 3.1.8
|
||||
resolution: "promise.prototype.finally@npm:3.1.8"
|
||||
@@ -28543,7 +28446,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"raw-body@npm:^2.3.0, raw-body@npm:^2.3.3, raw-body@npm:~2.5.3":
|
||||
"raw-body@npm:^2.3.0, raw-body@npm:~2.5.3":
|
||||
version: 2.5.3
|
||||
resolution: "raw-body@npm:2.5.3"
|
||||
dependencies:
|
||||
@@ -34810,7 +34713,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.0.0, yaml@npm:^2.2.1, yaml@npm:^2.3.4":
|
||||
"yaml@npm:^2.0.0, yaml@npm:^2.3.4":
|
||||
version: 2.8.3
|
||||
resolution: "yaml@npm:2.8.3"
|
||||
bin:
|
||||
|
||||
Reference in New Issue
Block a user