mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-06 02:28:51 +01:00
🏗️ refactor: Extract DB layers to data-schemas for shared use (#7650)
* refactor: move model definitions and database-related methods to packages/data-schemas * ci: update tests due to new DB structure fix: disable mocking `librechat-data-provider` feat: Add schema exports to data-schemas package - Introduced a new schema module that exports various schemas including action, agent, and user schemas. - Updated index.ts to include the new schema exports for better modularity and organization. ci: fix appleStrategy tests fix: Agent.spec.js ci: refactor handleTools tests to use MongoMemoryServer for in-memory database fix: getLogStores imports ci: update banViolation tests to use MongoMemoryServer and improve session mocking test: refactor samlStrategy tests to improve mock configurations and user handling ci: fix crypto mock in handleText tests for improved accuracy ci: refactor spendTokens tests to improve model imports and setup ci: refactor Message model tests to use MongoMemoryServer and improve database interactions * refactor: streamline IMessage interface and move feedback properties to types/message.ts * refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen) * refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity * refactor: remove unused mongoose imports from model files for cleaner code * refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code * refactor: remove unused import in Transaction model for cleaner code * ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch * chore: cleanup imports
This commit is contained in:
parent
4cbab86b45
commit
a2fc7d312a
161 changed files with 2998 additions and 2088 deletions
9
packages/data-schemas/src/models/action.ts
Normal file
9
packages/data-schemas/src/models/action.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import actionSchema from '~/schema/action';
|
||||
import type { IAction } from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Action model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createActionModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Action || mongoose.model<IAction>('Action', actionSchema);
|
||||
}
|
||||
9
packages/data-schemas/src/models/agent.ts
Normal file
9
packages/data-schemas/src/models/agent.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import agentSchema from '~/schema/agent';
|
||||
import type { IAgent } from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Agent model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createAgentModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Agent || mongoose.model<IAgent>('Agent', agentSchema);
|
||||
}
|
||||
9
packages/data-schemas/src/models/assistant.ts
Normal file
9
packages/data-schemas/src/models/assistant.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import assistantSchema from '~/schema/assistant';
|
||||
import type { IAssistant } from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Assistant model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createAssistantModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Assistant || mongoose.model<IAssistant>('Assistant', assistantSchema);
|
||||
}
|
||||
9
packages/data-schemas/src/models/balance.ts
Normal file
9
packages/data-schemas/src/models/balance.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import balanceSchema from '~/schema/balance';
|
||||
import type * as t from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Balance model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createBalanceModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Balance || mongoose.model<t.IBalance>('Balance', balanceSchema);
|
||||
}
|
||||
9
packages/data-schemas/src/models/banner.ts
Normal file
9
packages/data-schemas/src/models/banner.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import bannerSchema from '~/schema/banner';
|
||||
import type { IBanner } from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Banner model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createBannerModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Banner || mongoose.model<IBanner>('Banner', bannerSchema);
|
||||
}
|
||||
11
packages/data-schemas/src/models/conversationTag.ts
Normal file
11
packages/data-schemas/src/models/conversationTag.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import conversationTagSchema, { IConversationTag } from '~/schema/conversationTag';
|
||||
|
||||
/**
|
||||
* Creates or returns the ConversationTag model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createConversationTagModel(mongoose: typeof import('mongoose')) {
|
||||
return (
|
||||
mongoose.models.ConversationTag ||
|
||||
mongoose.model<IConversationTag>('ConversationTag', conversationTagSchema)
|
||||
);
|
||||
}
|
||||
11
packages/data-schemas/src/models/convo.ts
Normal file
11
packages/data-schemas/src/models/convo.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type * as t from '~/types';
|
||||
import convoSchema from '~/schema/convo';
|
||||
|
||||
/**
|
||||
* Creates or returns the Conversation model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createConversationModel(mongoose: typeof import('mongoose')) {
|
||||
return (
|
||||
mongoose.models.Conversation || mongoose.model<t.IConversation>('Conversation', convoSchema)
|
||||
);
|
||||
}
|
||||
9
packages/data-schemas/src/models/file.ts
Normal file
9
packages/data-schemas/src/models/file.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import fileSchema from '~/schema/file';
|
||||
import type { IMongoFile } from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the File model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createFileModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.File || mongoose.model<IMongoFile>('File', fileSchema);
|
||||
}
|
||||
52
packages/data-schemas/src/models/index.ts
Normal file
52
packages/data-schemas/src/models/index.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { createUserModel } from './user';
|
||||
import { createTokenModel } from './token';
|
||||
import { createSessionModel } from './session';
|
||||
import { createBalanceModel } from './balance';
|
||||
import { createConversationModel } from './convo';
|
||||
import { createMessageModel } from './message';
|
||||
import { createAgentModel } from './agent';
|
||||
import { createRoleModel } from './role';
|
||||
import { createActionModel } from './action';
|
||||
import { createAssistantModel } from './assistant';
|
||||
import { createFileModel } from './file';
|
||||
import { createBannerModel } from './banner';
|
||||
import { createProjectModel } from './project';
|
||||
import { createKeyModel } from './key';
|
||||
import { createPluginAuthModel } from './pluginAuth';
|
||||
import { createTransactionModel } from './transaction';
|
||||
import { createPresetModel } from './preset';
|
||||
import { createPromptModel } from './prompt';
|
||||
import { createPromptGroupModel } from './promptGroup';
|
||||
import { createConversationTagModel } from './conversationTag';
|
||||
import { createSharedLinkModel } from './sharedLink';
|
||||
import { createToolCallModel } from './toolCall';
|
||||
|
||||
/**
|
||||
* Creates all database models for all collections
|
||||
*/
|
||||
export function createModels(mongoose: typeof import('mongoose')) {
|
||||
return {
|
||||
User: createUserModel(mongoose),
|
||||
Token: createTokenModel(mongoose),
|
||||
Session: createSessionModel(mongoose),
|
||||
Balance: createBalanceModel(mongoose),
|
||||
Conversation: createConversationModel(mongoose),
|
||||
Message: createMessageModel(mongoose),
|
||||
Agent: createAgentModel(mongoose),
|
||||
Role: createRoleModel(mongoose),
|
||||
Action: createActionModel(mongoose),
|
||||
Assistant: createAssistantModel(mongoose),
|
||||
File: createFileModel(mongoose),
|
||||
Banner: createBannerModel(mongoose),
|
||||
Project: createProjectModel(mongoose),
|
||||
Key: createKeyModel(mongoose),
|
||||
PluginAuth: createPluginAuthModel(mongoose),
|
||||
Transaction: createTransactionModel(mongoose),
|
||||
Preset: createPresetModel(mongoose),
|
||||
Prompt: createPromptModel(mongoose),
|
||||
PromptGroup: createPromptGroupModel(mongoose),
|
||||
ConversationTag: createConversationTagModel(mongoose),
|
||||
SharedLink: createSharedLinkModel(mongoose),
|
||||
ToolCall: createToolCallModel(mongoose),
|
||||
};
|
||||
}
|
||||
8
packages/data-schemas/src/models/key.ts
Normal file
8
packages/data-schemas/src/models/key.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import keySchema, { IKey } from '~/schema/key';
|
||||
|
||||
/**
|
||||
* Creates or returns the Key model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createKeyModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Key || mongoose.model<IKey>('Key', keySchema);
|
||||
}
|
||||
9
packages/data-schemas/src/models/message.ts
Normal file
9
packages/data-schemas/src/models/message.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import messageSchema from '~/schema/message';
|
||||
import type * as t from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Message model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createMessageModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Message || mongoose.model<t.IMessage>('Message', messageSchema);
|
||||
}
|
||||
8
packages/data-schemas/src/models/pluginAuth.ts
Normal file
8
packages/data-schemas/src/models/pluginAuth.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import pluginAuthSchema, { IPluginAuth } from '~/schema/pluginAuth';
|
||||
|
||||
/**
|
||||
* Creates or returns the PluginAuth model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createPluginAuthModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.PluginAuth || mongoose.model<IPluginAuth>('PluginAuth', pluginAuthSchema);
|
||||
}
|
||||
515
packages/data-schemas/src/models/plugins/mongoMeili.ts
Normal file
515
packages/data-schemas/src/models/plugins/mongoMeili.ts
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
import _ from 'lodash';
|
||||
import { MeiliSearch, Index } from 'meilisearch';
|
||||
import mongoose, { Schema, Document, Model, Query } from 'mongoose';
|
||||
import logger from '~/config/meiliLogger';
|
||||
|
||||
interface MongoMeiliOptions {
|
||||
host: string;
|
||||
apiKey: string;
|
||||
indexName: string;
|
||||
primaryKey: string;
|
||||
}
|
||||
|
||||
interface MeiliIndexable {
|
||||
[key: string]: unknown;
|
||||
_meiliIndex?: boolean;
|
||||
}
|
||||
|
||||
interface ContentItem {
|
||||
type: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface DocumentWithMeiliIndex extends Document {
|
||||
_meiliIndex?: boolean;
|
||||
preprocessObjectForIndex?: () => Record<string, unknown>;
|
||||
addObjectToMeili?: () => Promise<void>;
|
||||
updateObjectToMeili?: () => Promise<void>;
|
||||
deleteObjectFromMeili?: () => Promise<void>;
|
||||
postSaveHook?: () => void;
|
||||
postUpdateHook?: () => void;
|
||||
postRemoveHook?: () => void;
|
||||
conversationId?: string;
|
||||
content?: ContentItem[];
|
||||
messageId?: string;
|
||||
unfinished?: boolean;
|
||||
messages?: unknown[];
|
||||
title?: string;
|
||||
toJSON(): Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface SchemaWithMeiliMethods extends Model<DocumentWithMeiliIndex> {
|
||||
syncWithMeili(): Promise<void>;
|
||||
setMeiliIndexSettings(settings: Record<string, unknown>): Promise<unknown>;
|
||||
meiliSearch(q: string, params: Record<string, unknown>, populate: boolean): Promise<unknown>;
|
||||
}
|
||||
|
||||
// Environment flags
|
||||
/**
|
||||
* Flag to indicate if search is enabled based on environment variables.
|
||||
*/
|
||||
const searchEnabled = process.env.SEARCH != null && process.env.SEARCH.toLowerCase() === 'true';
|
||||
|
||||
/**
|
||||
* Flag to indicate if MeiliSearch is enabled based on required environment variables.
|
||||
*/
|
||||
const meiliEnabled =
|
||||
process.env.MEILI_HOST != null && process.env.MEILI_MASTER_KEY != null && searchEnabled;
|
||||
|
||||
/**
|
||||
* Local implementation of parseTextParts to avoid dependency on librechat-data-provider
|
||||
* Extracts text content from an array of content items
|
||||
*/
|
||||
const parseTextParts = (content: ContentItem[]): string => {
|
||||
if (!Array.isArray(content)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.filter((item) => item.type === 'text' && typeof item.text === 'string')
|
||||
.map((item) => item.text)
|
||||
.join(' ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Local implementation to handle Bing convoId conversion
|
||||
*/
|
||||
const cleanUpPrimaryKeyValue = (value: string): string => {
|
||||
return value.replace(/--/g, '|');
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the required options for configuring the mongoMeili plugin.
|
||||
*/
|
||||
const validateOptions = (options: Partial<MongoMeiliOptions>): void => {
|
||||
const requiredKeys: (keyof MongoMeiliOptions)[] = ['host', 'apiKey', 'indexName'];
|
||||
requiredKeys.forEach((key) => {
|
||||
if (!options[key]) {
|
||||
throw new Error(`Missing mongoMeili Option: ${key}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function to create a MeiliMongooseModel class which extends a Mongoose model.
|
||||
* This class contains static and instance methods to synchronize and manage the MeiliSearch index
|
||||
* corresponding to the MongoDB collection.
|
||||
*
|
||||
* @param config - Configuration object.
|
||||
* @param config.index - The MeiliSearch index object.
|
||||
* @param config.attributesToIndex - List of attributes to index.
|
||||
* @returns A class definition that will be loaded into the Mongoose schema.
|
||||
*/
|
||||
const createMeiliMongooseModel = ({
|
||||
index,
|
||||
attributesToIndex,
|
||||
}: {
|
||||
index: Index<MeiliIndexable>;
|
||||
attributesToIndex: string[];
|
||||
}) => {
|
||||
const primaryKey = attributesToIndex[0];
|
||||
|
||||
class MeiliMongooseModel {
|
||||
/**
|
||||
* Synchronizes the data between the MongoDB collection and the MeiliSearch index.
|
||||
*
|
||||
* The synchronization process involves:
|
||||
* 1. Fetching all documents from the MongoDB collection and MeiliSearch index.
|
||||
* 2. Comparing documents from both sources.
|
||||
* 3. Deleting documents from MeiliSearch that no longer exist in MongoDB.
|
||||
* 4. Adding documents to MeiliSearch that exist in MongoDB but not in the index.
|
||||
* 5. Updating documents in MeiliSearch if key fields (such as `text` or `title`) differ.
|
||||
* 6. Updating the `_meiliIndex` field in MongoDB to indicate the indexing status.
|
||||
*
|
||||
* Note: The function processes documents in batches because MeiliSearch's
|
||||
* `index.getDocuments` requires an exact limit and `index.addDocuments` does not handle
|
||||
* partial failures in a batch.
|
||||
*
|
||||
* @returns {Promise<void>} Resolves when the synchronization is complete.
|
||||
*/
|
||||
static async syncWithMeili(this: SchemaWithMeiliMethods): Promise<void> {
|
||||
try {
|
||||
let moreDocuments = true;
|
||||
const mongoDocuments = await this.find().lean();
|
||||
|
||||
const format = (doc: Record<string, unknown>) =>
|
||||
_.omitBy(_.pick(doc, attributesToIndex), (v, k) => k.startsWith('$'));
|
||||
|
||||
const mongoMap = new Map(
|
||||
mongoDocuments.map((doc) => {
|
||||
const typedDoc = doc as Record<string, unknown>;
|
||||
return [typedDoc[primaryKey], format(typedDoc)];
|
||||
}),
|
||||
);
|
||||
const indexMap = new Map<unknown, Record<string, unknown>>();
|
||||
let offset = 0;
|
||||
const batchSize = 1000;
|
||||
|
||||
while (moreDocuments) {
|
||||
const batch = await index.getDocuments({ limit: batchSize, offset });
|
||||
if (batch.results.length === 0) {
|
||||
moreDocuments = false;
|
||||
}
|
||||
for (const doc of batch.results) {
|
||||
indexMap.set(doc[primaryKey], format(doc));
|
||||
}
|
||||
offset += batchSize;
|
||||
}
|
||||
|
||||
logger.debug('[syncWithMeili]', { indexMap: indexMap.size, mongoMap: mongoMap.size });
|
||||
|
||||
const updateOps: Array<{
|
||||
updateOne: {
|
||||
filter: Record<string, unknown>;
|
||||
update: { $set: { _meiliIndex: boolean } };
|
||||
};
|
||||
}> = [];
|
||||
|
||||
// Process documents present in the MeiliSearch index
|
||||
for (const [id, doc] of indexMap) {
|
||||
const update: Record<string, unknown> = {};
|
||||
update[primaryKey] = id;
|
||||
if (mongoMap.has(id)) {
|
||||
const mongoDoc = mongoMap.get(id);
|
||||
if (
|
||||
(doc.text && doc.text !== mongoDoc?.text) ||
|
||||
(doc.title && doc.title !== mongoDoc?.title)
|
||||
) {
|
||||
logger.debug(
|
||||
`[syncWithMeili] ${id} had document discrepancy in ${
|
||||
doc.text ? 'text' : 'title'
|
||||
} field`,
|
||||
);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
await index.addDocuments([doc]);
|
||||
}
|
||||
} else {
|
||||
await index.deleteDocument(id as string);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: false } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process documents present in MongoDB
|
||||
for (const [id, doc] of mongoMap) {
|
||||
const update: Record<string, unknown> = {};
|
||||
update[primaryKey] = id;
|
||||
if (!indexMap.has(id)) {
|
||||
await index.addDocuments([doc]);
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
} else if (doc._meiliIndex === false) {
|
||||
updateOps.push({
|
||||
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (updateOps.length > 0) {
|
||||
await this.collection.bulkWrite(updateOps);
|
||||
logger.debug(
|
||||
`[syncWithMeili] Finished indexing ${
|
||||
primaryKey === 'messageId' ? 'messages' : 'conversations'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[syncWithMeili] Error adding document to Meili', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates settings for the MeiliSearch index
|
||||
*/
|
||||
static async setMeiliIndexSettings(settings: Record<string, unknown>): Promise<unknown> {
|
||||
return await index.updateSettings(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the MeiliSearch index and optionally populates results
|
||||
*/
|
||||
static async meiliSearch(
|
||||
this: SchemaWithMeiliMethods,
|
||||
q: string,
|
||||
params: Record<string, unknown>,
|
||||
populate: boolean,
|
||||
): Promise<unknown> {
|
||||
const data = await index.search(q, params);
|
||||
|
||||
if (populate) {
|
||||
const query: Record<string, unknown> = {};
|
||||
query[primaryKey] = _.map(data.hits, (hit) =>
|
||||
cleanUpPrimaryKeyValue(hit[primaryKey] as string),
|
||||
);
|
||||
|
||||
const projection = Object.keys(this.schema.obj).reduce<Record<string, number>>(
|
||||
(results, key) => {
|
||||
if (!key.startsWith('$')) {
|
||||
results[key] = 1;
|
||||
}
|
||||
return results;
|
||||
},
|
||||
{ _id: 1, __v: 1 },
|
||||
);
|
||||
|
||||
const hitsFromMongoose = await this.find(query, projection).lean();
|
||||
|
||||
const populatedHits = data.hits.map((hit) => {
|
||||
const queryObj: Record<string, unknown> = {};
|
||||
queryObj[primaryKey] = hit[primaryKey];
|
||||
const originalHit = _.find(hitsFromMongoose, (item) => {
|
||||
const typedItem = item as Record<string, unknown>;
|
||||
return typedItem[primaryKey] === hit[primaryKey];
|
||||
});
|
||||
|
||||
return {
|
||||
...(originalHit && typeof originalHit === 'object' ? originalHit : {}),
|
||||
...hit,
|
||||
};
|
||||
});
|
||||
data.hits = populatedHits;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses the current document for indexing
|
||||
*/
|
||||
preprocessObjectForIndex(this: DocumentWithMeiliIndex): Record<string, unknown> {
|
||||
const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) =>
|
||||
k.startsWith('$'),
|
||||
);
|
||||
|
||||
if (
|
||||
object.conversationId &&
|
||||
typeof object.conversationId === 'string' &&
|
||||
object.conversationId.includes('|')
|
||||
) {
|
||||
object.conversationId = object.conversationId.replace(/\|/g, '--');
|
||||
}
|
||||
|
||||
if (object.content && Array.isArray(object.content)) {
|
||||
object.text = parseTextParts(object.content);
|
||||
delete object.content;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current document to the MeiliSearch index
|
||||
*/
|
||||
async addObjectToMeili(this: DocumentWithMeiliIndex): Promise<void> {
|
||||
const object = this.preprocessObjectForIndex!();
|
||||
try {
|
||||
await index.addDocuments([object]);
|
||||
} catch (error) {
|
||||
logger.error('[addObjectToMeili] Error adding document to Meili', error);
|
||||
}
|
||||
|
||||
await this.collection.updateMany(
|
||||
{ _id: this._id as mongoose.Types.ObjectId },
|
||||
{ $set: { _meiliIndex: true } },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current document in the MeiliSearch index
|
||||
*/
|
||||
async updateObjectToMeili(this: DocumentWithMeiliIndex): Promise<void> {
|
||||
const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) =>
|
||||
k.startsWith('$'),
|
||||
);
|
||||
await index.updateDocuments([object]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the current document from the MeiliSearch index.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteObjectFromMeili(this: DocumentWithMeiliIndex): Promise<void> {
|
||||
await index.deleteDocument(this._id as string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-save hook to synchronize the document with MeiliSearch.
|
||||
*
|
||||
* If the document is already indexed (i.e. `_meiliIndex` is true), it updates it;
|
||||
* otherwise, it adds the document to the index.
|
||||
*/
|
||||
postSaveHook(this: DocumentWithMeiliIndex): void {
|
||||
if (this._meiliIndex) {
|
||||
this.updateObjectToMeili!();
|
||||
} else {
|
||||
this.addObjectToMeili!();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-update hook to update the document in MeiliSearch.
|
||||
*
|
||||
* This hook is triggered after a document update, ensuring that changes are
|
||||
* propagated to the MeiliSearch index if the document is indexed.
|
||||
*/
|
||||
postUpdateHook(this: DocumentWithMeiliIndex): void {
|
||||
if (this._meiliIndex) {
|
||||
this.updateObjectToMeili!();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-remove hook to delete the document from MeiliSearch.
|
||||
*
|
||||
* This hook is triggered after a document is removed, ensuring that the document
|
||||
* is also removed from the MeiliSearch index if it was previously indexed.
|
||||
*/
|
||||
postRemoveHook(this: DocumentWithMeiliIndex): void {
|
||||
if (this._meiliIndex) {
|
||||
this.deleteObjectFromMeili!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MeiliMongooseModel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mongoose plugin to synchronize MongoDB collections with a MeiliSearch index.
|
||||
*
|
||||
* This plugin:
|
||||
* - Validates the provided options.
|
||||
* - Adds a `_meiliIndex` field to the schema to track indexing status.
|
||||
* - Sets up a MeiliSearch client and creates an index if it doesn't already exist.
|
||||
* - Loads class methods for syncing, searching, and managing documents in MeiliSearch.
|
||||
* - Registers Mongoose hooks (post-save, post-update, post-remove, etc.) to maintain index consistency.
|
||||
*
|
||||
* @param schema - The Mongoose schema to which the plugin is applied.
|
||||
* @param options - Configuration options.
|
||||
* @param options.host - The MeiliSearch host.
|
||||
* @param options.apiKey - The MeiliSearch API key.
|
||||
* @param options.indexName - The name of the MeiliSearch index.
|
||||
* @param options.primaryKey - The primary key field for indexing.
|
||||
*/
|
||||
export default function mongoMeili(schema: Schema, options: MongoMeiliOptions): void {
|
||||
validateOptions(options);
|
||||
|
||||
// Add _meiliIndex field to the schema to track if a document has been indexed in MeiliSearch.
|
||||
schema.add({
|
||||
_meiliIndex: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
select: false,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { host, apiKey, indexName, primaryKey } = options;
|
||||
|
||||
const client = new MeiliSearch({ host, apiKey });
|
||||
client.createIndex(indexName, { primaryKey });
|
||||
const index = client.index<MeiliIndexable>(indexName);
|
||||
|
||||
// Collect attributes from the schema that should be indexed
|
||||
const attributesToIndex: string[] = [
|
||||
...Object.entries(schema.obj).reduce<string[]>((results, [key, value]) => {
|
||||
const schemaValue = value as { meiliIndex?: boolean };
|
||||
return schemaValue.meiliIndex ? [...results, key] : results;
|
||||
}, []),
|
||||
];
|
||||
|
||||
schema.loadClass(createMeiliMongooseModel({ index, attributesToIndex }));
|
||||
|
||||
// Register Mongoose hooks
|
||||
schema.post('save', function (doc: DocumentWithMeiliIndex) {
|
||||
doc.postSaveHook?.();
|
||||
});
|
||||
|
||||
schema.post('updateOne', function (doc: DocumentWithMeiliIndex) {
|
||||
doc.postUpdateHook?.();
|
||||
});
|
||||
|
||||
schema.post('deleteOne', function (doc: DocumentWithMeiliIndex) {
|
||||
doc.postRemoveHook?.();
|
||||
});
|
||||
|
||||
// Pre-deleteMany hook: remove corresponding documents from MeiliSearch when multiple documents are deleted.
|
||||
schema.pre('deleteMany', async function (next) {
|
||||
if (!meiliEnabled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const conditions = (this as Query<unknown, unknown>).getQuery();
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) {
|
||||
const convoIndex = client.index('convos');
|
||||
const deletedConvos = await mongoose
|
||||
.model('Conversation')
|
||||
.find(conditions as mongoose.FilterQuery<unknown>)
|
||||
.lean();
|
||||
const promises = deletedConvos.map((convo: Record<string, unknown>) =>
|
||||
convoIndex.deleteDocument(convo.conversationId as string),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) {
|
||||
const messageIndex = client.index('messages');
|
||||
const deletedMessages = await mongoose
|
||||
.model('Message')
|
||||
.find(conditions as mongoose.FilterQuery<unknown>)
|
||||
.lean();
|
||||
const promises = deletedMessages.map((message: Record<string, unknown>) =>
|
||||
messageIndex.deleteDocument(message.messageId as string),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
return next();
|
||||
} catch (error) {
|
||||
if (meiliEnabled) {
|
||||
logger.error(
|
||||
'[MeiliMongooseModel.deleteMany] There was an issue deleting conversation indexes upon deletion. Next startup may be slow due to syncing.',
|
||||
error,
|
||||
);
|
||||
}
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
// Post-findOneAndUpdate hook
|
||||
schema.post('findOneAndUpdate', async function (doc: DocumentWithMeiliIndex) {
|
||||
if (!meiliEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc.unfinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
let meiliDoc: Record<string, unknown> | undefined;
|
||||
if (doc.messages) {
|
||||
try {
|
||||
meiliDoc = await client.index('convos').getDocument(doc.conversationId as string);
|
||||
} catch (error: unknown) {
|
||||
logger.debug(
|
||||
'[MeiliMongooseModel.findOneAndUpdate] Convo not found in MeiliSearch and will index ' +
|
||||
doc.conversationId,
|
||||
error as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (meiliDoc && meiliDoc.title === doc.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.postSaveHook?.();
|
||||
});
|
||||
}
|
||||
8
packages/data-schemas/src/models/preset.ts
Normal file
8
packages/data-schemas/src/models/preset.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import presetSchema, { IPreset } from '~/schema/preset';
|
||||
|
||||
/**
|
||||
* Creates or returns the Preset model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createPresetModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Preset || mongoose.model<IPreset>('Preset', presetSchema);
|
||||
}
|
||||
8
packages/data-schemas/src/models/project.ts
Normal file
8
packages/data-schemas/src/models/project.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import projectSchema, { IMongoProject } from '~/schema/project';
|
||||
|
||||
/**
|
||||
* Creates or returns the Project model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createProjectModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Project || mongoose.model<IMongoProject>('Project', projectSchema);
|
||||
}
|
||||
8
packages/data-schemas/src/models/prompt.ts
Normal file
8
packages/data-schemas/src/models/prompt.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import promptSchema, { IPrompt } from '~/schema/prompt';
|
||||
|
||||
/**
|
||||
* Creates or returns the Prompt model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createPromptModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Prompt || mongoose.model<IPrompt>('Prompt', promptSchema);
|
||||
}
|
||||
11
packages/data-schemas/src/models/promptGroup.ts
Normal file
11
packages/data-schemas/src/models/promptGroup.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import promptGroupSchema, { IPromptGroupDocument } from '~/schema/promptGroup';
|
||||
|
||||
/**
|
||||
* Creates or returns the PromptGroup model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createPromptGroupModel(mongoose: typeof import('mongoose')) {
|
||||
return (
|
||||
mongoose.models.PromptGroup ||
|
||||
mongoose.model<IPromptGroupDocument>('PromptGroup', promptGroupSchema)
|
||||
);
|
||||
}
|
||||
9
packages/data-schemas/src/models/role.ts
Normal file
9
packages/data-schemas/src/models/role.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import roleSchema from '~/schema/role';
|
||||
import type { IRole } from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Role model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createRoleModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Role || mongoose.model<IRole>('Role', roleSchema);
|
||||
}
|
||||
9
packages/data-schemas/src/models/session.ts
Normal file
9
packages/data-schemas/src/models/session.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import sessionSchema from '~/schema/session';
|
||||
import type * as t from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Session model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createSessionModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Session || mongoose.model<t.ISession>('Session', sessionSchema);
|
||||
}
|
||||
8
packages/data-schemas/src/models/sharedLink.ts
Normal file
8
packages/data-schemas/src/models/sharedLink.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import shareSchema, { ISharedLink } from '~/schema/share';
|
||||
|
||||
/**
|
||||
* Creates or returns the SharedLink model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createSharedLinkModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.SharedLink || mongoose.model<ISharedLink>('SharedLink', shareSchema);
|
||||
}
|
||||
9
packages/data-schemas/src/models/token.ts
Normal file
9
packages/data-schemas/src/models/token.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import tokenSchema from '~/schema/token';
|
||||
import type * as t from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the Token model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createTokenModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.Token || mongoose.model<t.IToken>('Token', tokenSchema);
|
||||
}
|
||||
8
packages/data-schemas/src/models/toolCall.ts
Normal file
8
packages/data-schemas/src/models/toolCall.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import toolCallSchema, { IToolCallData } from '~/schema/toolCall';
|
||||
|
||||
/**
|
||||
* Creates or returns the ToolCall model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createToolCallModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.ToolCall || mongoose.model<IToolCallData>('ToolCall', toolCallSchema);
|
||||
}
|
||||
10
packages/data-schemas/src/models/transaction.ts
Normal file
10
packages/data-schemas/src/models/transaction.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import transactionSchema, { ITransaction } from '~/schema/transaction';
|
||||
|
||||
/**
|
||||
* Creates or returns the Transaction model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createTransactionModel(mongoose: typeof import('mongoose')) {
|
||||
return (
|
||||
mongoose.models.Transaction || mongoose.model<ITransaction>('Transaction', transactionSchema)
|
||||
);
|
||||
}
|
||||
9
packages/data-schemas/src/models/user.ts
Normal file
9
packages/data-schemas/src/models/user.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import userSchema from '~/schema/user';
|
||||
import type * as t from '~/types';
|
||||
|
||||
/**
|
||||
* Creates or returns the User model using the provided mongoose instance and schema
|
||||
*/
|
||||
export function createUserModel(mongoose: typeof import('mongoose')) {
|
||||
return mongoose.models.User || mongoose.model<t.IUser>('User', userSchema);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue