@librechat/data-schemas 0.0.7 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2160 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +2156 -7
- package/dist/index.es.js.map +1 -1
- package/dist/types/config/meiliLogger.d.ts +4 -0
- package/dist/types/config/parsers.d.ts +32 -0
- package/dist/types/config/winston.d.ts +4 -0
- package/dist/types/crypto/index.d.ts +3 -0
- package/dist/types/index.d.ts +8 -46
- package/dist/types/methods/index.d.ts +89 -0
- package/dist/types/methods/memory.d.ts +35 -0
- package/dist/types/methods/pluginAuth.d.ts +36 -0
- package/dist/types/methods/role.d.ts +35 -0
- package/dist/types/methods/session.d.ts +48 -0
- package/dist/types/methods/share.d.ts +38 -0
- package/dist/types/methods/share.test.d.ts +1 -0
- package/dist/types/methods/token.d.ts +34 -0
- package/dist/types/methods/user.d.ts +39 -0
- package/dist/types/methods/user.test.d.ts +1 -0
- package/dist/types/models/action.d.ts +30 -0
- package/dist/types/models/agent.d.ts +30 -0
- package/dist/types/models/assistant.d.ts +30 -0
- package/dist/types/models/balance.d.ts +30 -0
- package/dist/types/models/banner.d.ts +30 -0
- package/dist/types/models/conversationTag.d.ts +30 -0
- package/dist/types/models/convo.d.ts +30 -0
- package/dist/types/models/file.d.ts +30 -0
- package/dist/types/models/index.d.ts +54 -0
- package/dist/types/models/key.d.ts +30 -0
- package/dist/types/models/memory.d.ts +27 -0
- package/dist/types/models/message.d.ts +30 -0
- package/dist/types/models/pluginAuth.d.ts +30 -0
- package/dist/types/models/plugins/mongoMeili.d.ts +74 -0
- package/dist/types/models/preset.d.ts +30 -0
- package/dist/types/models/project.d.ts +30 -0
- package/dist/types/models/prompt.d.ts +30 -0
- package/dist/types/models/promptGroup.d.ts +30 -0
- package/dist/types/models/role.d.ts +30 -0
- package/dist/types/models/session.d.ts +30 -0
- package/dist/types/models/sharedLink.d.ts +30 -0
- package/dist/types/models/token.d.ts +30 -0
- package/dist/types/models/toolCall.d.ts +30 -0
- package/dist/types/models/transaction.d.ts +30 -0
- package/dist/types/models/user.d.ts +30 -0
- package/dist/types/schema/action.d.ts +2 -27
- package/dist/types/schema/agent.d.ts +4 -31
- package/dist/types/schema/assistant.d.ts +4 -16
- package/dist/types/schema/balance.d.ts +4 -12
- package/dist/types/schema/convo.d.ts +2 -49
- package/dist/types/schema/file.d.ts +2 -26
- package/dist/types/schema/index.d.ts +24 -0
- package/dist/types/schema/memory.d.ts +29 -0
- package/dist/types/schema/message.d.ts +2 -36
- package/dist/types/schema/pluginAuth.d.ts +2 -9
- package/dist/types/schema/role.d.ts +2 -5
- package/dist/types/schema/session.d.ts +2 -6
- package/dist/types/schema/token.d.ts +2 -11
- package/dist/types/schema/user.d.ts +5 -36
- package/dist/types/types/action.d.ts +52 -0
- package/dist/types/types/agent.d.ts +55 -0
- package/dist/types/types/assistant.d.ts +39 -0
- package/dist/types/types/balance.d.ts +35 -0
- package/dist/types/types/banner.d.ts +34 -0
- package/dist/types/types/convo.d.ts +74 -0
- package/dist/types/types/file.d.ts +51 -0
- package/dist/types/types/index.d.ts +42 -0
- package/dist/types/types/memory.d.ts +63 -0
- package/dist/types/types/message.d.ts +67 -0
- package/dist/types/types/pluginAuth.d.ts +59 -0
- package/dist/types/types/role.d.ts +30 -0
- package/dist/types/types/session.d.ts +61 -0
- package/dist/types/types/share.d.ts +82 -0
- package/dist/types/types/token.d.ts +62 -0
- package/dist/types/types/user.d.ts +94 -0
- package/package.json +14 -5
- package/README.md +0 -114
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import { webcrypto } from 'node:crypto';
|
|
1
3
|
import mongoose, { Schema } from 'mongoose';
|
|
2
|
-
import { FileSources, Constants, PermissionTypes, Permissions, SystemRoles } from 'librechat-data-provider';
|
|
4
|
+
import { FileSources, Constants, PermissionTypes, Permissions, SystemRoles, roleDefaults } from 'librechat-data-provider';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import { MeiliSearch } from 'meilisearch';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import winston from 'winston';
|
|
9
|
+
import 'winston-daily-rotate-file';
|
|
10
|
+
import { klona } from 'klona';
|
|
11
|
+
import traverse from 'traverse';
|
|
12
|
+
import { nanoid } from 'nanoid';
|
|
13
|
+
|
|
14
|
+
async function signPayload({ payload, secret, expirationTime, }) {
|
|
15
|
+
return jwt.sign(payload, secret, { expiresIn: expirationTime });
|
|
16
|
+
}
|
|
17
|
+
async function hashToken(str) {
|
|
18
|
+
const data = new TextEncoder().encode(str);
|
|
19
|
+
const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
|
|
20
|
+
return Buffer.from(hashBuffer).toString('hex');
|
|
21
|
+
}
|
|
3
22
|
|
|
4
23
|
// Define the Auth sub-schema with type-safety.
|
|
5
24
|
const AuthSchema = new Schema({
|
|
@@ -131,6 +150,10 @@ const agentSchema = new Schema({
|
|
|
131
150
|
ref: 'Project',
|
|
132
151
|
index: true,
|
|
133
152
|
},
|
|
153
|
+
versions: {
|
|
154
|
+
type: [Schema.Types.Mixed],
|
|
155
|
+
default: [],
|
|
156
|
+
},
|
|
134
157
|
}, {
|
|
135
158
|
timestamps: true,
|
|
136
159
|
});
|
|
@@ -425,9 +448,9 @@ const convoSchema = new Schema({
|
|
|
425
448
|
type: String,
|
|
426
449
|
index: true,
|
|
427
450
|
},
|
|
428
|
-
messages: [{ type:
|
|
451
|
+
messages: [{ type: Schema.Types.ObjectId, ref: 'Message' }],
|
|
429
452
|
agentOptions: {
|
|
430
|
-
type:
|
|
453
|
+
type: Schema.Types.Mixed,
|
|
431
454
|
},
|
|
432
455
|
...conversationPreset,
|
|
433
456
|
agent_id: {
|
|
@@ -617,6 +640,25 @@ const messageSchema = new Schema({
|
|
|
617
640
|
finish_reason: {
|
|
618
641
|
type: String,
|
|
619
642
|
},
|
|
643
|
+
feedback: {
|
|
644
|
+
type: {
|
|
645
|
+
rating: {
|
|
646
|
+
type: String,
|
|
647
|
+
enum: ['thumbsUp', 'thumbsDown'],
|
|
648
|
+
required: true,
|
|
649
|
+
},
|
|
650
|
+
tag: {
|
|
651
|
+
type: mongoose.Schema.Types.Mixed,
|
|
652
|
+
required: false,
|
|
653
|
+
},
|
|
654
|
+
text: {
|
|
655
|
+
type: String,
|
|
656
|
+
required: false,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
default: undefined,
|
|
660
|
+
required: false,
|
|
661
|
+
},
|
|
620
662
|
_meiliIndex: {
|
|
621
663
|
type: Boolean,
|
|
622
664
|
required: false,
|
|
@@ -827,7 +869,7 @@ const promptGroupSchema = new Schema({
|
|
|
827
869
|
validator: function (v) {
|
|
828
870
|
return v === undefined || v === null || v === '' || /^[a-z0-9-]+$/.test(v);
|
|
829
871
|
},
|
|
830
|
-
message: (props) => `${props.value} is not a valid command. Only lowercase alphanumeric characters and hyphens are allowed
|
|
872
|
+
message: (props) => { var _a; return `${(_a = props === null || props === void 0 ? void 0 : props.value) !== null && _a !== void 0 ? _a : 'Value'} is not a valid command. Only lowercase alphanumeric characters and hyphens are allowed.`; },
|
|
831
873
|
},
|
|
832
874
|
maxlength: [
|
|
833
875
|
Constants.COMMANDS_MAX_LENGTH,
|
|
@@ -849,6 +891,13 @@ const rolePermissionsSchema = new Schema({
|
|
|
849
891
|
[Permissions.USE]: { type: Boolean, default: true },
|
|
850
892
|
[Permissions.CREATE]: { type: Boolean, default: true },
|
|
851
893
|
},
|
|
894
|
+
[PermissionTypes.MEMORIES]: {
|
|
895
|
+
[Permissions.USE]: { type: Boolean, default: true },
|
|
896
|
+
[Permissions.CREATE]: { type: Boolean, default: true },
|
|
897
|
+
[Permissions.UPDATE]: { type: Boolean, default: true },
|
|
898
|
+
[Permissions.READ]: { type: Boolean, default: true },
|
|
899
|
+
[Permissions.OPT_OUT]: { type: Boolean, default: true },
|
|
900
|
+
},
|
|
852
901
|
[PermissionTypes.AGENTS]: {
|
|
853
902
|
[Permissions.SHARED_GLOBAL]: { type: Boolean, default: false },
|
|
854
903
|
[Permissions.USE]: { type: Boolean, default: true },
|
|
@@ -863,6 +912,9 @@ const rolePermissionsSchema = new Schema({
|
|
|
863
912
|
[PermissionTypes.RUN_CODE]: {
|
|
864
913
|
[Permissions.USE]: { type: Boolean, default: true },
|
|
865
914
|
},
|
|
915
|
+
[PermissionTypes.WEB_SEARCH]: {
|
|
916
|
+
[Permissions.USE]: { type: Boolean, default: true },
|
|
917
|
+
},
|
|
866
918
|
}, { _id: false });
|
|
867
919
|
const roleSchema = new Schema({
|
|
868
920
|
name: { type: String, required: true, unique: true, index: true },
|
|
@@ -875,6 +927,12 @@ const roleSchema = new Schema({
|
|
|
875
927
|
[Permissions.USE]: true,
|
|
876
928
|
[Permissions.CREATE]: true,
|
|
877
929
|
},
|
|
930
|
+
[PermissionTypes.MEMORIES]: {
|
|
931
|
+
[Permissions.USE]: true,
|
|
932
|
+
[Permissions.CREATE]: true,
|
|
933
|
+
[Permissions.UPDATE]: true,
|
|
934
|
+
[Permissions.READ]: true,
|
|
935
|
+
},
|
|
878
936
|
[PermissionTypes.AGENTS]: {
|
|
879
937
|
[Permissions.SHARED_GLOBAL]: false,
|
|
880
938
|
[Permissions.USE]: true,
|
|
@@ -883,6 +941,7 @@ const roleSchema = new Schema({
|
|
|
883
941
|
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
|
884
942
|
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
|
885
943
|
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
|
|
944
|
+
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
|
|
886
945
|
}),
|
|
887
946
|
},
|
|
888
947
|
});
|
|
@@ -1046,7 +1105,7 @@ const BackupCodeSchema = new Schema({
|
|
|
1046
1105
|
used: { type: Boolean, default: false },
|
|
1047
1106
|
usedAt: { type: Date, default: null },
|
|
1048
1107
|
}, { _id: false });
|
|
1049
|
-
const
|
|
1108
|
+
const userSchema = new Schema({
|
|
1050
1109
|
name: {
|
|
1051
1110
|
type: String,
|
|
1052
1111
|
},
|
|
@@ -1057,7 +1116,7 @@ const User = new Schema({
|
|
|
1057
1116
|
},
|
|
1058
1117
|
email: {
|
|
1059
1118
|
type: String,
|
|
1060
|
-
required: [true,
|
|
1119
|
+
required: [true, "can't be blank"],
|
|
1061
1120
|
lowercase: true,
|
|
1062
1121
|
unique: true,
|
|
1063
1122
|
match: [/\S+@\S+\.\S+/, 'is invalid'],
|
|
@@ -1102,6 +1161,11 @@ const User = new Schema({
|
|
|
1102
1161
|
unique: true,
|
|
1103
1162
|
sparse: true,
|
|
1104
1163
|
},
|
|
1164
|
+
samlId: {
|
|
1165
|
+
type: String,
|
|
1166
|
+
unique: true,
|
|
1167
|
+
sparse: true,
|
|
1168
|
+
},
|
|
1105
1169
|
ldapId: {
|
|
1106
1170
|
type: String,
|
|
1107
1171
|
unique: true,
|
|
@@ -1146,7 +1210,2092 @@ const User = new Schema({
|
|
|
1146
1210
|
type: Boolean,
|
|
1147
1211
|
default: false,
|
|
1148
1212
|
},
|
|
1213
|
+
personalization: {
|
|
1214
|
+
type: {
|
|
1215
|
+
memories: {
|
|
1216
|
+
type: Boolean,
|
|
1217
|
+
default: true,
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
default: {},
|
|
1221
|
+
},
|
|
1149
1222
|
}, { timestamps: true });
|
|
1150
1223
|
|
|
1151
|
-
|
|
1224
|
+
const MemoryEntrySchema = new Schema({
|
|
1225
|
+
userId: {
|
|
1226
|
+
type: Schema.Types.ObjectId,
|
|
1227
|
+
ref: 'User',
|
|
1228
|
+
index: true,
|
|
1229
|
+
required: true,
|
|
1230
|
+
},
|
|
1231
|
+
key: {
|
|
1232
|
+
type: String,
|
|
1233
|
+
required: true,
|
|
1234
|
+
validate: {
|
|
1235
|
+
validator: (v) => /^[a-z_]+$/.test(v),
|
|
1236
|
+
message: 'Key must only contain lowercase letters and underscores',
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
value: {
|
|
1240
|
+
type: String,
|
|
1241
|
+
required: true,
|
|
1242
|
+
},
|
|
1243
|
+
tokenCount: {
|
|
1244
|
+
type: Number,
|
|
1245
|
+
default: 0,
|
|
1246
|
+
},
|
|
1247
|
+
updated_at: {
|
|
1248
|
+
type: Date,
|
|
1249
|
+
default: Date.now,
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Creates or returns the User model using the provided mongoose instance and schema
|
|
1255
|
+
*/
|
|
1256
|
+
function createUserModel(mongoose) {
|
|
1257
|
+
return mongoose.models.User || mongoose.model('User', userSchema);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Creates or returns the Token model using the provided mongoose instance and schema
|
|
1262
|
+
*/
|
|
1263
|
+
function createTokenModel(mongoose) {
|
|
1264
|
+
return mongoose.models.Token || mongoose.model('Token', tokenSchema);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Creates or returns the Session model using the provided mongoose instance and schema
|
|
1269
|
+
*/
|
|
1270
|
+
function createSessionModel(mongoose) {
|
|
1271
|
+
return mongoose.models.Session || mongoose.model('Session', sessionSchema);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Creates or returns the Balance model using the provided mongoose instance and schema
|
|
1276
|
+
*/
|
|
1277
|
+
function createBalanceModel(mongoose) {
|
|
1278
|
+
return mongoose.models.Balance || mongoose.model('Balance', balanceSchema);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const logDir$1 = path.join(__dirname, '..', '..', '..', 'api', 'logs');
|
|
1282
|
+
const { NODE_ENV: NODE_ENV$1, DEBUG_LOGGING: DEBUG_LOGGING$1 = 'false' } = process.env;
|
|
1283
|
+
const useDebugLogging$1 = (typeof DEBUG_LOGGING$1 === 'string' && DEBUG_LOGGING$1.toLowerCase() === 'true') ||
|
|
1284
|
+
DEBUG_LOGGING$1 === 'true';
|
|
1285
|
+
const levels$1 = {
|
|
1286
|
+
error: 0,
|
|
1287
|
+
warn: 1,
|
|
1288
|
+
info: 2,
|
|
1289
|
+
http: 3,
|
|
1290
|
+
verbose: 4,
|
|
1291
|
+
debug: 5,
|
|
1292
|
+
activity: 6,
|
|
1293
|
+
silly: 7,
|
|
1294
|
+
};
|
|
1295
|
+
winston.addColors({
|
|
1296
|
+
info: 'green',
|
|
1297
|
+
warn: 'italic yellow',
|
|
1298
|
+
error: 'red',
|
|
1299
|
+
debug: 'blue',
|
|
1300
|
+
});
|
|
1301
|
+
const level$1 = () => {
|
|
1302
|
+
const env = NODE_ENV$1 || 'development';
|
|
1303
|
+
const isDevelopment = env === 'development';
|
|
1304
|
+
return isDevelopment ? 'debug' : 'warn';
|
|
1305
|
+
};
|
|
1306
|
+
const fileFormat$1 = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.splat());
|
|
1307
|
+
const logLevel = useDebugLogging$1 ? 'debug' : 'error';
|
|
1308
|
+
const transports$1 = [
|
|
1309
|
+
new winston.transports.DailyRotateFile({
|
|
1310
|
+
level: logLevel,
|
|
1311
|
+
filename: `${logDir$1}/meiliSync-%DATE%.log`,
|
|
1312
|
+
datePattern: 'YYYY-MM-DD',
|
|
1313
|
+
zippedArchive: true,
|
|
1314
|
+
maxSize: '20m',
|
|
1315
|
+
maxFiles: '14d',
|
|
1316
|
+
format: fileFormat$1,
|
|
1317
|
+
}),
|
|
1318
|
+
];
|
|
1319
|
+
const consoleFormat$1 = winston.format.combine(winston.format.colorize({ all: true }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`));
|
|
1320
|
+
transports$1.push(new winston.transports.Console({
|
|
1321
|
+
level: 'info',
|
|
1322
|
+
format: consoleFormat$1,
|
|
1323
|
+
}));
|
|
1324
|
+
const logger$1 = winston.createLogger({
|
|
1325
|
+
level: level$1(),
|
|
1326
|
+
levels: levels$1,
|
|
1327
|
+
transports: transports$1,
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
// Environment flags
|
|
1331
|
+
/**
|
|
1332
|
+
* Flag to indicate if search is enabled based on environment variables.
|
|
1333
|
+
*/
|
|
1334
|
+
const searchEnabled = process.env.SEARCH != null && process.env.SEARCH.toLowerCase() === 'true';
|
|
1335
|
+
/**
|
|
1336
|
+
* Flag to indicate if MeiliSearch is enabled based on required environment variables.
|
|
1337
|
+
*/
|
|
1338
|
+
const meiliEnabled = process.env.MEILI_HOST != null && process.env.MEILI_MASTER_KEY != null && searchEnabled;
|
|
1339
|
+
/**
|
|
1340
|
+
* Local implementation of parseTextParts to avoid dependency on librechat-data-provider
|
|
1341
|
+
* Extracts text content from an array of content items
|
|
1342
|
+
*/
|
|
1343
|
+
const parseTextParts = (content) => {
|
|
1344
|
+
if (!Array.isArray(content)) {
|
|
1345
|
+
return '';
|
|
1346
|
+
}
|
|
1347
|
+
return content
|
|
1348
|
+
.filter((item) => item.type === 'text' && typeof item.text === 'string')
|
|
1349
|
+
.map((item) => item.text)
|
|
1350
|
+
.join(' ')
|
|
1351
|
+
.trim();
|
|
1352
|
+
};
|
|
1353
|
+
/**
|
|
1354
|
+
* Local implementation to handle Bing convoId conversion
|
|
1355
|
+
*/
|
|
1356
|
+
const cleanUpPrimaryKeyValue = (value) => {
|
|
1357
|
+
return value.replace(/--/g, '|');
|
|
1358
|
+
};
|
|
1359
|
+
/**
|
|
1360
|
+
* Validates the required options for configuring the mongoMeili plugin.
|
|
1361
|
+
*/
|
|
1362
|
+
const validateOptions = (options) => {
|
|
1363
|
+
const requiredKeys = ['host', 'apiKey', 'indexName'];
|
|
1364
|
+
requiredKeys.forEach((key) => {
|
|
1365
|
+
if (!options[key]) {
|
|
1366
|
+
throw new Error(`Missing mongoMeili Option: ${key}`);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
};
|
|
1370
|
+
/**
|
|
1371
|
+
* Factory function to create a MeiliMongooseModel class which extends a Mongoose model.
|
|
1372
|
+
* This class contains static and instance methods to synchronize and manage the MeiliSearch index
|
|
1373
|
+
* corresponding to the MongoDB collection.
|
|
1374
|
+
*
|
|
1375
|
+
* @param config - Configuration object.
|
|
1376
|
+
* @param config.index - The MeiliSearch index object.
|
|
1377
|
+
* @param config.attributesToIndex - List of attributes to index.
|
|
1378
|
+
* @returns A class definition that will be loaded into the Mongoose schema.
|
|
1379
|
+
*/
|
|
1380
|
+
const createMeiliMongooseModel = ({ index, attributesToIndex, }) => {
|
|
1381
|
+
const primaryKey = attributesToIndex[0];
|
|
1382
|
+
class MeiliMongooseModel {
|
|
1383
|
+
/**
|
|
1384
|
+
* Synchronizes the data between the MongoDB collection and the MeiliSearch index.
|
|
1385
|
+
*
|
|
1386
|
+
* The synchronization process involves:
|
|
1387
|
+
* 1. Fetching all documents from the MongoDB collection and MeiliSearch index.
|
|
1388
|
+
* 2. Comparing documents from both sources.
|
|
1389
|
+
* 3. Deleting documents from MeiliSearch that no longer exist in MongoDB.
|
|
1390
|
+
* 4. Adding documents to MeiliSearch that exist in MongoDB but not in the index.
|
|
1391
|
+
* 5. Updating documents in MeiliSearch if key fields (such as `text` or `title`) differ.
|
|
1392
|
+
* 6. Updating the `_meiliIndex` field in MongoDB to indicate the indexing status.
|
|
1393
|
+
*
|
|
1394
|
+
* Note: The function processes documents in batches because MeiliSearch's
|
|
1395
|
+
* `index.getDocuments` requires an exact limit and `index.addDocuments` does not handle
|
|
1396
|
+
* partial failures in a batch.
|
|
1397
|
+
*
|
|
1398
|
+
* @returns {Promise<void>} Resolves when the synchronization is complete.
|
|
1399
|
+
*/
|
|
1400
|
+
static async syncWithMeili() {
|
|
1401
|
+
try {
|
|
1402
|
+
let moreDocuments = true;
|
|
1403
|
+
const mongoDocuments = await this.find().lean();
|
|
1404
|
+
const format = (doc) => _.omitBy(_.pick(doc, attributesToIndex), (v, k) => k.startsWith('$'));
|
|
1405
|
+
const mongoMap = new Map(mongoDocuments.map((doc) => {
|
|
1406
|
+
const typedDoc = doc;
|
|
1407
|
+
return [typedDoc[primaryKey], format(typedDoc)];
|
|
1408
|
+
}));
|
|
1409
|
+
const indexMap = new Map();
|
|
1410
|
+
let offset = 0;
|
|
1411
|
+
const batchSize = 1000;
|
|
1412
|
+
while (moreDocuments) {
|
|
1413
|
+
const batch = await index.getDocuments({ limit: batchSize, offset });
|
|
1414
|
+
if (batch.results.length === 0) {
|
|
1415
|
+
moreDocuments = false;
|
|
1416
|
+
}
|
|
1417
|
+
for (const doc of batch.results) {
|
|
1418
|
+
indexMap.set(doc[primaryKey], format(doc));
|
|
1419
|
+
}
|
|
1420
|
+
offset += batchSize;
|
|
1421
|
+
}
|
|
1422
|
+
logger$1.debug('[syncWithMeili]', { indexMap: indexMap.size, mongoMap: mongoMap.size });
|
|
1423
|
+
const updateOps = [];
|
|
1424
|
+
// Process documents present in the MeiliSearch index
|
|
1425
|
+
for (const [id, doc] of indexMap) {
|
|
1426
|
+
const update = {};
|
|
1427
|
+
update[primaryKey] = id;
|
|
1428
|
+
if (mongoMap.has(id)) {
|
|
1429
|
+
const mongoDoc = mongoMap.get(id);
|
|
1430
|
+
if ((doc.text && doc.text !== (mongoDoc === null || mongoDoc === void 0 ? void 0 : mongoDoc.text)) ||
|
|
1431
|
+
(doc.title && doc.title !== (mongoDoc === null || mongoDoc === void 0 ? void 0 : mongoDoc.title))) {
|
|
1432
|
+
logger$1.debug(`[syncWithMeili] ${id} had document discrepancy in ${doc.text ? 'text' : 'title'} field`);
|
|
1433
|
+
updateOps.push({
|
|
1434
|
+
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
|
1435
|
+
});
|
|
1436
|
+
await index.addDocuments([doc]);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1440
|
+
await index.deleteDocument(id);
|
|
1441
|
+
updateOps.push({
|
|
1442
|
+
updateOne: { filter: update, update: { $set: { _meiliIndex: false } } },
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
// Process documents present in MongoDB
|
|
1447
|
+
for (const [id, doc] of mongoMap) {
|
|
1448
|
+
const update = {};
|
|
1449
|
+
update[primaryKey] = id;
|
|
1450
|
+
if (!indexMap.has(id)) {
|
|
1451
|
+
await index.addDocuments([doc]);
|
|
1452
|
+
updateOps.push({
|
|
1453
|
+
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
else if (doc._meiliIndex === false) {
|
|
1457
|
+
updateOps.push({
|
|
1458
|
+
updateOne: { filter: update, update: { $set: { _meiliIndex: true } } },
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (updateOps.length > 0) {
|
|
1463
|
+
await this.collection.bulkWrite(updateOps);
|
|
1464
|
+
logger$1.debug(`[syncWithMeili] Finished indexing ${primaryKey === 'messageId' ? 'messages' : 'conversations'}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
logger$1.error('[syncWithMeili] Error adding document to Meili:', error);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Updates settings for the MeiliSearch index
|
|
1473
|
+
*/
|
|
1474
|
+
static async setMeiliIndexSettings(settings) {
|
|
1475
|
+
return await index.updateSettings(settings);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Searches the MeiliSearch index and optionally populates results
|
|
1479
|
+
*/
|
|
1480
|
+
static async meiliSearch(q, params, populate) {
|
|
1481
|
+
const data = await index.search(q, params);
|
|
1482
|
+
if (populate) {
|
|
1483
|
+
const query = {};
|
|
1484
|
+
query[primaryKey] = _.map(data.hits, (hit) => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
|
1485
|
+
const projection = Object.keys(this.schema.obj).reduce((results, key) => {
|
|
1486
|
+
if (!key.startsWith('$')) {
|
|
1487
|
+
results[key] = 1;
|
|
1488
|
+
}
|
|
1489
|
+
return results;
|
|
1490
|
+
}, { _id: 1, __v: 1 });
|
|
1491
|
+
const hitsFromMongoose = await this.find(query, projection).lean();
|
|
1492
|
+
const populatedHits = data.hits.map((hit) => {
|
|
1493
|
+
hit[primaryKey];
|
|
1494
|
+
const originalHit = _.find(hitsFromMongoose, (item) => {
|
|
1495
|
+
const typedItem = item;
|
|
1496
|
+
return typedItem[primaryKey] === hit[primaryKey];
|
|
1497
|
+
});
|
|
1498
|
+
return {
|
|
1499
|
+
...(originalHit && typeof originalHit === 'object' ? originalHit : {}),
|
|
1500
|
+
...hit,
|
|
1501
|
+
};
|
|
1502
|
+
});
|
|
1503
|
+
data.hits = populatedHits;
|
|
1504
|
+
}
|
|
1505
|
+
return data;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Preprocesses the current document for indexing
|
|
1509
|
+
*/
|
|
1510
|
+
preprocessObjectForIndex() {
|
|
1511
|
+
const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) => k.startsWith('$'));
|
|
1512
|
+
if (object.conversationId &&
|
|
1513
|
+
typeof object.conversationId === 'string' &&
|
|
1514
|
+
object.conversationId.includes('|')) {
|
|
1515
|
+
object.conversationId = object.conversationId.replace(/\|/g, '--');
|
|
1516
|
+
}
|
|
1517
|
+
if (object.content && Array.isArray(object.content)) {
|
|
1518
|
+
object.text = parseTextParts(object.content);
|
|
1519
|
+
delete object.content;
|
|
1520
|
+
}
|
|
1521
|
+
return object;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Adds the current document to the MeiliSearch index
|
|
1525
|
+
*/
|
|
1526
|
+
async addObjectToMeili(next) {
|
|
1527
|
+
const object = this.preprocessObjectForIndex();
|
|
1528
|
+
try {
|
|
1529
|
+
await index.addDocuments([object]);
|
|
1530
|
+
}
|
|
1531
|
+
catch (error) {
|
|
1532
|
+
logger$1.error('[addObjectToMeili] Error adding document to Meili:', error);
|
|
1533
|
+
return next();
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } });
|
|
1537
|
+
}
|
|
1538
|
+
catch (error) {
|
|
1539
|
+
logger$1.error('[addObjectToMeili] Error updating _meiliIndex field:', error);
|
|
1540
|
+
return next();
|
|
1541
|
+
}
|
|
1542
|
+
next();
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Updates the current document in the MeiliSearch index
|
|
1546
|
+
*/
|
|
1547
|
+
async updateObjectToMeili(next) {
|
|
1548
|
+
try {
|
|
1549
|
+
const object = _.omitBy(_.pick(this.toJSON(), attributesToIndex), (v, k) => k.startsWith('$'));
|
|
1550
|
+
await index.updateDocuments([object]);
|
|
1551
|
+
next();
|
|
1552
|
+
}
|
|
1553
|
+
catch (error) {
|
|
1554
|
+
logger$1.error('[updateObjectToMeili] Error updating document in Meili:', error);
|
|
1555
|
+
return next();
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Deletes the current document from the MeiliSearch index.
|
|
1560
|
+
*
|
|
1561
|
+
* @returns {Promise<void>}
|
|
1562
|
+
*/
|
|
1563
|
+
async deleteObjectFromMeili(next) {
|
|
1564
|
+
try {
|
|
1565
|
+
await index.deleteDocument(this._id);
|
|
1566
|
+
next();
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
logger$1.error('[deleteObjectFromMeili] Error deleting document from Meili:', error);
|
|
1570
|
+
return next();
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Post-save hook to synchronize the document with MeiliSearch.
|
|
1575
|
+
*
|
|
1576
|
+
* If the document is already indexed (i.e. `_meiliIndex` is true), it updates it;
|
|
1577
|
+
* otherwise, it adds the document to the index.
|
|
1578
|
+
*/
|
|
1579
|
+
postSaveHook(next) {
|
|
1580
|
+
if (this._meiliIndex) {
|
|
1581
|
+
this.updateObjectToMeili(next);
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
this.addObjectToMeili(next);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Post-update hook to update the document in MeiliSearch.
|
|
1589
|
+
*
|
|
1590
|
+
* This hook is triggered after a document update, ensuring that changes are
|
|
1591
|
+
* propagated to the MeiliSearch index if the document is indexed.
|
|
1592
|
+
*/
|
|
1593
|
+
postUpdateHook(next) {
|
|
1594
|
+
if (this._meiliIndex) {
|
|
1595
|
+
this.updateObjectToMeili(next);
|
|
1596
|
+
}
|
|
1597
|
+
else {
|
|
1598
|
+
next();
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Post-remove hook to delete the document from MeiliSearch.
|
|
1603
|
+
*
|
|
1604
|
+
* This hook is triggered after a document is removed, ensuring that the document
|
|
1605
|
+
* is also removed from the MeiliSearch index if it was previously indexed.
|
|
1606
|
+
*/
|
|
1607
|
+
postRemoveHook(next) {
|
|
1608
|
+
if (this._meiliIndex) {
|
|
1609
|
+
this.deleteObjectFromMeili(next);
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
next();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return MeiliMongooseModel;
|
|
1617
|
+
};
|
|
1618
|
+
/**
|
|
1619
|
+
* Mongoose plugin to synchronize MongoDB collections with a MeiliSearch index.
|
|
1620
|
+
*
|
|
1621
|
+
* This plugin:
|
|
1622
|
+
* - Validates the provided options.
|
|
1623
|
+
* - Adds a `_meiliIndex` field to the schema to track indexing status.
|
|
1624
|
+
* - Sets up a MeiliSearch client and creates an index if it doesn't already exist.
|
|
1625
|
+
* - Loads class methods for syncing, searching, and managing documents in MeiliSearch.
|
|
1626
|
+
* - Registers Mongoose hooks (post-save, post-update, post-remove, etc.) to maintain index consistency.
|
|
1627
|
+
*
|
|
1628
|
+
* @param schema - The Mongoose schema to which the plugin is applied.
|
|
1629
|
+
* @param options - Configuration options.
|
|
1630
|
+
* @param options.host - The MeiliSearch host.
|
|
1631
|
+
* @param options.apiKey - The MeiliSearch API key.
|
|
1632
|
+
* @param options.indexName - The name of the MeiliSearch index.
|
|
1633
|
+
* @param options.primaryKey - The primary key field for indexing.
|
|
1634
|
+
*/
|
|
1635
|
+
function mongoMeili(schema, options) {
|
|
1636
|
+
const mongoose = options.mongoose;
|
|
1637
|
+
validateOptions(options);
|
|
1638
|
+
// Add _meiliIndex field to the schema to track if a document has been indexed in MeiliSearch.
|
|
1639
|
+
schema.add({
|
|
1640
|
+
_meiliIndex: {
|
|
1641
|
+
type: Boolean,
|
|
1642
|
+
required: false,
|
|
1643
|
+
select: false,
|
|
1644
|
+
default: false,
|
|
1645
|
+
},
|
|
1646
|
+
});
|
|
1647
|
+
const { host, apiKey, indexName, primaryKey } = options;
|
|
1648
|
+
const client = new MeiliSearch({ host, apiKey });
|
|
1649
|
+
client.createIndex(indexName, { primaryKey });
|
|
1650
|
+
const index = client.index(indexName);
|
|
1651
|
+
// Collect attributes from the schema that should be indexed
|
|
1652
|
+
const attributesToIndex = [
|
|
1653
|
+
...Object.entries(schema.obj).reduce((results, [key, value]) => {
|
|
1654
|
+
const schemaValue = value;
|
|
1655
|
+
return schemaValue.meiliIndex ? [...results, key] : results;
|
|
1656
|
+
}, []),
|
|
1657
|
+
];
|
|
1658
|
+
schema.loadClass(createMeiliMongooseModel({ index, attributesToIndex }));
|
|
1659
|
+
// Register Mongoose hooks
|
|
1660
|
+
schema.post('save', function (doc, next) {
|
|
1661
|
+
var _a;
|
|
1662
|
+
(_a = doc.postSaveHook) === null || _a === void 0 ? void 0 : _a.call(doc, next);
|
|
1663
|
+
});
|
|
1664
|
+
schema.post('updateOne', function (doc, next) {
|
|
1665
|
+
var _a;
|
|
1666
|
+
(_a = doc.postUpdateHook) === null || _a === void 0 ? void 0 : _a.call(doc, next);
|
|
1667
|
+
});
|
|
1668
|
+
schema.post('deleteOne', function (doc, next) {
|
|
1669
|
+
var _a;
|
|
1670
|
+
(_a = doc.postRemoveHook) === null || _a === void 0 ? void 0 : _a.call(doc, next);
|
|
1671
|
+
});
|
|
1672
|
+
// Pre-deleteMany hook: remove corresponding documents from MeiliSearch when multiple documents are deleted.
|
|
1673
|
+
schema.pre('deleteMany', async function (next) {
|
|
1674
|
+
if (!meiliEnabled) {
|
|
1675
|
+
return next();
|
|
1676
|
+
}
|
|
1677
|
+
try {
|
|
1678
|
+
const conditions = this.getQuery();
|
|
1679
|
+
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) {
|
|
1680
|
+
const convoIndex = client.index('convos');
|
|
1681
|
+
const deletedConvos = await mongoose
|
|
1682
|
+
.model('Conversation')
|
|
1683
|
+
.find(conditions)
|
|
1684
|
+
.lean();
|
|
1685
|
+
const promises = deletedConvos.map((convo) => convoIndex.deleteDocument(convo.conversationId));
|
|
1686
|
+
await Promise.all(promises);
|
|
1687
|
+
}
|
|
1688
|
+
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) {
|
|
1689
|
+
const messageIndex = client.index('messages');
|
|
1690
|
+
const deletedMessages = await mongoose
|
|
1691
|
+
.model('Message')
|
|
1692
|
+
.find(conditions)
|
|
1693
|
+
.lean();
|
|
1694
|
+
const promises = deletedMessages.map((message) => messageIndex.deleteDocument(message.messageId));
|
|
1695
|
+
await Promise.all(promises);
|
|
1696
|
+
}
|
|
1697
|
+
return next();
|
|
1698
|
+
}
|
|
1699
|
+
catch (error) {
|
|
1700
|
+
if (meiliEnabled) {
|
|
1701
|
+
logger$1.error('[MeiliMongooseModel.deleteMany] There was an issue deleting conversation indexes upon deletion. Next startup may be slow due to syncing.', error);
|
|
1702
|
+
}
|
|
1703
|
+
return next();
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
// Post-findOneAndUpdate hook
|
|
1707
|
+
schema.post('findOneAndUpdate', async function (doc, next) {
|
|
1708
|
+
var _a;
|
|
1709
|
+
if (!meiliEnabled) {
|
|
1710
|
+
return next();
|
|
1711
|
+
}
|
|
1712
|
+
if (doc.unfinished) {
|
|
1713
|
+
return next();
|
|
1714
|
+
}
|
|
1715
|
+
let meiliDoc;
|
|
1716
|
+
if (doc.messages) {
|
|
1717
|
+
try {
|
|
1718
|
+
meiliDoc = await client.index('convos').getDocument(doc.conversationId);
|
|
1719
|
+
}
|
|
1720
|
+
catch (error) {
|
|
1721
|
+
logger$1.debug('[MeiliMongooseModel.findOneAndUpdate] Convo not found in MeiliSearch and will index ' +
|
|
1722
|
+
doc.conversationId, error);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (meiliDoc && meiliDoc.title === doc.title) {
|
|
1726
|
+
return next();
|
|
1727
|
+
}
|
|
1728
|
+
(_a = doc.postSaveHook) === null || _a === void 0 ? void 0 : _a.call(doc, next);
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Creates or returns the Conversation model using the provided mongoose instance and schema
|
|
1734
|
+
*/
|
|
1735
|
+
function createConversationModel(mongoose) {
|
|
1736
|
+
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
|
1737
|
+
convoSchema.plugin(mongoMeili, {
|
|
1738
|
+
mongoose,
|
|
1739
|
+
host: process.env.MEILI_HOST,
|
|
1740
|
+
apiKey: process.env.MEILI_MASTER_KEY,
|
|
1741
|
+
/** Note: Will get created automatically if it doesn't exist already */
|
|
1742
|
+
indexName: 'convos',
|
|
1743
|
+
primaryKey: 'conversationId',
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return (mongoose.models.Conversation || mongoose.model('Conversation', convoSchema));
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/**
|
|
1750
|
+
* Creates or returns the Message model using the provided mongoose instance and schema
|
|
1751
|
+
*/
|
|
1752
|
+
function createMessageModel(mongoose) {
|
|
1753
|
+
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
|
1754
|
+
messageSchema.plugin(mongoMeili, {
|
|
1755
|
+
mongoose,
|
|
1756
|
+
host: process.env.MEILI_HOST,
|
|
1757
|
+
apiKey: process.env.MEILI_MASTER_KEY,
|
|
1758
|
+
indexName: 'messages',
|
|
1759
|
+
primaryKey: 'messageId',
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
return mongoose.models.Message || mongoose.model('Message', messageSchema);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Creates or returns the Agent model using the provided mongoose instance and schema
|
|
1767
|
+
*/
|
|
1768
|
+
function createAgentModel(mongoose) {
|
|
1769
|
+
return mongoose.models.Agent || mongoose.model('Agent', agentSchema);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Creates or returns the Role model using the provided mongoose instance and schema
|
|
1774
|
+
*/
|
|
1775
|
+
function createRoleModel(mongoose) {
|
|
1776
|
+
return mongoose.models.Role || mongoose.model('Role', roleSchema);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Creates or returns the Action model using the provided mongoose instance and schema
|
|
1781
|
+
*/
|
|
1782
|
+
function createActionModel(mongoose) {
|
|
1783
|
+
return mongoose.models.Action || mongoose.model('Action', Action);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Creates or returns the Assistant model using the provided mongoose instance and schema
|
|
1788
|
+
*/
|
|
1789
|
+
function createAssistantModel(mongoose) {
|
|
1790
|
+
return mongoose.models.Assistant || mongoose.model('Assistant', assistantSchema);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Creates or returns the File model using the provided mongoose instance and schema
|
|
1795
|
+
*/
|
|
1796
|
+
function createFileModel(mongoose) {
|
|
1797
|
+
return mongoose.models.File || mongoose.model('File', file);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Creates or returns the Banner model using the provided mongoose instance and schema
|
|
1802
|
+
*/
|
|
1803
|
+
function createBannerModel(mongoose) {
|
|
1804
|
+
return mongoose.models.Banner || mongoose.model('Banner', bannerSchema);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* Creates or returns the Project model using the provided mongoose instance and schema
|
|
1809
|
+
*/
|
|
1810
|
+
function createProjectModel(mongoose) {
|
|
1811
|
+
return mongoose.models.Project || mongoose.model('Project', projectSchema);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Creates or returns the Key model using the provided mongoose instance and schema
|
|
1816
|
+
*/
|
|
1817
|
+
function createKeyModel(mongoose) {
|
|
1818
|
+
return mongoose.models.Key || mongoose.model('Key', keySchema);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* Creates or returns the PluginAuth model using the provided mongoose instance and schema
|
|
1823
|
+
*/
|
|
1824
|
+
function createPluginAuthModel(mongoose) {
|
|
1825
|
+
return mongoose.models.PluginAuth || mongoose.model('PluginAuth', pluginAuthSchema);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Creates or returns the Transaction model using the provided mongoose instance and schema
|
|
1830
|
+
*/
|
|
1831
|
+
function createTransactionModel(mongoose) {
|
|
1832
|
+
return (mongoose.models.Transaction || mongoose.model('Transaction', transactionSchema));
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Creates or returns the Preset model using the provided mongoose instance and schema
|
|
1837
|
+
*/
|
|
1838
|
+
function createPresetModel(mongoose) {
|
|
1839
|
+
return mongoose.models.Preset || mongoose.model('Preset', presetSchema);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Creates or returns the Prompt model using the provided mongoose instance and schema
|
|
1844
|
+
*/
|
|
1845
|
+
function createPromptModel(mongoose) {
|
|
1846
|
+
return mongoose.models.Prompt || mongoose.model('Prompt', promptSchema);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* Creates or returns the PromptGroup model using the provided mongoose instance and schema
|
|
1851
|
+
*/
|
|
1852
|
+
function createPromptGroupModel(mongoose) {
|
|
1853
|
+
return (mongoose.models.PromptGroup ||
|
|
1854
|
+
mongoose.model('PromptGroup', promptGroupSchema));
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Creates or returns the ConversationTag model using the provided mongoose instance and schema
|
|
1859
|
+
*/
|
|
1860
|
+
function createConversationTagModel(mongoose) {
|
|
1861
|
+
return (mongoose.models.ConversationTag ||
|
|
1862
|
+
mongoose.model('ConversationTag', conversationTag));
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* Creates or returns the SharedLink model using the provided mongoose instance and schema
|
|
1867
|
+
*/
|
|
1868
|
+
function createSharedLinkModel(mongoose) {
|
|
1869
|
+
return mongoose.models.SharedLink || mongoose.model('SharedLink', shareSchema);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Creates or returns the ToolCall model using the provided mongoose instance and schema
|
|
1874
|
+
*/
|
|
1875
|
+
function createToolCallModel(mongoose) {
|
|
1876
|
+
return mongoose.models.ToolCall || mongoose.model('ToolCall', toolCallSchema);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function createMemoryModel(mongoose) {
|
|
1880
|
+
return mongoose.models.MemoryEntry || mongoose.model('MemoryEntry', MemoryEntrySchema);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* Creates all database models for all collections
|
|
1885
|
+
*/
|
|
1886
|
+
function createModels(mongoose) {
|
|
1887
|
+
return {
|
|
1888
|
+
User: createUserModel(mongoose),
|
|
1889
|
+
Token: createTokenModel(mongoose),
|
|
1890
|
+
Session: createSessionModel(mongoose),
|
|
1891
|
+
Balance: createBalanceModel(mongoose),
|
|
1892
|
+
Conversation: createConversationModel(mongoose),
|
|
1893
|
+
Message: createMessageModel(mongoose),
|
|
1894
|
+
Agent: createAgentModel(mongoose),
|
|
1895
|
+
Role: createRoleModel(mongoose),
|
|
1896
|
+
Action: createActionModel(mongoose),
|
|
1897
|
+
Assistant: createAssistantModel(mongoose),
|
|
1898
|
+
File: createFileModel(mongoose),
|
|
1899
|
+
Banner: createBannerModel(mongoose),
|
|
1900
|
+
Project: createProjectModel(mongoose),
|
|
1901
|
+
Key: createKeyModel(mongoose),
|
|
1902
|
+
PluginAuth: createPluginAuthModel(mongoose),
|
|
1903
|
+
Transaction: createTransactionModel(mongoose),
|
|
1904
|
+
Preset: createPresetModel(mongoose),
|
|
1905
|
+
Prompt: createPromptModel(mongoose),
|
|
1906
|
+
PromptGroup: createPromptGroupModel(mongoose),
|
|
1907
|
+
ConversationTag: createConversationTagModel(mongoose),
|
|
1908
|
+
SharedLink: createSharedLinkModel(mongoose),
|
|
1909
|
+
ToolCall: createToolCallModel(mongoose),
|
|
1910
|
+
MemoryEntry: createMemoryModel(mongoose),
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/** Factory function that takes mongoose instance and returns the methods */
|
|
1915
|
+
function createUserMethods(mongoose) {
|
|
1916
|
+
/**
|
|
1917
|
+
* Search for a single user based on partial data and return matching user document as plain object.
|
|
1918
|
+
*/
|
|
1919
|
+
async function findUser(searchCriteria, fieldsToSelect) {
|
|
1920
|
+
const User = mongoose.models.User;
|
|
1921
|
+
const query = User.findOne(searchCriteria);
|
|
1922
|
+
if (fieldsToSelect) {
|
|
1923
|
+
query.select(fieldsToSelect);
|
|
1924
|
+
}
|
|
1925
|
+
return (await query.lean());
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Count the number of user documents in the collection based on the provided filter.
|
|
1929
|
+
*/
|
|
1930
|
+
async function countUsers(filter = {}) {
|
|
1931
|
+
const User = mongoose.models.User;
|
|
1932
|
+
return await User.countDocuments(filter);
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Creates a new user, optionally with a TTL of 1 week.
|
|
1936
|
+
*/
|
|
1937
|
+
async function createUser(data, balanceConfig, disableTTL = true, returnUser = false) {
|
|
1938
|
+
const User = mongoose.models.User;
|
|
1939
|
+
const Balance = mongoose.models.Balance;
|
|
1940
|
+
const userData = {
|
|
1941
|
+
...data,
|
|
1942
|
+
expiresAt: disableTTL ? undefined : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds
|
|
1943
|
+
};
|
|
1944
|
+
if (disableTTL) {
|
|
1945
|
+
delete userData.expiresAt;
|
|
1946
|
+
}
|
|
1947
|
+
const user = await User.create(userData);
|
|
1948
|
+
// If balance is enabled, create or update a balance record for the user
|
|
1949
|
+
if ((balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.enabled) && (balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.startBalance)) {
|
|
1950
|
+
const update = {
|
|
1951
|
+
$inc: { tokenCredits: balanceConfig.startBalance },
|
|
1952
|
+
};
|
|
1953
|
+
if (balanceConfig.autoRefillEnabled &&
|
|
1954
|
+
balanceConfig.refillIntervalValue != null &&
|
|
1955
|
+
balanceConfig.refillIntervalUnit != null &&
|
|
1956
|
+
balanceConfig.refillAmount != null) {
|
|
1957
|
+
update.$set = {
|
|
1958
|
+
autoRefillEnabled: true,
|
|
1959
|
+
refillIntervalValue: balanceConfig.refillIntervalValue,
|
|
1960
|
+
refillIntervalUnit: balanceConfig.refillIntervalUnit,
|
|
1961
|
+
refillAmount: balanceConfig.refillAmount,
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
await Balance.findOneAndUpdate({ user: user._id }, update, {
|
|
1965
|
+
upsert: true,
|
|
1966
|
+
new: true,
|
|
1967
|
+
}).lean();
|
|
1968
|
+
}
|
|
1969
|
+
if (returnUser) {
|
|
1970
|
+
return user.toObject();
|
|
1971
|
+
}
|
|
1972
|
+
return user._id;
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Update a user with new data without overwriting existing properties.
|
|
1976
|
+
*/
|
|
1977
|
+
async function updateUser(userId, updateData) {
|
|
1978
|
+
const User = mongoose.models.User;
|
|
1979
|
+
const updateOperation = {
|
|
1980
|
+
$set: updateData,
|
|
1981
|
+
$unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL
|
|
1982
|
+
};
|
|
1983
|
+
return (await User.findByIdAndUpdate(userId, updateOperation, {
|
|
1984
|
+
new: true,
|
|
1985
|
+
runValidators: true,
|
|
1986
|
+
}).lean());
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Retrieve a user by ID and convert the found user document to a plain object.
|
|
1990
|
+
*/
|
|
1991
|
+
async function getUserById(userId, fieldsToSelect) {
|
|
1992
|
+
const User = mongoose.models.User;
|
|
1993
|
+
const query = User.findById(userId);
|
|
1994
|
+
if (fieldsToSelect) {
|
|
1995
|
+
query.select(fieldsToSelect);
|
|
1996
|
+
}
|
|
1997
|
+
return (await query.lean());
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Delete a user by their unique ID.
|
|
2001
|
+
*/
|
|
2002
|
+
async function deleteUserById(userId) {
|
|
2003
|
+
try {
|
|
2004
|
+
const User = mongoose.models.User;
|
|
2005
|
+
const result = await User.deleteOne({ _id: userId });
|
|
2006
|
+
if (result.deletedCount === 0) {
|
|
2007
|
+
return { deletedCount: 0, message: 'No user found with that ID.' };
|
|
2008
|
+
}
|
|
2009
|
+
return { deletedCount: result.deletedCount, message: 'User was deleted successfully.' };
|
|
2010
|
+
}
|
|
2011
|
+
catch (error) {
|
|
2012
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
2013
|
+
throw new Error('Error deleting user: ' + errorMessage);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Generates a JWT token for a given user.
|
|
2018
|
+
*/
|
|
2019
|
+
async function generateToken(user) {
|
|
2020
|
+
if (!user) {
|
|
2021
|
+
throw new Error('No user provided');
|
|
2022
|
+
}
|
|
2023
|
+
let expires = 1000 * 60 * 15;
|
|
2024
|
+
if (process.env.SESSION_EXPIRY !== undefined && process.env.SESSION_EXPIRY !== '') {
|
|
2025
|
+
try {
|
|
2026
|
+
const evaluated = eval(process.env.SESSION_EXPIRY);
|
|
2027
|
+
if (evaluated) {
|
|
2028
|
+
expires = evaluated;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
catch (error) {
|
|
2032
|
+
console.warn('Invalid SESSION_EXPIRY expression, using default:', error);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return await signPayload({
|
|
2036
|
+
payload: {
|
|
2037
|
+
id: user._id,
|
|
2038
|
+
username: user.username,
|
|
2039
|
+
provider: user.provider,
|
|
2040
|
+
email: user.email,
|
|
2041
|
+
},
|
|
2042
|
+
secret: process.env.JWT_SECRET,
|
|
2043
|
+
expirationTime: expires / 1000,
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Update a user's personalization memories setting.
|
|
2048
|
+
* Handles the edge case where the personalization object doesn't exist.
|
|
2049
|
+
*/
|
|
2050
|
+
async function toggleUserMemories(userId, memoriesEnabled) {
|
|
2051
|
+
const User = mongoose.models.User;
|
|
2052
|
+
// First, ensure the personalization object exists
|
|
2053
|
+
const user = await User.findById(userId);
|
|
2054
|
+
if (!user) {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
// Use $set to update the nested field, which will create the personalization object if it doesn't exist
|
|
2058
|
+
const updateOperation = {
|
|
2059
|
+
$set: {
|
|
2060
|
+
'personalization.memories': memoriesEnabled,
|
|
2061
|
+
},
|
|
2062
|
+
};
|
|
2063
|
+
return (await User.findByIdAndUpdate(userId, updateOperation, {
|
|
2064
|
+
new: true,
|
|
2065
|
+
runValidators: true,
|
|
2066
|
+
}).lean());
|
|
2067
|
+
}
|
|
2068
|
+
// Return all methods
|
|
2069
|
+
return {
|
|
2070
|
+
findUser,
|
|
2071
|
+
countUsers,
|
|
2072
|
+
createUser,
|
|
2073
|
+
updateUser,
|
|
2074
|
+
getUserById,
|
|
2075
|
+
deleteUserById,
|
|
2076
|
+
generateToken,
|
|
2077
|
+
toggleUserMemories,
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
const SPLAT_SYMBOL = Symbol.for('splat');
|
|
2082
|
+
const MESSAGE_SYMBOL = Symbol.for('message');
|
|
2083
|
+
const CONSOLE_JSON_STRING_LENGTH = parseInt(process.env.CONSOLE_JSON_STRING_LENGTH || '', 10) || 255;
|
|
2084
|
+
const sensitiveKeys = [
|
|
2085
|
+
/^(sk-)[^\s]+/, // OpenAI API key pattern
|
|
2086
|
+
/(Bearer )[^\s]+/, // Header: Bearer token pattern
|
|
2087
|
+
/(api-key:? )[^\s]+/, // Header: API key pattern
|
|
2088
|
+
/(key=)[^\s]+/, // URL query param: sensitive key pattern (Google)
|
|
2089
|
+
];
|
|
2090
|
+
/**
|
|
2091
|
+
* Determines if a given value string is sensitive and returns matching regex patterns.
|
|
2092
|
+
*
|
|
2093
|
+
* @param valueStr - The value string to check.
|
|
2094
|
+
* @returns An array of regex patterns that match the value string.
|
|
2095
|
+
*/
|
|
2096
|
+
function getMatchingSensitivePatterns(valueStr) {
|
|
2097
|
+
if (valueStr) {
|
|
2098
|
+
// Filter and return all regex patterns that match the value string
|
|
2099
|
+
return sensitiveKeys.filter((regex) => regex.test(valueStr));
|
|
2100
|
+
}
|
|
2101
|
+
return [];
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Redacts sensitive information from a console message and trims it to a specified length if provided.
|
|
2105
|
+
* @param str - The console message to be redacted.
|
|
2106
|
+
* @param trimLength - The optional length at which to trim the redacted message.
|
|
2107
|
+
* @returns The redacted and optionally trimmed console message.
|
|
2108
|
+
*/
|
|
2109
|
+
function redactMessage(str, trimLength) {
|
|
2110
|
+
if (!str) {
|
|
2111
|
+
return '';
|
|
2112
|
+
}
|
|
2113
|
+
const patterns = getMatchingSensitivePatterns(str);
|
|
2114
|
+
patterns.forEach((pattern) => {
|
|
2115
|
+
str = str.replace(pattern, '$1[REDACTED]');
|
|
2116
|
+
});
|
|
2117
|
+
return str;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Redacts sensitive information from log messages if the log level is 'error'.
|
|
2121
|
+
* Note: Intentionally mutates the object.
|
|
2122
|
+
* @param info - The log information object.
|
|
2123
|
+
* @returns The modified log information object.
|
|
2124
|
+
*/
|
|
2125
|
+
const redactFormat = winston.format((info) => {
|
|
2126
|
+
if (info.level === 'error') {
|
|
2127
|
+
// Type guard to ensure message is a string
|
|
2128
|
+
if (typeof info.message === 'string') {
|
|
2129
|
+
info.message = redactMessage(info.message);
|
|
2130
|
+
}
|
|
2131
|
+
// Handle MESSAGE_SYMBOL with type safety
|
|
2132
|
+
const symbolValue = info[MESSAGE_SYMBOL];
|
|
2133
|
+
if (typeof symbolValue === 'string') {
|
|
2134
|
+
info[MESSAGE_SYMBOL] = redactMessage(symbolValue);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return info;
|
|
2138
|
+
});
|
|
2139
|
+
/**
|
|
2140
|
+
* Truncates long strings, especially base64 image data, within log messages.
|
|
2141
|
+
*
|
|
2142
|
+
* @param value - The value to be inspected and potentially truncated.
|
|
2143
|
+
* @param length - The length at which to truncate the value. Default: 100.
|
|
2144
|
+
* @returns The truncated or original value.
|
|
2145
|
+
*/
|
|
2146
|
+
const truncateLongStrings = (value, length = 100) => {
|
|
2147
|
+
if (typeof value === 'string') {
|
|
2148
|
+
return value.length > length ? value.substring(0, length) + '... [truncated]' : value;
|
|
2149
|
+
}
|
|
2150
|
+
return value;
|
|
2151
|
+
};
|
|
2152
|
+
/**
|
|
2153
|
+
* An array mapping function that truncates long strings (objects converted to JSON strings).
|
|
2154
|
+
* @param item - The item to be condensed.
|
|
2155
|
+
* @returns The condensed item.
|
|
2156
|
+
*/
|
|
2157
|
+
const condenseArray = (item) => {
|
|
2158
|
+
if (typeof item === 'string') {
|
|
2159
|
+
return truncateLongStrings(JSON.stringify(item));
|
|
2160
|
+
}
|
|
2161
|
+
else if (typeof item === 'object') {
|
|
2162
|
+
return truncateLongStrings(JSON.stringify(item));
|
|
2163
|
+
}
|
|
2164
|
+
return item;
|
|
2165
|
+
};
|
|
2166
|
+
/**
|
|
2167
|
+
* Formats log messages for debugging purposes.
|
|
2168
|
+
* - Truncates long strings within log messages.
|
|
2169
|
+
* - Condenses arrays by truncating long strings and objects as strings within array items.
|
|
2170
|
+
* - Redacts sensitive information from log messages if the log level is 'error'.
|
|
2171
|
+
* - Converts log information object to a formatted string.
|
|
2172
|
+
*
|
|
2173
|
+
* @param options - The options for formatting log messages.
|
|
2174
|
+
* @returns The formatted log message.
|
|
2175
|
+
*/
|
|
2176
|
+
const debugTraverse = winston.format.printf(({ level, message, timestamp, ...metadata }) => {
|
|
2177
|
+
if (!message) {
|
|
2178
|
+
return `${timestamp} ${level}`;
|
|
2179
|
+
}
|
|
2180
|
+
// Type-safe version of the CJS logic: !message?.trim || typeof message !== 'string'
|
|
2181
|
+
if (typeof message !== 'string' || !message.trim) {
|
|
2182
|
+
return `${timestamp} ${level}: ${JSON.stringify(message)}`;
|
|
2183
|
+
}
|
|
2184
|
+
let msg = `${timestamp} ${level}: ${truncateLongStrings(message.trim(), 150)}`;
|
|
2185
|
+
try {
|
|
2186
|
+
if (level !== 'debug') {
|
|
2187
|
+
return msg;
|
|
2188
|
+
}
|
|
2189
|
+
if (!metadata) {
|
|
2190
|
+
return msg;
|
|
2191
|
+
}
|
|
2192
|
+
// Type-safe access to SPLAT_SYMBOL using bracket notation
|
|
2193
|
+
const metadataRecord = metadata;
|
|
2194
|
+
const splatArray = metadataRecord[SPLAT_SYMBOL];
|
|
2195
|
+
const debugValue = Array.isArray(splatArray) ? splatArray[0] : undefined;
|
|
2196
|
+
if (!debugValue) {
|
|
2197
|
+
return msg;
|
|
2198
|
+
}
|
|
2199
|
+
if (debugValue && Array.isArray(debugValue)) {
|
|
2200
|
+
msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`;
|
|
2201
|
+
return msg;
|
|
2202
|
+
}
|
|
2203
|
+
if (typeof debugValue !== 'object') {
|
|
2204
|
+
return (msg += ` ${debugValue}`);
|
|
2205
|
+
}
|
|
2206
|
+
msg += '\n{';
|
|
2207
|
+
const copy = klona(metadata);
|
|
2208
|
+
traverse(copy).forEach(function (value) {
|
|
2209
|
+
var _a;
|
|
2210
|
+
if (typeof (this === null || this === void 0 ? void 0 : this.key) === 'symbol') {
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
let _parentKey = '';
|
|
2214
|
+
const parent = this.parent;
|
|
2215
|
+
if (typeof (parent === null || parent === void 0 ? void 0 : parent.key) !== 'symbol' && (parent === null || parent === void 0 ? void 0 : parent.key)) {
|
|
2216
|
+
_parentKey = parent.key;
|
|
2217
|
+
}
|
|
2218
|
+
const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`;
|
|
2219
|
+
const tabs = `${parent && parent.notRoot ? ' ' : ' '}`;
|
|
2220
|
+
const currentKey = (_a = this === null || this === void 0 ? void 0 : this.key) !== null && _a !== void 0 ? _a : 'unknown';
|
|
2221
|
+
if (this.isLeaf && typeof value === 'string') {
|
|
2222
|
+
const truncatedText = truncateLongStrings(value);
|
|
2223
|
+
msg += `\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`;
|
|
2224
|
+
}
|
|
2225
|
+
else if (this.notLeaf && Array.isArray(value) && value.length > 0) {
|
|
2226
|
+
const currentMessage = `\n${tabs}// ${value.length} ${currentKey.replace(/s$/, '')}(s)`;
|
|
2227
|
+
this.update(currentMessage, true);
|
|
2228
|
+
msg += currentMessage;
|
|
2229
|
+
const stringifiedArray = value.map(condenseArray);
|
|
2230
|
+
msg += `\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`;
|
|
2231
|
+
}
|
|
2232
|
+
else if (this.isLeaf && typeof value === 'function') {
|
|
2233
|
+
msg += `\n${tabs}${parentKey}${currentKey}: function,`;
|
|
2234
|
+
}
|
|
2235
|
+
else if (this.isLeaf) {
|
|
2236
|
+
msg += `\n${tabs}${parentKey}${currentKey}: ${value},`;
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
msg += '\n}';
|
|
2240
|
+
return msg;
|
|
2241
|
+
}
|
|
2242
|
+
catch (e) {
|
|
2243
|
+
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
|
2244
|
+
return (msg += `\n[LOGGER PARSING ERROR] ${errorMessage}`);
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
/**
|
|
2248
|
+
* Truncates long string values in JSON log objects.
|
|
2249
|
+
* Prevents outputting extremely long values (e.g., base64, blobs).
|
|
2250
|
+
*/
|
|
2251
|
+
const jsonTruncateFormat = winston.format((info) => {
|
|
2252
|
+
const truncateLongStrings = (str, maxLength) => str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
|
|
2253
|
+
const seen = new WeakSet();
|
|
2254
|
+
const truncateObject = (obj) => {
|
|
2255
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
2256
|
+
return obj;
|
|
2257
|
+
}
|
|
2258
|
+
// Handle circular references - now with proper object type
|
|
2259
|
+
if (seen.has(obj)) {
|
|
2260
|
+
return '[Circular]';
|
|
2261
|
+
}
|
|
2262
|
+
seen.add(obj);
|
|
2263
|
+
if (Array.isArray(obj)) {
|
|
2264
|
+
return obj.map((item) => truncateObject(item));
|
|
2265
|
+
}
|
|
2266
|
+
// We know this is an object at this point
|
|
2267
|
+
const objectRecord = obj;
|
|
2268
|
+
const newObj = {};
|
|
2269
|
+
Object.entries(objectRecord).forEach(([key, value]) => {
|
|
2270
|
+
if (typeof value === 'string') {
|
|
2271
|
+
newObj[key] = truncateLongStrings(value, CONSOLE_JSON_STRING_LENGTH);
|
|
2272
|
+
}
|
|
2273
|
+
else {
|
|
2274
|
+
newObj[key] = truncateObject(value);
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
return newObj;
|
|
2278
|
+
};
|
|
2279
|
+
return truncateObject(info);
|
|
2280
|
+
});
|
|
2281
|
+
|
|
2282
|
+
const logDir = path.join(__dirname, '..', '..', '..', 'api', 'logs');
|
|
2283
|
+
const { NODE_ENV, DEBUG_LOGGING, CONSOLE_JSON, DEBUG_CONSOLE } = process.env;
|
|
2284
|
+
const useConsoleJson = typeof CONSOLE_JSON === 'string' && CONSOLE_JSON.toLowerCase() === 'true';
|
|
2285
|
+
const useDebugConsole = typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE.toLowerCase() === 'true';
|
|
2286
|
+
const useDebugLogging = typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING.toLowerCase() === 'true';
|
|
2287
|
+
const levels = {
|
|
2288
|
+
error: 0,
|
|
2289
|
+
warn: 1,
|
|
2290
|
+
info: 2,
|
|
2291
|
+
http: 3,
|
|
2292
|
+
verbose: 4,
|
|
2293
|
+
debug: 5,
|
|
2294
|
+
activity: 6,
|
|
2295
|
+
silly: 7,
|
|
2296
|
+
};
|
|
2297
|
+
winston.addColors({
|
|
2298
|
+
info: 'green',
|
|
2299
|
+
warn: 'italic yellow',
|
|
2300
|
+
error: 'red',
|
|
2301
|
+
debug: 'blue',
|
|
2302
|
+
});
|
|
2303
|
+
const level = () => {
|
|
2304
|
+
const env = NODE_ENV || 'development';
|
|
2305
|
+
return env === 'development' ? 'debug' : 'warn';
|
|
2306
|
+
};
|
|
2307
|
+
const fileFormat = winston.format.combine(redactFormat(), winston.format.timestamp({ format: () => new Date().toISOString() }), winston.format.errors({ stack: true }), winston.format.splat());
|
|
2308
|
+
const transports = [
|
|
2309
|
+
new winston.transports.DailyRotateFile({
|
|
2310
|
+
level: 'error',
|
|
2311
|
+
filename: `${logDir}/error-%DATE%.log`,
|
|
2312
|
+
datePattern: 'YYYY-MM-DD',
|
|
2313
|
+
zippedArchive: true,
|
|
2314
|
+
maxSize: '20m',
|
|
2315
|
+
maxFiles: '14d',
|
|
2316
|
+
format: fileFormat,
|
|
2317
|
+
}),
|
|
2318
|
+
];
|
|
2319
|
+
if (useDebugLogging) {
|
|
2320
|
+
transports.push(new winston.transports.DailyRotateFile({
|
|
2321
|
+
level: 'debug',
|
|
2322
|
+
filename: `${logDir}/debug-%DATE%.log`,
|
|
2323
|
+
datePattern: 'YYYY-MM-DD',
|
|
2324
|
+
zippedArchive: true,
|
|
2325
|
+
maxSize: '20m',
|
|
2326
|
+
maxFiles: '14d',
|
|
2327
|
+
format: winston.format.combine(fileFormat, debugTraverse),
|
|
2328
|
+
}));
|
|
2329
|
+
}
|
|
2330
|
+
const consoleFormat = winston.format.combine(redactFormat(), winston.format.colorize({ all: true }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf((info) => {
|
|
2331
|
+
const message = `${info.timestamp} ${info.level}: ${info.message}`;
|
|
2332
|
+
return info.level.includes('error') ? redactMessage(message) : message;
|
|
2333
|
+
}));
|
|
2334
|
+
let consoleLogLevel = 'info';
|
|
2335
|
+
if (useDebugConsole) {
|
|
2336
|
+
consoleLogLevel = 'debug';
|
|
2337
|
+
}
|
|
2338
|
+
// Add console transport
|
|
2339
|
+
if (useDebugConsole) {
|
|
2340
|
+
transports.push(new winston.transports.Console({
|
|
2341
|
+
level: consoleLogLevel,
|
|
2342
|
+
format: useConsoleJson
|
|
2343
|
+
? winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json())
|
|
2344
|
+
: winston.format.combine(fileFormat, debugTraverse),
|
|
2345
|
+
}));
|
|
2346
|
+
}
|
|
2347
|
+
else if (useConsoleJson) {
|
|
2348
|
+
transports.push(new winston.transports.Console({
|
|
2349
|
+
level: consoleLogLevel,
|
|
2350
|
+
format: winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json()),
|
|
2351
|
+
}));
|
|
2352
|
+
}
|
|
2353
|
+
else {
|
|
2354
|
+
transports.push(new winston.transports.Console({
|
|
2355
|
+
level: consoleLogLevel,
|
|
2356
|
+
format: consoleFormat,
|
|
2357
|
+
}));
|
|
2358
|
+
}
|
|
2359
|
+
// Create logger
|
|
2360
|
+
const logger = winston.createLogger({
|
|
2361
|
+
level: level(),
|
|
2362
|
+
levels,
|
|
2363
|
+
transports,
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
var _a;
|
|
2367
|
+
class SessionError extends Error {
|
|
2368
|
+
constructor(message, code = 'SESSION_ERROR') {
|
|
2369
|
+
super(message);
|
|
2370
|
+
this.name = 'SessionError';
|
|
2371
|
+
this.code = code;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
const { REFRESH_TOKEN_EXPIRY } = (_a = process.env) !== null && _a !== void 0 ? _a : {};
|
|
2375
|
+
const expires = REFRESH_TOKEN_EXPIRY
|
|
2376
|
+
? eval(REFRESH_TOKEN_EXPIRY)
|
|
2377
|
+
: 1000 * 60 * 60 * 24 * 7; // 7 days default
|
|
2378
|
+
// Factory function that takes mongoose instance and returns the methods
|
|
2379
|
+
function createSessionMethods(mongoose) {
|
|
2380
|
+
const Session = mongoose.models.Session;
|
|
2381
|
+
/**
|
|
2382
|
+
* Creates a new session for a user
|
|
2383
|
+
*/
|
|
2384
|
+
async function createSession(userId, options = {}) {
|
|
2385
|
+
if (!userId) {
|
|
2386
|
+
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
|
2387
|
+
}
|
|
2388
|
+
try {
|
|
2389
|
+
const session = new Session({
|
|
2390
|
+
user: userId,
|
|
2391
|
+
expiration: options.expiration || new Date(Date.now() + expires),
|
|
2392
|
+
});
|
|
2393
|
+
const refreshToken = await generateRefreshToken(session);
|
|
2394
|
+
return { session, refreshToken };
|
|
2395
|
+
}
|
|
2396
|
+
catch (error) {
|
|
2397
|
+
logger.error('[createSession] Error creating session:', error);
|
|
2398
|
+
throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Finds a session by various parameters
|
|
2403
|
+
*/
|
|
2404
|
+
async function findSession(params, options = { lean: true }) {
|
|
2405
|
+
try {
|
|
2406
|
+
const query = {};
|
|
2407
|
+
if (!params.refreshToken && !params.userId && !params.sessionId) {
|
|
2408
|
+
throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS');
|
|
2409
|
+
}
|
|
2410
|
+
if (params.refreshToken) {
|
|
2411
|
+
const tokenHash = await hashToken(params.refreshToken);
|
|
2412
|
+
query.refreshTokenHash = tokenHash;
|
|
2413
|
+
}
|
|
2414
|
+
if (params.userId) {
|
|
2415
|
+
query.user = params.userId;
|
|
2416
|
+
}
|
|
2417
|
+
if (params.sessionId) {
|
|
2418
|
+
const sessionId = typeof params.sessionId === 'object' &&
|
|
2419
|
+
params.sessionId !== null &&
|
|
2420
|
+
'sessionId' in params.sessionId
|
|
2421
|
+
? params.sessionId.sessionId
|
|
2422
|
+
: params.sessionId;
|
|
2423
|
+
if (!mongoose.Types.ObjectId.isValid(sessionId)) {
|
|
2424
|
+
throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID');
|
|
2425
|
+
}
|
|
2426
|
+
query._id = sessionId;
|
|
2427
|
+
}
|
|
2428
|
+
// Add expiration check to only return valid sessions
|
|
2429
|
+
query.expiration = { $gt: new Date() };
|
|
2430
|
+
const sessionQuery = Session.findOne(query);
|
|
2431
|
+
if (options.lean) {
|
|
2432
|
+
return (await sessionQuery.lean());
|
|
2433
|
+
}
|
|
2434
|
+
return await sessionQuery.exec();
|
|
2435
|
+
}
|
|
2436
|
+
catch (error) {
|
|
2437
|
+
logger.error('[findSession] Error finding session:', error);
|
|
2438
|
+
throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED');
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Updates session expiration
|
|
2443
|
+
*/
|
|
2444
|
+
async function updateExpiration(session, newExpiration) {
|
|
2445
|
+
try {
|
|
2446
|
+
const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session;
|
|
2447
|
+
if (!sessionDoc) {
|
|
2448
|
+
throw new SessionError('Session not found', 'SESSION_NOT_FOUND');
|
|
2449
|
+
}
|
|
2450
|
+
sessionDoc.expiration = newExpiration || new Date(Date.now() + expires);
|
|
2451
|
+
return await sessionDoc.save();
|
|
2452
|
+
}
|
|
2453
|
+
catch (error) {
|
|
2454
|
+
logger.error('[updateExpiration] Error updating session:', error);
|
|
2455
|
+
throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED');
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Deletes a session by refresh token or session ID
|
|
2460
|
+
*/
|
|
2461
|
+
async function deleteSession(params) {
|
|
2462
|
+
try {
|
|
2463
|
+
if (!params.refreshToken && !params.sessionId) {
|
|
2464
|
+
throw new SessionError('Either refreshToken or sessionId is required', 'INVALID_DELETE_PARAMS');
|
|
2465
|
+
}
|
|
2466
|
+
const query = {};
|
|
2467
|
+
if (params.refreshToken) {
|
|
2468
|
+
query.refreshTokenHash = await hashToken(params.refreshToken);
|
|
2469
|
+
}
|
|
2470
|
+
if (params.sessionId) {
|
|
2471
|
+
query._id = params.sessionId;
|
|
2472
|
+
}
|
|
2473
|
+
const result = await Session.deleteOne(query);
|
|
2474
|
+
if (result.deletedCount === 0) {
|
|
2475
|
+
logger.warn('[deleteSession] No session found to delete');
|
|
2476
|
+
}
|
|
2477
|
+
return result;
|
|
2478
|
+
}
|
|
2479
|
+
catch (error) {
|
|
2480
|
+
logger.error('[deleteSession] Error deleting session:', error);
|
|
2481
|
+
throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Deletes all sessions for a user
|
|
2486
|
+
*/
|
|
2487
|
+
async function deleteAllUserSessions(userId, options = {}) {
|
|
2488
|
+
try {
|
|
2489
|
+
if (!userId) {
|
|
2490
|
+
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
|
2491
|
+
}
|
|
2492
|
+
const userIdString = typeof userId === 'object' && userId !== null ? userId.userId : userId;
|
|
2493
|
+
if (!mongoose.Types.ObjectId.isValid(userIdString)) {
|
|
2494
|
+
throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT');
|
|
2495
|
+
}
|
|
2496
|
+
const query = { user: userIdString };
|
|
2497
|
+
if (options.excludeCurrentSession && options.currentSessionId) {
|
|
2498
|
+
query._id = { $ne: options.currentSessionId };
|
|
2499
|
+
}
|
|
2500
|
+
const result = await Session.deleteMany(query);
|
|
2501
|
+
if (result.deletedCount && result.deletedCount > 0) {
|
|
2502
|
+
logger.debug(`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`);
|
|
2503
|
+
}
|
|
2504
|
+
return result;
|
|
2505
|
+
}
|
|
2506
|
+
catch (error) {
|
|
2507
|
+
logger.error('[deleteAllUserSessions] Error deleting user sessions:', error);
|
|
2508
|
+
throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Generates a refresh token for a session
|
|
2513
|
+
*/
|
|
2514
|
+
async function generateRefreshToken(session) {
|
|
2515
|
+
if (!session || !session.user) {
|
|
2516
|
+
throw new SessionError('Invalid session object', 'INVALID_SESSION');
|
|
2517
|
+
}
|
|
2518
|
+
try {
|
|
2519
|
+
const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
|
|
2520
|
+
if (!session.expiration) {
|
|
2521
|
+
session.expiration = new Date(expiresIn);
|
|
2522
|
+
}
|
|
2523
|
+
const refreshToken = await signPayload({
|
|
2524
|
+
payload: {
|
|
2525
|
+
id: session.user,
|
|
2526
|
+
sessionId: session._id,
|
|
2527
|
+
},
|
|
2528
|
+
secret: process.env.JWT_REFRESH_SECRET,
|
|
2529
|
+
expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
|
|
2530
|
+
});
|
|
2531
|
+
session.refreshTokenHash = await hashToken(refreshToken);
|
|
2532
|
+
await session.save();
|
|
2533
|
+
return refreshToken;
|
|
2534
|
+
}
|
|
2535
|
+
catch (error) {
|
|
2536
|
+
logger.error('[generateRefreshToken] Error generating refresh token:', error);
|
|
2537
|
+
throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Counts active sessions for a user
|
|
2542
|
+
*/
|
|
2543
|
+
async function countActiveSessions(userId) {
|
|
2544
|
+
try {
|
|
2545
|
+
if (!userId) {
|
|
2546
|
+
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
|
2547
|
+
}
|
|
2548
|
+
return await Session.countDocuments({
|
|
2549
|
+
user: userId,
|
|
2550
|
+
expiration: { $gt: new Date() },
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
catch (error) {
|
|
2554
|
+
logger.error('[countActiveSessions] Error counting active sessions:', error);
|
|
2555
|
+
throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED');
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return {
|
|
2559
|
+
findSession,
|
|
2560
|
+
SessionError,
|
|
2561
|
+
deleteSession,
|
|
2562
|
+
createSession,
|
|
2563
|
+
updateExpiration,
|
|
2564
|
+
countActiveSessions,
|
|
2565
|
+
generateRefreshToken,
|
|
2566
|
+
deleteAllUserSessions,
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// Factory function that takes mongoose instance and returns the methods
|
|
2571
|
+
function createTokenMethods(mongoose) {
|
|
2572
|
+
/**
|
|
2573
|
+
* Creates a new Token instance.
|
|
2574
|
+
*/
|
|
2575
|
+
async function createToken(tokenData) {
|
|
2576
|
+
try {
|
|
2577
|
+
const Token = mongoose.models.Token;
|
|
2578
|
+
const currentTime = new Date();
|
|
2579
|
+
const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000);
|
|
2580
|
+
const newTokenData = {
|
|
2581
|
+
...tokenData,
|
|
2582
|
+
createdAt: currentTime,
|
|
2583
|
+
expiresAt,
|
|
2584
|
+
};
|
|
2585
|
+
return await Token.create(newTokenData);
|
|
2586
|
+
}
|
|
2587
|
+
catch (error) {
|
|
2588
|
+
logger.debug('An error occurred while creating token:', error);
|
|
2589
|
+
throw error;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Updates a Token document that matches the provided query.
|
|
2594
|
+
*/
|
|
2595
|
+
async function updateToken(query, updateData) {
|
|
2596
|
+
try {
|
|
2597
|
+
const Token = mongoose.models.Token;
|
|
2598
|
+
return await Token.findOneAndUpdate(query, updateData, { new: true });
|
|
2599
|
+
}
|
|
2600
|
+
catch (error) {
|
|
2601
|
+
logger.debug('An error occurred while updating token:', error);
|
|
2602
|
+
throw error;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Deletes all Token documents that match the provided token, user ID, or email.
|
|
2607
|
+
*/
|
|
2608
|
+
async function deleteTokens(query) {
|
|
2609
|
+
try {
|
|
2610
|
+
const Token = mongoose.models.Token;
|
|
2611
|
+
return await Token.deleteMany({
|
|
2612
|
+
$or: [
|
|
2613
|
+
{ userId: query.userId },
|
|
2614
|
+
{ token: query.token },
|
|
2615
|
+
{ email: query.email },
|
|
2616
|
+
{ identifier: query.identifier },
|
|
2617
|
+
],
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
catch (error) {
|
|
2621
|
+
logger.debug('An error occurred while deleting tokens:', error);
|
|
2622
|
+
throw error;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Finds a Token document that matches the provided query.
|
|
2627
|
+
*/
|
|
2628
|
+
async function findToken(query) {
|
|
2629
|
+
try {
|
|
2630
|
+
const Token = mongoose.models.Token;
|
|
2631
|
+
const conditions = [];
|
|
2632
|
+
if (query.userId) {
|
|
2633
|
+
conditions.push({ userId: query.userId });
|
|
2634
|
+
}
|
|
2635
|
+
if (query.token) {
|
|
2636
|
+
conditions.push({ token: query.token });
|
|
2637
|
+
}
|
|
2638
|
+
if (query.email) {
|
|
2639
|
+
conditions.push({ email: query.email });
|
|
2640
|
+
}
|
|
2641
|
+
if (query.identifier) {
|
|
2642
|
+
conditions.push({ identifier: query.identifier });
|
|
2643
|
+
}
|
|
2644
|
+
const token = await Token.findOne({
|
|
2645
|
+
$and: conditions,
|
|
2646
|
+
}).lean();
|
|
2647
|
+
return token;
|
|
2648
|
+
}
|
|
2649
|
+
catch (error) {
|
|
2650
|
+
logger.debug('An error occurred while finding token:', error);
|
|
2651
|
+
throw error;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
// Return all methods
|
|
2655
|
+
return {
|
|
2656
|
+
findToken,
|
|
2657
|
+
createToken,
|
|
2658
|
+
updateToken,
|
|
2659
|
+
deleteTokens,
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// Factory function that takes mongoose instance and returns the methods
|
|
2664
|
+
function createRoleMethods(mongoose) {
|
|
2665
|
+
/**
|
|
2666
|
+
* Initialize default roles in the system.
|
|
2667
|
+
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
|
|
2668
|
+
* Updates existing roles with new permission types if they're missing.
|
|
2669
|
+
*/
|
|
2670
|
+
async function initializeRoles() {
|
|
2671
|
+
const Role = mongoose.models.Role;
|
|
2672
|
+
for (const roleName of [SystemRoles.ADMIN, SystemRoles.USER]) {
|
|
2673
|
+
let role = await Role.findOne({ name: roleName });
|
|
2674
|
+
const defaultPerms = roleDefaults[roleName].permissions;
|
|
2675
|
+
if (!role) {
|
|
2676
|
+
// Create new role if it doesn't exist.
|
|
2677
|
+
role = new Role(roleDefaults[roleName]);
|
|
2678
|
+
}
|
|
2679
|
+
else {
|
|
2680
|
+
// Ensure role.permissions is defined.
|
|
2681
|
+
role.permissions = role.permissions || {};
|
|
2682
|
+
// For each permission type in defaults, add it if missing.
|
|
2683
|
+
for (const permType of Object.keys(defaultPerms)) {
|
|
2684
|
+
if (role.permissions[permType] == null) {
|
|
2685
|
+
role.permissions[permType] = defaultPerms[permType];
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
await role.save();
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* List all roles in the system (for testing purposes)
|
|
2694
|
+
* Returns an array of all roles with their names and permissions
|
|
2695
|
+
*/
|
|
2696
|
+
async function listRoles() {
|
|
2697
|
+
const Role = mongoose.models.Role;
|
|
2698
|
+
return await Role.find({}).select('name permissions').lean();
|
|
2699
|
+
}
|
|
2700
|
+
// Return all methods you want to expose
|
|
2701
|
+
return {
|
|
2702
|
+
listRoles,
|
|
2703
|
+
initializeRoles,
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
/**
|
|
2708
|
+
* Formats a date in YYYY-MM-DD format
|
|
2709
|
+
*/
|
|
2710
|
+
const formatDate = (date) => {
|
|
2711
|
+
return date.toISOString().split('T')[0];
|
|
2712
|
+
};
|
|
2713
|
+
// Factory function that takes mongoose instance and returns the methods
|
|
2714
|
+
function createMemoryMethods(mongoose) {
|
|
2715
|
+
const MemoryEntry = mongoose.models.MemoryEntry;
|
|
2716
|
+
/**
|
|
2717
|
+
* Creates a new memory entry for a user
|
|
2718
|
+
* Throws an error if a memory with the same key already exists
|
|
2719
|
+
*/
|
|
2720
|
+
async function createMemory({ userId, key, value, tokenCount = 0, }) {
|
|
2721
|
+
try {
|
|
2722
|
+
if ((key === null || key === void 0 ? void 0 : key.toLowerCase()) === 'nothing') {
|
|
2723
|
+
return { ok: false };
|
|
2724
|
+
}
|
|
2725
|
+
const existingMemory = await MemoryEntry.findOne({ userId, key });
|
|
2726
|
+
if (existingMemory) {
|
|
2727
|
+
throw new Error('Memory with this key already exists');
|
|
2728
|
+
}
|
|
2729
|
+
await MemoryEntry.create({
|
|
2730
|
+
userId,
|
|
2731
|
+
key,
|
|
2732
|
+
value,
|
|
2733
|
+
tokenCount,
|
|
2734
|
+
updated_at: new Date(),
|
|
2735
|
+
});
|
|
2736
|
+
return { ok: true };
|
|
2737
|
+
}
|
|
2738
|
+
catch (error) {
|
|
2739
|
+
throw new Error(`Failed to create memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Sets or updates a memory entry for a user
|
|
2744
|
+
*/
|
|
2745
|
+
async function setMemory({ userId, key, value, tokenCount = 0, }) {
|
|
2746
|
+
try {
|
|
2747
|
+
if ((key === null || key === void 0 ? void 0 : key.toLowerCase()) === 'nothing') {
|
|
2748
|
+
return { ok: false };
|
|
2749
|
+
}
|
|
2750
|
+
await MemoryEntry.findOneAndUpdate({ userId, key }, {
|
|
2751
|
+
value,
|
|
2752
|
+
tokenCount,
|
|
2753
|
+
updated_at: new Date(),
|
|
2754
|
+
}, {
|
|
2755
|
+
upsert: true,
|
|
2756
|
+
new: true,
|
|
2757
|
+
});
|
|
2758
|
+
return { ok: true };
|
|
2759
|
+
}
|
|
2760
|
+
catch (error) {
|
|
2761
|
+
throw new Error(`Failed to set memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Deletes a specific memory entry for a user
|
|
2766
|
+
*/
|
|
2767
|
+
async function deleteMemory({ userId, key }) {
|
|
2768
|
+
try {
|
|
2769
|
+
const result = await MemoryEntry.findOneAndDelete({ userId, key });
|
|
2770
|
+
return { ok: !!result };
|
|
2771
|
+
}
|
|
2772
|
+
catch (error) {
|
|
2773
|
+
throw new Error(`Failed to delete memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Gets all memory entries for a user
|
|
2778
|
+
*/
|
|
2779
|
+
async function getAllUserMemories(userId) {
|
|
2780
|
+
try {
|
|
2781
|
+
return (await MemoryEntry.find({ userId }).lean());
|
|
2782
|
+
}
|
|
2783
|
+
catch (error) {
|
|
2784
|
+
throw new Error(`Failed to get all memories: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Gets and formats all memories for a user in two different formats
|
|
2789
|
+
*/
|
|
2790
|
+
async function getFormattedMemories({ userId, }) {
|
|
2791
|
+
try {
|
|
2792
|
+
const memories = await getAllUserMemories(userId);
|
|
2793
|
+
if (!memories || memories.length === 0) {
|
|
2794
|
+
return { withKeys: '', withoutKeys: '', totalTokens: 0 };
|
|
2795
|
+
}
|
|
2796
|
+
const sortedMemories = memories.sort((a, b) => new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime());
|
|
2797
|
+
const totalTokens = sortedMemories.reduce((sum, memory) => {
|
|
2798
|
+
return sum + (memory.tokenCount || 0);
|
|
2799
|
+
}, 0);
|
|
2800
|
+
const withKeys = sortedMemories
|
|
2801
|
+
.map((memory, index) => {
|
|
2802
|
+
const date = formatDate(new Date(memory.updated_at));
|
|
2803
|
+
const tokenInfo = memory.tokenCount ? ` [${memory.tokenCount} tokens]` : '';
|
|
2804
|
+
return `${index + 1}. [${date}]. ["key": "${memory.key}"]${tokenInfo}. ["value": "${memory.value}"]`;
|
|
2805
|
+
})
|
|
2806
|
+
.join('\n\n');
|
|
2807
|
+
const withoutKeys = sortedMemories
|
|
2808
|
+
.map((memory, index) => {
|
|
2809
|
+
const date = formatDate(new Date(memory.updated_at));
|
|
2810
|
+
return `${index + 1}. [${date}]. ${memory.value}`;
|
|
2811
|
+
})
|
|
2812
|
+
.join('\n\n');
|
|
2813
|
+
return { withKeys, withoutKeys, totalTokens };
|
|
2814
|
+
}
|
|
2815
|
+
catch (error) {
|
|
2816
|
+
logger.error('Failed to get formatted memories:', error);
|
|
2817
|
+
return { withKeys: '', withoutKeys: '', totalTokens: 0 };
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
return {
|
|
2821
|
+
setMemory,
|
|
2822
|
+
createMemory,
|
|
2823
|
+
deleteMemory,
|
|
2824
|
+
getAllUserMemories,
|
|
2825
|
+
getFormattedMemories,
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
class ShareServiceError extends Error {
|
|
2830
|
+
constructor(message, code) {
|
|
2831
|
+
super(message);
|
|
2832
|
+
this.name = 'ShareServiceError';
|
|
2833
|
+
this.code = code;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
function memoizedAnonymizeId(prefix) {
|
|
2837
|
+
const memo = new Map();
|
|
2838
|
+
return (id) => {
|
|
2839
|
+
if (!memo.has(id)) {
|
|
2840
|
+
memo.set(id, `${prefix}_${nanoid()}`);
|
|
2841
|
+
}
|
|
2842
|
+
return memo.get(id);
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
const anonymizeConvoId = memoizedAnonymizeId('convo');
|
|
2846
|
+
const anonymizeAssistantId = memoizedAnonymizeId('a');
|
|
2847
|
+
const anonymizeMessageId = (id) => id === Constants.NO_PARENT ? id : memoizedAnonymizeId('msg')(id);
|
|
2848
|
+
function anonymizeConvo(conversation) {
|
|
2849
|
+
if (!conversation) {
|
|
2850
|
+
return null;
|
|
2851
|
+
}
|
|
2852
|
+
const newConvo = { ...conversation };
|
|
2853
|
+
if (newConvo.assistant_id) {
|
|
2854
|
+
newConvo.assistant_id = anonymizeAssistantId(newConvo.assistant_id);
|
|
2855
|
+
}
|
|
2856
|
+
return newConvo;
|
|
2857
|
+
}
|
|
2858
|
+
function anonymizeMessages(messages, newConvoId) {
|
|
2859
|
+
if (!Array.isArray(messages)) {
|
|
2860
|
+
return [];
|
|
2861
|
+
}
|
|
2862
|
+
const idMap = new Map();
|
|
2863
|
+
return messages.map((message) => {
|
|
2864
|
+
var _a, _b;
|
|
2865
|
+
const newMessageId = anonymizeMessageId(message.messageId);
|
|
2866
|
+
idMap.set(message.messageId, newMessageId);
|
|
2867
|
+
const anonymizedAttachments = (_a = message.attachments) === null || _a === void 0 ? void 0 : _a.map((attachment) => {
|
|
2868
|
+
return {
|
|
2869
|
+
...attachment,
|
|
2870
|
+
messageId: newMessageId,
|
|
2871
|
+
conversationId: newConvoId,
|
|
2872
|
+
};
|
|
2873
|
+
});
|
|
2874
|
+
return {
|
|
2875
|
+
...message,
|
|
2876
|
+
messageId: newMessageId,
|
|
2877
|
+
parentMessageId: idMap.get(message.parentMessageId || '') ||
|
|
2878
|
+
anonymizeMessageId(message.parentMessageId || ''),
|
|
2879
|
+
conversationId: newConvoId,
|
|
2880
|
+
model: ((_b = message.model) === null || _b === void 0 ? void 0 : _b.startsWith('asst_'))
|
|
2881
|
+
? anonymizeAssistantId(message.model)
|
|
2882
|
+
: message.model,
|
|
2883
|
+
attachments: anonymizedAttachments,
|
|
2884
|
+
};
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
/** Factory function that takes mongoose instance and returns the methods */
|
|
2888
|
+
function createShareMethods(mongoose) {
|
|
2889
|
+
/**
|
|
2890
|
+
* Get shared messages for a public share link
|
|
2891
|
+
*/
|
|
2892
|
+
async function getSharedMessages(shareId) {
|
|
2893
|
+
try {
|
|
2894
|
+
const SharedLink = mongoose.models.SharedLink;
|
|
2895
|
+
const share = (await SharedLink.findOne({ shareId, isPublic: true })
|
|
2896
|
+
.populate({
|
|
2897
|
+
path: 'messages',
|
|
2898
|
+
select: '-_id -__v -user',
|
|
2899
|
+
})
|
|
2900
|
+
.select('-_id -__v -user')
|
|
2901
|
+
.lean());
|
|
2902
|
+
if (!(share === null || share === void 0 ? void 0 : share.conversationId) || !share.isPublic) {
|
|
2903
|
+
return null;
|
|
2904
|
+
}
|
|
2905
|
+
const newConvoId = anonymizeConvoId(share.conversationId);
|
|
2906
|
+
const result = {
|
|
2907
|
+
shareId: share.shareId || shareId,
|
|
2908
|
+
title: share.title,
|
|
2909
|
+
isPublic: share.isPublic,
|
|
2910
|
+
createdAt: share.createdAt,
|
|
2911
|
+
updatedAt: share.updatedAt,
|
|
2912
|
+
conversationId: newConvoId,
|
|
2913
|
+
messages: anonymizeMessages(share.messages, newConvoId),
|
|
2914
|
+
};
|
|
2915
|
+
return result;
|
|
2916
|
+
}
|
|
2917
|
+
catch (error) {
|
|
2918
|
+
logger.error('[getSharedMessages] Error getting share link', {
|
|
2919
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
2920
|
+
shareId,
|
|
2921
|
+
});
|
|
2922
|
+
throw new ShareServiceError('Error getting share link', 'SHARE_FETCH_ERROR');
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Get shared links for a specific user with pagination and search
|
|
2927
|
+
*/
|
|
2928
|
+
async function getSharedLinks(user, pageParam, pageSize = 10, isPublic = true, sortBy = 'createdAt', sortDirection = 'desc', search) {
|
|
2929
|
+
var _a;
|
|
2930
|
+
try {
|
|
2931
|
+
const SharedLink = mongoose.models.SharedLink;
|
|
2932
|
+
const Conversation = mongoose.models.Conversation;
|
|
2933
|
+
const query = { user, isPublic };
|
|
2934
|
+
if (pageParam) {
|
|
2935
|
+
if (sortDirection === 'desc') {
|
|
2936
|
+
query[sortBy] = { $lt: pageParam };
|
|
2937
|
+
}
|
|
2938
|
+
else {
|
|
2939
|
+
query[sortBy] = { $gt: pageParam };
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (search && search.trim()) {
|
|
2943
|
+
try {
|
|
2944
|
+
const searchResults = await Conversation.meiliSearch(search);
|
|
2945
|
+
if (!((_a = searchResults === null || searchResults === void 0 ? void 0 : searchResults.hits) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
2946
|
+
return {
|
|
2947
|
+
links: [],
|
|
2948
|
+
nextCursor: undefined,
|
|
2949
|
+
hasNextPage: false,
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
const conversationIds = searchResults.hits.map((hit) => hit.conversationId);
|
|
2953
|
+
query['conversationId'] = { $in: conversationIds };
|
|
2954
|
+
}
|
|
2955
|
+
catch (searchError) {
|
|
2956
|
+
logger.error('[getSharedLinks] Meilisearch error', {
|
|
2957
|
+
error: searchError instanceof Error ? searchError.message : 'Unknown error',
|
|
2958
|
+
user,
|
|
2959
|
+
});
|
|
2960
|
+
return {
|
|
2961
|
+
links: [],
|
|
2962
|
+
nextCursor: undefined,
|
|
2963
|
+
hasNextPage: false,
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
const sort = {};
|
|
2968
|
+
sort[sortBy] = sortDirection === 'desc' ? -1 : 1;
|
|
2969
|
+
const sharedLinks = await SharedLink.find(query)
|
|
2970
|
+
.sort(sort)
|
|
2971
|
+
.limit(pageSize + 1)
|
|
2972
|
+
.select('-__v -user')
|
|
2973
|
+
.lean();
|
|
2974
|
+
const hasNextPage = sharedLinks.length > pageSize;
|
|
2975
|
+
const links = sharedLinks.slice(0, pageSize);
|
|
2976
|
+
const nextCursor = hasNextPage
|
|
2977
|
+
? links[links.length - 1][sortBy]
|
|
2978
|
+
: undefined;
|
|
2979
|
+
return {
|
|
2980
|
+
links: links.map((link) => ({
|
|
2981
|
+
shareId: link.shareId || '',
|
|
2982
|
+
title: (link === null || link === void 0 ? void 0 : link.title) || 'Untitled',
|
|
2983
|
+
isPublic: link.isPublic,
|
|
2984
|
+
createdAt: link.createdAt || new Date(),
|
|
2985
|
+
conversationId: link.conversationId,
|
|
2986
|
+
})),
|
|
2987
|
+
nextCursor,
|
|
2988
|
+
hasNextPage,
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
catch (error) {
|
|
2992
|
+
logger.error('[getSharedLinks] Error getting shares', {
|
|
2993
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
2994
|
+
user,
|
|
2995
|
+
});
|
|
2996
|
+
throw new ShareServiceError('Error getting shares', 'SHARES_FETCH_ERROR');
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Delete all shared links for a user
|
|
3001
|
+
*/
|
|
3002
|
+
async function deleteAllSharedLinks(user) {
|
|
3003
|
+
try {
|
|
3004
|
+
const SharedLink = mongoose.models.SharedLink;
|
|
3005
|
+
const result = await SharedLink.deleteMany({ user });
|
|
3006
|
+
return {
|
|
3007
|
+
message: 'All shared links deleted successfully',
|
|
3008
|
+
deletedCount: result.deletedCount,
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
catch (error) {
|
|
3012
|
+
logger.error('[deleteAllSharedLinks] Error deleting shared links', {
|
|
3013
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
3014
|
+
user,
|
|
3015
|
+
});
|
|
3016
|
+
throw new ShareServiceError('Error deleting shared links', 'BULK_DELETE_ERROR');
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Create a new shared link for a conversation
|
|
3021
|
+
*/
|
|
3022
|
+
async function createSharedLink(user, conversationId) {
|
|
3023
|
+
if (!user || !conversationId) {
|
|
3024
|
+
throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS');
|
|
3025
|
+
}
|
|
3026
|
+
try {
|
|
3027
|
+
const Message = mongoose.models.Message;
|
|
3028
|
+
const SharedLink = mongoose.models.SharedLink;
|
|
3029
|
+
const Conversation = mongoose.models.Conversation;
|
|
3030
|
+
const [existingShare, conversationMessages] = await Promise.all([
|
|
3031
|
+
SharedLink.findOne({ conversationId, user, isPublic: true })
|
|
3032
|
+
.select('-_id -__v -user')
|
|
3033
|
+
.lean(),
|
|
3034
|
+
Message.find({ conversationId, user }).sort({ createdAt: 1 }).lean(),
|
|
3035
|
+
]);
|
|
3036
|
+
if (existingShare && existingShare.isPublic) {
|
|
3037
|
+
logger.error('[createSharedLink] Share already exists', {
|
|
3038
|
+
user,
|
|
3039
|
+
conversationId,
|
|
3040
|
+
});
|
|
3041
|
+
throw new ShareServiceError('Share already exists', 'SHARE_EXISTS');
|
|
3042
|
+
}
|
|
3043
|
+
else if (existingShare) {
|
|
3044
|
+
await SharedLink.deleteOne({ conversationId, user });
|
|
3045
|
+
}
|
|
3046
|
+
const conversation = (await Conversation.findOne({ conversationId, user }).lean());
|
|
3047
|
+
// Check if user owns the conversation
|
|
3048
|
+
if (!conversation) {
|
|
3049
|
+
throw new ShareServiceError('Conversation not found or access denied', 'CONVERSATION_NOT_FOUND');
|
|
3050
|
+
}
|
|
3051
|
+
// Check if there are any messages to share
|
|
3052
|
+
if (!conversationMessages || conversationMessages.length === 0) {
|
|
3053
|
+
throw new ShareServiceError('No messages to share', 'NO_MESSAGES');
|
|
3054
|
+
}
|
|
3055
|
+
const title = conversation.title || 'Untitled';
|
|
3056
|
+
const shareId = nanoid();
|
|
3057
|
+
await SharedLink.create({
|
|
3058
|
+
shareId,
|
|
3059
|
+
conversationId,
|
|
3060
|
+
messages: conversationMessages,
|
|
3061
|
+
title,
|
|
3062
|
+
user,
|
|
3063
|
+
});
|
|
3064
|
+
return { shareId, conversationId };
|
|
3065
|
+
}
|
|
3066
|
+
catch (error) {
|
|
3067
|
+
if (error instanceof ShareServiceError) {
|
|
3068
|
+
throw error;
|
|
3069
|
+
}
|
|
3070
|
+
logger.error('[createSharedLink] Error creating shared link', {
|
|
3071
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
3072
|
+
user,
|
|
3073
|
+
conversationId,
|
|
3074
|
+
});
|
|
3075
|
+
throw new ShareServiceError('Error creating shared link', 'SHARE_CREATE_ERROR');
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Get a shared link for a conversation
|
|
3080
|
+
*/
|
|
3081
|
+
async function getSharedLink(user, conversationId) {
|
|
3082
|
+
if (!user || !conversationId) {
|
|
3083
|
+
throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS');
|
|
3084
|
+
}
|
|
3085
|
+
try {
|
|
3086
|
+
const SharedLink = mongoose.models.SharedLink;
|
|
3087
|
+
const share = (await SharedLink.findOne({ conversationId, user, isPublic: true })
|
|
3088
|
+
.select('shareId -_id')
|
|
3089
|
+
.lean());
|
|
3090
|
+
if (!share) {
|
|
3091
|
+
return { shareId: null, success: false };
|
|
3092
|
+
}
|
|
3093
|
+
return { shareId: share.shareId || null, success: true };
|
|
3094
|
+
}
|
|
3095
|
+
catch (error) {
|
|
3096
|
+
logger.error('[getSharedLink] Error getting shared link', {
|
|
3097
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
3098
|
+
user,
|
|
3099
|
+
conversationId,
|
|
3100
|
+
});
|
|
3101
|
+
throw new ShareServiceError('Error getting shared link', 'SHARE_FETCH_ERROR');
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Update a shared link with new messages
|
|
3106
|
+
*/
|
|
3107
|
+
async function updateSharedLink(user, shareId) {
|
|
3108
|
+
if (!user || !shareId) {
|
|
3109
|
+
throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS');
|
|
3110
|
+
}
|
|
3111
|
+
try {
|
|
3112
|
+
const SharedLink = mongoose.models.SharedLink;
|
|
3113
|
+
const Message = mongoose.models.Message;
|
|
3114
|
+
const share = (await SharedLink.findOne({ shareId, user })
|
|
3115
|
+
.select('-_id -__v -user')
|
|
3116
|
+
.lean());
|
|
3117
|
+
if (!share) {
|
|
3118
|
+
throw new ShareServiceError('Share not found', 'SHARE_NOT_FOUND');
|
|
3119
|
+
}
|
|
3120
|
+
const updatedMessages = await Message.find({ conversationId: share.conversationId, user })
|
|
3121
|
+
.sort({ createdAt: 1 })
|
|
3122
|
+
.lean();
|
|
3123
|
+
const newShareId = nanoid();
|
|
3124
|
+
const update = {
|
|
3125
|
+
messages: updatedMessages,
|
|
3126
|
+
user,
|
|
3127
|
+
shareId: newShareId,
|
|
3128
|
+
};
|
|
3129
|
+
const updatedShare = (await SharedLink.findOneAndUpdate({ shareId, user }, update, {
|
|
3130
|
+
new: true,
|
|
3131
|
+
upsert: false,
|
|
3132
|
+
runValidators: true,
|
|
3133
|
+
}).lean());
|
|
3134
|
+
if (!updatedShare) {
|
|
3135
|
+
throw new ShareServiceError('Share update failed', 'SHARE_UPDATE_ERROR');
|
|
3136
|
+
}
|
|
3137
|
+
anonymizeConvo(updatedShare);
|
|
3138
|
+
return { shareId: newShareId, conversationId: updatedShare.conversationId };
|
|
3139
|
+
}
|
|
3140
|
+
catch (error) {
|
|
3141
|
+
logger.error('[updateSharedLink] Error updating shared link', {
|
|
3142
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
3143
|
+
user,
|
|
3144
|
+
shareId,
|
|
3145
|
+
});
|
|
3146
|
+
throw new ShareServiceError(error instanceof ShareServiceError ? error.message : 'Error updating shared link', error instanceof ShareServiceError ? error.code : 'SHARE_UPDATE_ERROR');
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Delete a shared link
|
|
3151
|
+
*/
|
|
3152
|
+
async function deleteSharedLink(user, shareId) {
|
|
3153
|
+
if (!user || !shareId) {
|
|
3154
|
+
throw new ShareServiceError('Missing required parameters', 'INVALID_PARAMS');
|
|
3155
|
+
}
|
|
3156
|
+
try {
|
|
3157
|
+
const SharedLink = mongoose.models.SharedLink;
|
|
3158
|
+
const result = await SharedLink.findOneAndDelete({ shareId, user }).lean();
|
|
3159
|
+
if (!result) {
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
return {
|
|
3163
|
+
success: true,
|
|
3164
|
+
shareId,
|
|
3165
|
+
message: 'Share deleted successfully',
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
catch (error) {
|
|
3169
|
+
logger.error('[deleteSharedLink] Error deleting shared link', {
|
|
3170
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
3171
|
+
user,
|
|
3172
|
+
shareId,
|
|
3173
|
+
});
|
|
3174
|
+
throw new ShareServiceError('Error deleting shared link', 'SHARE_DELETE_ERROR');
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
// Return all methods
|
|
3178
|
+
return {
|
|
3179
|
+
getSharedLink,
|
|
3180
|
+
getSharedLinks,
|
|
3181
|
+
createSharedLink,
|
|
3182
|
+
updateSharedLink,
|
|
3183
|
+
deleteSharedLink,
|
|
3184
|
+
getSharedMessages,
|
|
3185
|
+
deleteAllSharedLinks,
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
// Factory function that takes mongoose instance and returns the methods
|
|
3190
|
+
function createPluginAuthMethods(mongoose) {
|
|
3191
|
+
const PluginAuth = mongoose.models.PluginAuth;
|
|
3192
|
+
/**
|
|
3193
|
+
* Finds a single plugin auth entry by userId and authField
|
|
3194
|
+
*/
|
|
3195
|
+
async function findOnePluginAuth({ userId, authField, }) {
|
|
3196
|
+
try {
|
|
3197
|
+
return await PluginAuth.findOne({ userId, authField }).lean();
|
|
3198
|
+
}
|
|
3199
|
+
catch (error) {
|
|
3200
|
+
throw new Error(`Failed to find plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Finds multiple plugin auth entries by userId and pluginKeys
|
|
3205
|
+
*/
|
|
3206
|
+
async function findPluginAuthsByKeys({ userId, pluginKeys, }) {
|
|
3207
|
+
try {
|
|
3208
|
+
if (!pluginKeys || pluginKeys.length === 0) {
|
|
3209
|
+
return [];
|
|
3210
|
+
}
|
|
3211
|
+
return await PluginAuth.find({
|
|
3212
|
+
userId,
|
|
3213
|
+
pluginKey: { $in: pluginKeys },
|
|
3214
|
+
}).lean();
|
|
3215
|
+
}
|
|
3216
|
+
catch (error) {
|
|
3217
|
+
throw new Error(`Failed to find plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* Updates or creates a plugin auth entry
|
|
3222
|
+
*/
|
|
3223
|
+
async function updatePluginAuth({ userId, authField, pluginKey, value, }) {
|
|
3224
|
+
try {
|
|
3225
|
+
const existingAuth = await PluginAuth.findOne({ userId, pluginKey, authField }).lean();
|
|
3226
|
+
if (existingAuth) {
|
|
3227
|
+
return await PluginAuth.findOneAndUpdate({ userId, pluginKey, authField }, { $set: { value } }, { new: true, upsert: true }).lean();
|
|
3228
|
+
}
|
|
3229
|
+
else {
|
|
3230
|
+
const newPluginAuth = await new PluginAuth({
|
|
3231
|
+
userId,
|
|
3232
|
+
authField,
|
|
3233
|
+
value,
|
|
3234
|
+
pluginKey,
|
|
3235
|
+
});
|
|
3236
|
+
await newPluginAuth.save();
|
|
3237
|
+
return newPluginAuth.toObject();
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
catch (error) {
|
|
3241
|
+
throw new Error(`Failed to update plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Deletes plugin auth entries based on provided parameters
|
|
3246
|
+
*/
|
|
3247
|
+
async function deletePluginAuth({ userId, authField, pluginKey, all = false, }) {
|
|
3248
|
+
try {
|
|
3249
|
+
if (all) {
|
|
3250
|
+
const filter = { userId };
|
|
3251
|
+
if (pluginKey) {
|
|
3252
|
+
filter.pluginKey = pluginKey;
|
|
3253
|
+
}
|
|
3254
|
+
return await PluginAuth.deleteMany(filter);
|
|
3255
|
+
}
|
|
3256
|
+
if (!authField) {
|
|
3257
|
+
throw new Error('authField is required when all is false');
|
|
3258
|
+
}
|
|
3259
|
+
return await PluginAuth.deleteOne({ userId, authField });
|
|
3260
|
+
}
|
|
3261
|
+
catch (error) {
|
|
3262
|
+
throw new Error(`Failed to delete plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Deletes all plugin auth entries for a user
|
|
3267
|
+
*/
|
|
3268
|
+
async function deleteAllUserPluginAuths(userId) {
|
|
3269
|
+
try {
|
|
3270
|
+
return await PluginAuth.deleteMany({ userId });
|
|
3271
|
+
}
|
|
3272
|
+
catch (error) {
|
|
3273
|
+
throw new Error(`Failed to delete all user plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return {
|
|
3277
|
+
findOnePluginAuth,
|
|
3278
|
+
findPluginAuthsByKeys,
|
|
3279
|
+
updatePluginAuth,
|
|
3280
|
+
deletePluginAuth,
|
|
3281
|
+
deleteAllUserPluginAuths,
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
/**
|
|
3286
|
+
* Creates all database methods for all collections
|
|
3287
|
+
*/
|
|
3288
|
+
function createMethods(mongoose) {
|
|
3289
|
+
return {
|
|
3290
|
+
...createUserMethods(mongoose),
|
|
3291
|
+
...createSessionMethods(mongoose),
|
|
3292
|
+
...createTokenMethods(mongoose),
|
|
3293
|
+
...createRoleMethods(mongoose),
|
|
3294
|
+
...createMemoryMethods(mongoose),
|
|
3295
|
+
...createShareMethods(mongoose),
|
|
3296
|
+
...createPluginAuthMethods(mongoose),
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
export { Action as actionSchema, agentSchema, assistantSchema, balanceSchema, bannerSchema, categoriesSchema, conversationTag as conversationTagSchema, convoSchema, createMethods, createModels, file as fileSchema, hashToken, keySchema, logger, logger$1 as meiliLogger, MemoryEntrySchema as memorySchema, messageSchema, pluginAuthSchema, presetSchema, projectSchema, promptGroupSchema, promptSchema, roleSchema, sessionSchema, shareSchema, signPayload, tokenSchema, toolCallSchema, transactionSchema, userSchema };
|
|
1152
3301
|
//# sourceMappingURL=index.es.js.map
|