Skip to main content
Glama
dictionary.controller.ts21.3 kB
import { isDeepStrictEqual } from 'node:util'; import * as eventListener from '@controllers/eventListener.controller'; import type { ContentNode, DictionaryId, Dictionary as LocalDictionary, LocalDictionaryId, } from '@intlayer/types'; import { logger } from '@logger'; import type { ResponseWithSession } from '@middlewares/sessionAuth.middleware'; import * as dictionaryService from '@services/dictionary.service'; import { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject'; import { type AppError, ErrorHandler } from '@utils/errors'; import { type DictionaryFiltersParams, getDictionaryFiltersAndPagination, } from '@utils/filtersAndPagination/getDictionaryFiltersAndPagination'; import type { FiltersAndPagination } from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody'; import { mapDictionaryToAPI } from '@utils/mapper/dictionary'; import { hasPermission } from '@utils/permissions'; import { formatPaginatedResponse, formatResponse, type PaginatedResponse, type ResponseData, } from '@utils/responseData'; import type { NextFunction, Request } from 'express'; import { t } from 'express-intlayer'; import type { Dictionary, DictionaryAPI, DictionaryCreationData, DictionaryData, VersionedContent, } from '@/types/dictionary.types'; export type GetDictionariesParams = FiltersAndPagination<DictionaryFiltersParams>; export type GetDictionariesResult = PaginatedResponse<DictionaryAPI>; const removeMetadata = <T extends Record<string, any>>(obj: T): T => { if (Array.isArray(obj)) { return obj.map(removeMetadata) as unknown as T; } if (obj && typeof obj === 'object') { const clone: T = {} as T; for (const key in obj) { if (key !== 'metadata') { clone[key] = removeMetadata(obj[key]); } } return clone as T; } return obj as T; }; /** * Retrieves a list of dictionaries based on filters and pagination. */ export const getDictionaries = async ( req: Request<GetDictionariesParams>, res: ResponseWithSession<GetDictionariesResult>, _next: NextFunction ): Promise<void> => { const { user, project, roles } = res.locals; const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } = getDictionaryFiltersAndPagination(req, res); if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } try { const dictionaries = await dictionaryService.findDictionaries( { ...filters, projectIds: project.id, }, skip, pageSize, sortOptions ); if ( !hasPermission( roles, 'dictionary:read' )({ ...res.locals, targetDictionaries: dictionaries, }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } const totalItems = await dictionaryService.countDictionaries(filters); const dictionariesAPI = dictionaries.map((el) => mapDictionaryToAPI(el)); const responseData = formatPaginatedResponse<DictionaryAPI>({ data: dictionariesAPI, page, pageSize, totalPages: getNumberOfPages(totalItems), totalItems, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type GetDictionariesKeysResult = ResponseData<string[]>; /** * Retrieves a list of dictionaries keys based on filters and pagination. */ export const getDictionariesKeys = async ( _req: Request, res: ResponseWithSession<GetDictionariesKeysResult>, _next: NextFunction ) => { const { project, roles } = res.locals; if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } try { const dictionaries = await dictionaryService.findDictionaries({ projectIds: project.id, }); if ( !hasPermission( roles, 'dictionary:read' )({ ...res.locals, targetDictionaries: dictionaries, }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } const dictionariesKeys = dictionaries.map((dictionary) => dictionary.key); const responseData = formatResponse<string[]>({ data: dictionariesKeys, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type GetDictionariesUpdateTimestampResult = ResponseData< Record<DictionaryId, { key: string; updatedAt: number }> >; /** * Retrieves a list of dictionaries keys based on filters and pagination. */ export const getDictionariesUpdateTimestamp = async ( _req: Request, res: ResponseWithSession<GetDictionariesUpdateTimestampResult>, _next: NextFunction ) => { const { project, roles } = res.locals; if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } try { const dictionaries = await dictionaryService.findDictionaries({ projectIds: project.id, }); if ( !hasPermission( roles, 'dictionary:read' )({ ...res.locals, targetDictionaries: dictionaries, }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } const dictionariesUpdateTimestamp = dictionaries.reduce( (acc, dictionary) => ({ ...acc, [dictionary.id]: { key: dictionary.key, updatedAt: new Date(dictionary.updatedAt).getTime(), }, }), {} ); const responseData = formatResponse< Record<string, { key: string; updatedAt: number }> >({ data: dictionariesUpdateTimestamp, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type GetDictionaryParams = { dictionaryKey: string }; export type GetDictionaryQuery = { version?: string }; export type GetDictionaryResult = ResponseData<DictionaryAPI>; /** * Retrieves a list of dictionaries based on filters and pagination. */ export const getDictionaryByKey = async ( req: Request<GetDictionaryParams, any, any, GetDictionaryQuery>, res: ResponseWithSession<GetDictionaryResult>, _next: NextFunction ): Promise<void> => { const { project, user, roles } = res.locals; const { dictionaryKey } = req.params; const version = req.query.version; if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } try { const dictionary = await dictionaryService.getDictionaryByKey( dictionaryKey, project.id ); if ( !hasPermission( roles, 'dictionary:read' )({ ...res.locals, targetDictionaries: [dictionary], }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } if (!dictionary.projectIds.map(String).includes(String(project.id))) { ErrorHandler.handleGenericErrorResponse( res, 'DICTIONARY_PROJECT_MISMATCH' ); return; } const apiResult = mapDictionaryToAPI(dictionary, version); const responseData = formatResponse<DictionaryAPI>({ data: apiResult, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AddDictionaryBody = { dictionary: DictionaryCreationData }; export type AddDictionaryResult = ResponseData<DictionaryAPI>; /** * Adds a new dictionary to the database. */ export const addDictionary = async ( req: Request<any, any, AddDictionaryBody>, res: ResponseWithSession<AddDictionaryResult>, _next: NextFunction ): Promise<void> => { const { project, user, roles } = res.locals; const dictionaryData = req.body.dictionary; if (!dictionaryData) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_DATA_NOT_FOUND'); return; } if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } if (!dictionaryData.projectIds?.includes(String(project.id))) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_PROJECT_MISMATCH'); return; } const dictionary: DictionaryData = { key: dictionaryData.key, title: dictionaryData.title, description: dictionaryData.description, content: new Map([ [ 'v1', { // Remove metadata as markdown metadata are dynamic data inserted at build time content: removeMetadata(dictionaryData.content ?? {}) as ContentNode, }, ], ]), creatorId: user.id, projectIds: dictionaryData.projectIds ?? [String(project.id)], }; if (!hasPermission(roles, 'dictionary:write')(res.locals)) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } try { const newDictionary = await dictionaryService.createDictionary(dictionary); const apiResult = mapDictionaryToAPI(newDictionary); const responseData = formatResponse<DictionaryAPI>({ message: t({ en: 'Dictionary created successfully', fr: 'Dictionnaire créé avec succès', es: 'Diccionario creado con éxito', }), description: t({ en: 'Your dictionary has been created successfully', fr: 'Votre dictionnaire a été créé avec succès', es: 'Su diccionario ha sido creado con éxito', }), data: apiResult, }); res.json(responseData); eventListener.sendDictionaryUpdate([ { dictionary: mapDictionaryToAPI(newDictionary), status: 'ADDED', }, ]); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type PushDictionariesBody = { dictionaries: LocalDictionary[]; }; type PushDictionariesResultData = { newDictionaries: { key: string; localId: LocalDictionaryId; id: string | undefined; }[]; updatedDictionaries: { key: string; localId: LocalDictionaryId; id: string | undefined; }[]; error: { id: string | undefined; key: string; localId: LocalDictionaryId | undefined; message: string; }[]; }; export type PushDictionariesResult = ResponseData<PushDictionariesResultData>; /** * Check each dictionaries, add the new ones and update the existing ones. * @param req - Express request object. * @param res - Express response object. * @returns Response containing the created dictionary. */ export const pushDictionaries = async ( req: Request<any, any, PushDictionariesBody>, res: ResponseWithSession<PushDictionariesResult>, _next: NextFunction ): Promise<void> => { const { project, user, roles } = res.locals; const dictionaryData = req.body.dictionaries; if ( typeof dictionaryData === 'object' && Array.isArray(dictionaryData) && dictionaryData.length === 0 ) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARIES_NOT_PROVIDED'); return; } else if (!dictionaryData) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_DATA_NOT_FOUND'); return; } if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } if (!hasPermission(roles, 'dictionary:write')(res.locals)) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } try { const existingDictionaries = dictionaryData.filter( (dictionary) => dictionary.id !== undefined ); const newDictionaries = dictionaryData.filter( (dictionary) => dictionary.id === undefined ); const newDictionariesResult: PushDictionariesResultData['newDictionaries'] = []; const updatedDictionariesResult: PushDictionariesResultData['updatedDictionaries'] = []; const errorResult: PushDictionariesResultData['error'] = []; for (const dictionaryDataEl of newDictionaries) { const dictionary: DictionaryData = { title: dictionaryDataEl.title, description: dictionaryDataEl.description, projectIds: [String(project.id)], creatorId: user.id, content: new Map([ // Remove metadata as markdown metadata are dynamic data inserted at build time [ 'v1', { content: removeMetadata(dictionaryDataEl.content) ?? ({} as ContentNode), }, ], ]), key: dictionaryDataEl.key, }; try { const newDictionary = await dictionaryService.createDictionary(dictionary); newDictionariesResult.push({ key: newDictionary.key, localId: dictionaryDataEl.localId!, id: newDictionary.id, }); } catch (error) { errorResult.push({ id: dictionaryDataEl.id!, key: dictionaryDataEl.key, localId: dictionaryDataEl.localId!, message: (error as AppError).message, }); } } for (const dictionaryDataEl of existingDictionaries) { const remoteDictionary = await dictionaryService.getDictionaryById( dictionaryDataEl.id! ); // Remove metadata as markdown metadata are dynamic data inserted at build time const cleanedContent = removeMetadata(dictionaryDataEl.content); const versionList = [...(remoteDictionary.content.keys() ?? [])]; const lastVersion = versionList[versionList.length - 1]; const lastContent = (remoteDictionary.content.get(lastVersion) ?.content as DictionaryAPI['content']) ?? null; const isSameContent = isDeepStrictEqual(lastContent, cleanedContent); const newContent: VersionedContent = new Map(remoteDictionary.content); if (!isSameContent) { const newContentVersion = dictionaryService.incrementVersion(remoteDictionary); newContent.set(newContentVersion, { // Remove metadata as markdown metadata are dynamic data inserted at build time content: cleanedContent, }); } const dictionary: DictionaryData = { ...ensureMongoDocumentToObject(remoteDictionary), ...dictionaryDataEl, content: newContent, projectIds: [String(project.id)], creatorId: user.id, key: remoteDictionary.key, }; try { const updatedDictionary = await dictionaryService.updateDictionaryByKey( remoteDictionary.key, dictionary, project.id ); updatedDictionariesResult.push({ key: updatedDictionary.key, localId: dictionaryDataEl.localId!, id: updatedDictionary.id, }); } catch (error) { errorResult.push({ id: dictionaryDataEl.id!, key: dictionaryDataEl.key, localId: dictionaryDataEl.localId!, message: (error as AppError).message, }); } } const result: PushDictionariesResultData = { newDictionaries: newDictionariesResult, updatedDictionaries: updatedDictionariesResult, error: errorResult, }; const responseData = formatResponse<PushDictionariesResultData>({ message: t({ en: 'Dictionaries updated successfully', fr: 'Dictionnaires mis à jour avec succès', es: 'Diccionarios actualizados con éxito', }), description: t({ en: 'Your dictionaries have been updated successfully', fr: 'Vos dictionnaires ont été mis à jour avec succès', es: 'Sus diccionarios han sido actualizados con éxito', }), data: result, }); eventListener.sendDictionaryUpdate([ ...newDictionariesResult.map( (dictionary) => ({ dictionary, status: 'ADDED', }) as eventListener.SendDictionaryUpdateArg ), ...updatedDictionariesResult.map( (dictionary) => ({ dictionary, status: 'UPDATED', }) as eventListener.SendDictionaryUpdateArg ), ]); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type UpdateDictionaryParam = { dictionaryId: string }; export type UpdateDictionaryBody = Partial<Dictionary>; export type UpdateDictionaryResult = ResponseData<DictionaryAPI>; /** * Updates an existing dictionary in the database. */ export const updateDictionary = async ( req: Request<UpdateDictionaryParam, any, UpdateDictionaryBody>, res: ResponseWithSession<UpdateDictionaryResult>, _next: NextFunction ): Promise<void> => { const { dictionaryId } = req.params; const { project, roles } = res.locals; const dictionaryData = req.body; if (!dictionaryData) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_DATA_NOT_FOUND'); return; } if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } if (!dictionaryData.projectIds?.includes(String(project.id))) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_PROJECT_MISMATCH'); return; } if (typeof dictionaryId === 'undefined') { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_ID_NOT_FOUND'); return; } if (!hasPermission(roles, 'dictionary:write')(res.locals)) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } try { const updatedDictionary = await dictionaryService.updateDictionaryById( dictionaryId, dictionaryData ); const apiResult = mapDictionaryToAPI(updatedDictionary); const responseData = formatResponse<DictionaryAPI>({ message: t({ en: 'Dictionary updated successfully', fr: 'Dictionnaire mis à jour avec succès', es: 'Diccionario actualizado con éxito', }), description: t({ en: 'Your dictionary has been updated successfully', fr: 'Votre dictionnaire a été mis à jour avec succès', es: 'Su diccionario ha sido actualizado con éxito', }), data: apiResult, }); eventListener.sendDictionaryUpdate([ { dictionary: apiResult, status: 'UPDATED', }, ]); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type DeleteDictionaryParam = { dictionaryId: string }; export type DeleteDictionaryResult = ResponseData<DictionaryAPI>; /** * Deletes a dictionary from the database by its ID. */ export const deleteDictionary = async ( req: Request<DeleteDictionaryParam>, res: ResponseWithSession<DeleteDictionaryResult>, _next: NextFunction ): Promise<void> => { const { project, roles } = res.locals; const { dictionaryId } = req.params as Partial<DeleteDictionaryParam>; if (!dictionaryId) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_ID_NOT_FOUND'); return; } if (!project) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECT_NOT_DEFINED'); return; } if (!hasPermission(roles, 'dictionary:admin')(res.locals)) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } try { const dictionaryToDelete = await dictionaryService.getDictionaryById(dictionaryId); if (!dictionaryToDelete.projectIds.includes(project.id)) { ErrorHandler.handleGenericErrorResponse( res, 'DICTIONARY_PROJECT_MISMATCH' ); return; } const deletedDictionary = await dictionaryService.deleteDictionaryById(dictionaryId); if (!deletedDictionary) { ErrorHandler.handleGenericErrorResponse(res, 'DICTIONARY_NOT_FOUND', { dictionaryId, }); return; } logger.info(`Dictionary deleted: ${String(deletedDictionary.id)}`); const apiResult = mapDictionaryToAPI(deletedDictionary); const responseData = formatResponse<DictionaryAPI>({ message: t({ en: 'Dictionary deleted successfully', fr: 'Dictionnaire supprimé avec succès', es: 'Diccionario eliminado con éxito', }), description: t({ en: 'Your dictionary has been deleted successfully', fr: 'Votre dictionnaire a été supprimé avec succès', es: 'Su diccionario ha sido eliminado con éxito', }), data: apiResult, }); res.json(responseData); eventListener.sendDictionaryUpdate([ { dictionary: apiResult, status: 'DELETED', }, ]); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server