@mastra/elasticsearch 1.3.1 → 1.4.0

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.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { Client } from "@elastic/elasticsearch";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
- import { createVectorErrorId } from "@mastra/core/storage";
3
+ import { MastraStorage, MemoryStorage, ScoresStorage, TABLE_MESSAGES, TABLE_RESOURCES, TABLE_SCORERS, TABLE_THREADS, TABLE_WORKFLOW_SNAPSHOT, WorkflowsStorage, calculatePagination, createStorageErrorId, createVectorErrorId, ensureDate, filterByDateRange, jsonValueEquals, matchesExpectedWorkflowStatus, normalizePerPage, serializeDate, storageMessageMatchesMetadataFilter, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
4
  import { MastraVector, validateTopK, validateUpsert } from "@mastra/core/vector";
5
5
  import { BaseFilterTranslator } from "@mastra/core/vector/filter";
6
+ import { MessageList } from "@mastra/core/agent";
7
+ import crypto$1 from "crypto";
8
+ import { saveScorePayloadSchema } from "@mastra/core/evals";
6
9
  //#region package.json
7
- var version = "1.3.1";
10
+ var version = "1.4.0";
8
11
  //#endregion
9
12
  //#region src/vector/filter.ts
10
13
  /**
@@ -840,6 +843,1618 @@ var ElasticSearchVector = class extends MastraVector {
840
843
  }
841
844
  };
842
845
  //#endregion
843
- export { ElasticSearchVector };
846
+ //#region src/storage/domains/utils.ts
847
+ /**
848
+ * Generate a document key from table name and key parts.
849
+ *
850
+ * @example
851
+ * ```typescript
852
+ * getKey('mastra_threads', { id: 'thread-123' });
853
+ * // Returns: 'mastra_threads:id:thread-123'
854
+ * ```
855
+ */
856
+ function getKey(tableName, keys) {
857
+ return `${tableName}:${Object.entries(keys).filter(([_, value]) => value !== void 0).map(([key, value]) => {
858
+ if (value && typeof value === "object") return `${key}:${JSON.stringify(value)}`;
859
+ return `${key}:${value}`;
860
+ }).join(":")}`;
861
+ }
862
+ /**
863
+ * Process a record for storage, generating the appropriate document id and serializing dates.
864
+ */
865
+ function processRecord(tableName, record) {
866
+ let key;
867
+ if (tableName === TABLE_MESSAGES) key = getKey(tableName, {
868
+ threadId: record.threadId,
869
+ id: record.id
870
+ });
871
+ else if (tableName === TABLE_WORKFLOW_SNAPSHOT) key = getKey(tableName, {
872
+ namespace: record.namespace || "workflows",
873
+ workflow_name: record.workflow_name,
874
+ run_id: record.run_id,
875
+ ...record.resourceId ? { resourceId: record.resourceId } : {}
876
+ });
877
+ else key = getKey(tableName, { id: record.id });
878
+ const processedRecord = {
879
+ ...record,
880
+ createdAt: serializeDate(record.createdAt),
881
+ updatedAt: serializeDate(record.updatedAt)
882
+ };
883
+ return {
884
+ key,
885
+ processedRecord
886
+ };
887
+ }
888
+ //#endregion
889
+ //#region src/storage/db.ts
890
+ const SEARCH_PAGE_SIZE = 1e3;
891
+ /**
892
+ * Thin document-store layer over ElasticSearch.
893
+ *
894
+ * Each Mastra table maps to one ElasticSearch index (same name, already lowercase).
895
+ * Records are stored as opaque JSON strings in a non-indexed `doc` field, with a
896
+ * `key` keyword field (copy of `_id`) used for stable `search_after` pagination.
897
+ *
898
+ * ElasticSearch is near-real-time: all writes use `refresh: true` so
899
+ * subsequent searches observe them, and point reads use `_get` by id (which is
900
+ * real-time regardless of refresh).
901
+ */
902
+ var ElasticSearchDB = class {
903
+ client;
904
+ ensuredIndexes = /* @__PURE__ */ new Set();
905
+ constructor({ client }) {
906
+ this.client = client;
907
+ }
908
+ getClient() {
909
+ return this.client;
910
+ }
911
+ async ensureIndex(tableName) {
912
+ if (this.ensuredIndexes.has(tableName)) return;
913
+ try {
914
+ if (!await this.client.indices.exists({ index: tableName })) await this.client.indices.create({
915
+ index: tableName,
916
+ mappings: {
917
+ dynamic: false,
918
+ properties: {
919
+ key: { type: "keyword" },
920
+ doc: {
921
+ type: "text",
922
+ index: false
923
+ }
924
+ }
925
+ }
926
+ });
927
+ this.ensuredIndexes.add(tableName);
928
+ } catch (error) {
929
+ const message = error?.message || error?.toString();
930
+ if (message && message.toLowerCase().includes("already exists")) {
931
+ this.ensuredIndexes.add(tableName);
932
+ return;
933
+ }
934
+ throw new MastraError({
935
+ id: createStorageErrorId("ELASTICSEARCH", "ENSURE_INDEX", "FAILED"),
936
+ domain: ErrorDomain.STORAGE,
937
+ category: ErrorCategory.THIRD_PARTY,
938
+ details: { tableName }
939
+ }, error);
940
+ }
941
+ }
942
+ async insert({ tableName, record }) {
943
+ const { key, processedRecord } = processRecord(tableName, record);
944
+ await this.set({
945
+ tableName,
946
+ key,
947
+ value: processedRecord
948
+ });
949
+ }
950
+ async set({ tableName, key, value }) {
951
+ await this.ensureIndex(tableName);
952
+ try {
953
+ await this.client.index({
954
+ index: tableName,
955
+ id: key,
956
+ document: {
957
+ key,
958
+ doc: JSON.stringify(value)
959
+ },
960
+ refresh: true
961
+ });
962
+ } catch (error) {
963
+ throw new MastraError({
964
+ id: createStorageErrorId("ELASTICSEARCH", "INSERT", "FAILED"),
965
+ domain: ErrorDomain.STORAGE,
966
+ category: ErrorCategory.THIRD_PARTY,
967
+ details: { tableName }
968
+ }, error);
969
+ }
970
+ }
971
+ async bulkSet({ tableName, entries }) {
972
+ if (entries.length === 0) return;
973
+ await this.ensureIndex(tableName);
974
+ try {
975
+ const operations = entries.flatMap(({ key, value }) => [{ index: {
976
+ _index: tableName,
977
+ _id: key
978
+ } }, {
979
+ key,
980
+ doc: JSON.stringify(value)
981
+ }]);
982
+ const response = await this.client.bulk({
983
+ operations,
984
+ refresh: true
985
+ });
986
+ if (response.errors) {
987
+ const firstError = response.items.find((item) => item.index?.error)?.index?.error;
988
+ throw new Error(`Bulk write failed: ${firstError?.reason ?? "unknown error"}`);
989
+ }
990
+ } catch (error) {
991
+ throw new MastraError({
992
+ id: createStorageErrorId("ELASTICSEARCH", "BATCH_INSERT", "FAILED"),
993
+ domain: ErrorDomain.STORAGE,
994
+ category: ErrorCategory.THIRD_PARTY,
995
+ details: { tableName }
996
+ }, error);
997
+ }
998
+ }
999
+ async get({ tableName, keys }) {
1000
+ const key = getKey(tableName, keys);
1001
+ return this.getByKey({
1002
+ tableName,
1003
+ key
1004
+ });
1005
+ }
1006
+ async getByKey({ tableName, key }) {
1007
+ await this.ensureIndex(tableName);
1008
+ try {
1009
+ const response = await this.client.get({
1010
+ index: tableName,
1011
+ id: key
1012
+ }, { ignore: [404] });
1013
+ if (!response.found || !response._source?.doc) return null;
1014
+ return JSON.parse(response._source.doc);
1015
+ } catch (error) {
1016
+ throw new MastraError({
1017
+ id: createStorageErrorId("ELASTICSEARCH", "LOAD", "FAILED"),
1018
+ domain: ErrorDomain.STORAGE,
1019
+ category: ErrorCategory.THIRD_PARTY,
1020
+ details: { tableName }
1021
+ }, error);
1022
+ }
1023
+ }
1024
+ /**
1025
+ * Returns all documents in a table, parsed. Uses `search_after` on the `key`
1026
+ * field for stable deep pagination.
1027
+ */
1028
+ async listAll({ tableName, keyPrefix }) {
1029
+ return (await this.listAllEntries({
1030
+ tableName,
1031
+ keyPrefix
1032
+ })).map((entry) => entry.value);
1033
+ }
1034
+ /**
1035
+ * Returns all `{ key, value }` entries in a table (optionally filtered by key
1036
+ * prefix), using `search_after` pagination.
1037
+ */
1038
+ async listAllEntries({ tableName, keyPrefix }) {
1039
+ await this.ensureIndex(tableName);
1040
+ try {
1041
+ const results = [];
1042
+ let searchAfter;
1043
+ while (true) {
1044
+ const hits = (await this.client.search({
1045
+ index: tableName,
1046
+ size: SEARCH_PAGE_SIZE,
1047
+ query: keyPrefix ? { prefix: { key: keyPrefix } } : { match_all: {} },
1048
+ sort: [{ key: "asc" }],
1049
+ ...searchAfter ? { search_after: searchAfter } : {}
1050
+ })).hits.hits;
1051
+ for (const hit of hits) if (hit._source?.doc) results.push({
1052
+ key: hit._source.key,
1053
+ value: JSON.parse(hit._source.doc)
1054
+ });
1055
+ if (hits.length < SEARCH_PAGE_SIZE) break;
1056
+ searchAfter = hits[hits.length - 1].sort;
1057
+ }
1058
+ return results;
1059
+ } catch (error) {
1060
+ throw new MastraError({
1061
+ id: createStorageErrorId("ELASTICSEARCH", "SCAN", "FAILED"),
1062
+ domain: ErrorDomain.STORAGE,
1063
+ category: ErrorCategory.THIRD_PARTY,
1064
+ details: { tableName }
1065
+ }, error);
1066
+ }
1067
+ }
1068
+ async delete({ tableName, key }) {
1069
+ await this.ensureIndex(tableName);
1070
+ try {
1071
+ await this.client.delete({
1072
+ index: tableName,
1073
+ id: key,
1074
+ refresh: true
1075
+ }, { ignore: [404] });
1076
+ } catch (error) {
1077
+ throw new MastraError({
1078
+ id: createStorageErrorId("ELASTICSEARCH", "DELETE", "FAILED"),
1079
+ domain: ErrorDomain.STORAGE,
1080
+ category: ErrorCategory.THIRD_PARTY,
1081
+ details: { tableName }
1082
+ }, error);
1083
+ }
1084
+ }
1085
+ async deleteMany({ tableName, keys }) {
1086
+ if (keys.length === 0) return;
1087
+ await this.ensureIndex(tableName);
1088
+ try {
1089
+ await this.client.deleteByQuery({
1090
+ index: tableName,
1091
+ query: { terms: { key: keys } },
1092
+ refresh: true,
1093
+ conflicts: "proceed"
1094
+ });
1095
+ } catch (error) {
1096
+ throw new MastraError({
1097
+ id: createStorageErrorId("ELASTICSEARCH", "DELETE_MANY", "FAILED"),
1098
+ domain: ErrorDomain.STORAGE,
1099
+ category: ErrorCategory.THIRD_PARTY,
1100
+ details: { tableName }
1101
+ }, error);
1102
+ }
1103
+ }
1104
+ async deleteData({ tableName, keyPrefix }) {
1105
+ await this.ensureIndex(tableName);
1106
+ try {
1107
+ await this.client.deleteByQuery({
1108
+ index: tableName,
1109
+ query: keyPrefix ? { prefix: { key: keyPrefix } } : { match_all: {} },
1110
+ refresh: true,
1111
+ conflicts: "proceed"
1112
+ });
1113
+ } catch (error) {
1114
+ throw new MastraError({
1115
+ id: createStorageErrorId("ELASTICSEARCH", "CLEAR_TABLE", "FAILED"),
1116
+ domain: ErrorDomain.STORAGE,
1117
+ category: ErrorCategory.THIRD_PARTY,
1118
+ details: { tableName }
1119
+ }, error);
1120
+ }
1121
+ }
1122
+ };
1123
+ //#endregion
1124
+ //#region src/storage/domains/memory/index.ts
1125
+ var MemoryElasticSearch = class extends MemoryStorage {
1126
+ supportsPartialThreadUpdate = true;
1127
+ db;
1128
+ constructor(config) {
1129
+ super();
1130
+ this.db = new ElasticSearchDB({ client: config.client });
1131
+ }
1132
+ async dangerouslyClearAll() {
1133
+ await this.db.deleteData({ tableName: TABLE_THREADS });
1134
+ await this.db.deleteData({ tableName: TABLE_MESSAGES });
1135
+ await this.db.deleteData({ tableName: TABLE_RESOURCES });
1136
+ }
1137
+ async getThreadById({ threadId, resourceId }) {
1138
+ try {
1139
+ const thread = await this.db.get({
1140
+ tableName: TABLE_THREADS,
1141
+ keys: { id: threadId }
1142
+ });
1143
+ if (!thread || resourceId !== void 0 && thread.resourceId !== resourceId) return null;
1144
+ return {
1145
+ ...thread,
1146
+ createdAt: ensureDate(thread.createdAt),
1147
+ updatedAt: ensureDate(thread.updatedAt),
1148
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata
1149
+ };
1150
+ } catch (error) {
1151
+ throw new MastraError({
1152
+ id: createStorageErrorId("ELASTICSEARCH", "GET_THREAD_BY_ID", "FAILED"),
1153
+ domain: ErrorDomain.STORAGE,
1154
+ category: ErrorCategory.THIRD_PARTY,
1155
+ details: { threadId }
1156
+ }, error);
1157
+ }
1158
+ }
1159
+ async listThreadsByResourceId(args) {
1160
+ return this.listThreads(args);
1161
+ }
1162
+ async listThreads(args) {
1163
+ const { page = 0, perPage: perPageInput, orderBy, filter } = args;
1164
+ const { field, direction } = this.parseOrderBy(orderBy);
1165
+ try {
1166
+ this.validatePaginationInput(page, perPageInput ?? 100);
1167
+ } catch (error) {
1168
+ throw new MastraError({
1169
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_THREADS", "INVALID_PAGE"),
1170
+ domain: ErrorDomain.STORAGE,
1171
+ category: ErrorCategory.USER,
1172
+ details: {
1173
+ page,
1174
+ ...perPageInput !== void 0 && { perPage: perPageInput }
1175
+ }
1176
+ }, error instanceof Error ? error : /* @__PURE__ */ new Error("Invalid pagination parameters"));
1177
+ }
1178
+ const perPage = normalizePerPage(perPageInput, 100);
1179
+ try {
1180
+ this.validateMetadataKeys(filter?.metadata);
1181
+ } catch (error) {
1182
+ throw new MastraError({
1183
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_THREADS", "INVALID_METADATA_KEY"),
1184
+ domain: ErrorDomain.STORAGE,
1185
+ category: ErrorCategory.USER,
1186
+ details: { metadataKeys: filter?.metadata ? Object.keys(filter.metadata).join(", ") : "" }
1187
+ }, error instanceof Error ? error : /* @__PURE__ */ new Error("Invalid metadata key"));
1188
+ }
1189
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1190
+ try {
1191
+ const allThreads = [];
1192
+ const results = await this.db.listAll({ tableName: TABLE_THREADS });
1193
+ for (const thread of results) {
1194
+ if (filter?.resourceId && thread.resourceId !== filter.resourceId) continue;
1195
+ if (filter?.metadata && Object.keys(filter.metadata).length > 0) {
1196
+ const threadMetadata = typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata;
1197
+ if (!Object.entries(filter.metadata).every(([key, value]) => jsonValueEquals(threadMetadata?.[key], value))) continue;
1198
+ }
1199
+ allThreads.push({
1200
+ ...thread,
1201
+ createdAt: ensureDate(thread.createdAt),
1202
+ updatedAt: ensureDate(thread.updatedAt),
1203
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata
1204
+ });
1205
+ }
1206
+ const sortedThreads = this.sortThreads(allThreads, field, direction);
1207
+ const total = sortedThreads.length;
1208
+ const end = perPageInput === false ? total : offset + perPage;
1209
+ return {
1210
+ threads: sortedThreads.slice(offset, end),
1211
+ total,
1212
+ page,
1213
+ perPage: perPageForResponse,
1214
+ hasMore: perPageInput === false ? false : end < total
1215
+ };
1216
+ } catch (error) {
1217
+ if (error instanceof MastraError && error.category === ErrorCategory.USER) throw error;
1218
+ const mastraError = new MastraError({
1219
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_THREADS", "FAILED"),
1220
+ domain: ErrorDomain.STORAGE,
1221
+ category: ErrorCategory.THIRD_PARTY,
1222
+ details: {
1223
+ ...filter?.resourceId && { resourceId: filter.resourceId },
1224
+ hasMetadataFilter: !!filter?.metadata,
1225
+ page,
1226
+ perPage
1227
+ }
1228
+ }, error);
1229
+ this.logger.trackException(mastraError);
1230
+ this.logger.error(mastraError.toString());
1231
+ throw mastraError;
1232
+ }
1233
+ }
1234
+ async saveThread({ thread }) {
1235
+ try {
1236
+ await this.db.insert({
1237
+ tableName: TABLE_THREADS,
1238
+ record: thread
1239
+ });
1240
+ return thread;
1241
+ } catch (error) {
1242
+ const mastraError = new MastraError({
1243
+ id: createStorageErrorId("ELASTICSEARCH", "SAVE_THREAD", "FAILED"),
1244
+ domain: ErrorDomain.STORAGE,
1245
+ category: ErrorCategory.THIRD_PARTY,
1246
+ details: { threadId: thread.id }
1247
+ }, error);
1248
+ this.logger.trackException(mastraError);
1249
+ this.logger.error(mastraError.toString());
1250
+ throw mastraError;
1251
+ }
1252
+ }
1253
+ async updateThread({ id, title, metadata }) {
1254
+ const thread = await this.getThreadById({ threadId: id });
1255
+ if (!thread) throw new MastraError({
1256
+ id: createStorageErrorId("ELASTICSEARCH", "UPDATE_THREAD", "FAILED"),
1257
+ domain: ErrorDomain.STORAGE,
1258
+ category: ErrorCategory.USER,
1259
+ text: `Thread ${id} not found`,
1260
+ details: { threadId: id }
1261
+ });
1262
+ const updatedThread = {
1263
+ ...thread,
1264
+ title: title ?? thread.title,
1265
+ metadata: {
1266
+ ...thread.metadata,
1267
+ ...metadata
1268
+ },
1269
+ updatedAt: /* @__PURE__ */ new Date()
1270
+ };
1271
+ try {
1272
+ await this.saveThread({ thread: updatedThread });
1273
+ return updatedThread;
1274
+ } catch (error) {
1275
+ throw new MastraError({
1276
+ id: createStorageErrorId("ELASTICSEARCH", "UPDATE_THREAD", "FAILED"),
1277
+ domain: ErrorDomain.STORAGE,
1278
+ category: ErrorCategory.THIRD_PARTY,
1279
+ details: { threadId: id }
1280
+ }, error);
1281
+ }
1282
+ }
1283
+ async deleteThread({ threadId }) {
1284
+ try {
1285
+ const entries = await this.db.listAllEntries({
1286
+ tableName: TABLE_MESSAGES,
1287
+ keyPrefix: threadMessagesPrefix(threadId)
1288
+ });
1289
+ const keysToDelete = [...entries.map((entry) => entry.key), ...entries.map((entry) => getMessageIndexKey(entry.value.id))];
1290
+ await this.db.deleteMany({
1291
+ tableName: TABLE_MESSAGES,
1292
+ keys: keysToDelete
1293
+ });
1294
+ await this.db.delete({
1295
+ tableName: TABLE_THREADS,
1296
+ key: getKey(TABLE_THREADS, { id: threadId })
1297
+ });
1298
+ } catch (error) {
1299
+ throw new MastraError({
1300
+ id: createStorageErrorId("ELASTICSEARCH", "DELETE_THREAD", "FAILED"),
1301
+ domain: ErrorDomain.STORAGE,
1302
+ category: ErrorCategory.THIRD_PARTY,
1303
+ details: { threadId }
1304
+ }, error);
1305
+ }
1306
+ }
1307
+ async saveMessages(args) {
1308
+ const { messages } = args;
1309
+ if (messages.length === 0) return { messages: [] };
1310
+ const threadId = messages[0]?.threadId;
1311
+ let existingThread = null;
1312
+ try {
1313
+ if (!threadId) throw new Error("Thread ID is required");
1314
+ existingThread = await this.getThreadById({ threadId });
1315
+ if (!existingThread) throw new Error(`Thread ${threadId} not found`);
1316
+ } catch (error) {
1317
+ throw new MastraError({
1318
+ id: createStorageErrorId("ELASTICSEARCH", "SAVE_MESSAGES", "INVALID_ARGS"),
1319
+ domain: ErrorDomain.STORAGE,
1320
+ category: ErrorCategory.USER
1321
+ }, error);
1322
+ }
1323
+ const messagesWithIndex = messages.map((message, index) => {
1324
+ if (!message.threadId) throw new Error(`Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`);
1325
+ if (!message.resourceId) throw new Error(`Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`);
1326
+ return {
1327
+ ...message,
1328
+ _index: index
1329
+ };
1330
+ });
1331
+ try {
1332
+ const keysToDelete = [];
1333
+ const entries = [];
1334
+ for (const message of messagesWithIndex) {
1335
+ const existingIndex = await this.db.getByKey({
1336
+ tableName: TABLE_MESSAGES,
1337
+ key: getMessageIndexKey(message.id)
1338
+ });
1339
+ if (existingIndex?.threadId && existingIndex.threadId !== message.threadId) keysToDelete.push(getMessageKey(existingIndex.threadId, message.id));
1340
+ entries.push({
1341
+ key: getMessageKey(message.threadId, message.id),
1342
+ value: message
1343
+ });
1344
+ entries.push({
1345
+ key: getMessageIndexKey(message.id),
1346
+ value: { threadId: message.threadId }
1347
+ });
1348
+ }
1349
+ await this.db.deleteMany({
1350
+ tableName: TABLE_MESSAGES,
1351
+ keys: keysToDelete
1352
+ });
1353
+ await this.db.bulkSet({
1354
+ tableName: TABLE_MESSAGES,
1355
+ entries
1356
+ });
1357
+ const updatedThread = {
1358
+ ...existingThread,
1359
+ updatedAt: /* @__PURE__ */ new Date()
1360
+ };
1361
+ await this.db.insert({
1362
+ tableName: TABLE_THREADS,
1363
+ record: updatedThread
1364
+ });
1365
+ return { messages: new MessageList().add(messages, "memory").get.all.db() };
1366
+ } catch (error) {
1367
+ throw new MastraError({
1368
+ id: createStorageErrorId("ELASTICSEARCH", "SAVE_MESSAGES", "FAILED"),
1369
+ domain: ErrorDomain.STORAGE,
1370
+ category: ErrorCategory.THIRD_PARTY,
1371
+ details: { threadId }
1372
+ }, error);
1373
+ }
1374
+ }
1375
+ /**
1376
+ * Returns all messages that belong to a thread, sorted in insertion order
1377
+ * (createdAt with `_index` tiebreaker).
1378
+ */
1379
+ async listThreadMessages(threadId) {
1380
+ return (await this.db.listAll({
1381
+ tableName: TABLE_MESSAGES,
1382
+ keyPrefix: threadMessagesPrefix(threadId)
1383
+ })).sort((a, b) => getMessageScore(a) - getMessageScore(b));
1384
+ }
1385
+ /** Returns all message documents across all threads (excludes index docs). */
1386
+ async listAllMessages() {
1387
+ return this.db.listAll({
1388
+ tableName: TABLE_MESSAGES,
1389
+ keyPrefix: `${TABLE_MESSAGES}:threadId:`
1390
+ });
1391
+ }
1392
+ async getThreadIdForMessage(messageId) {
1393
+ const indexed = await this.db.getByKey({
1394
+ tableName: TABLE_MESSAGES,
1395
+ key: getMessageIndexKey(messageId)
1396
+ });
1397
+ if (indexed?.threadId) return indexed.threadId;
1398
+ const message = (await this.listAllMessages()).find((msg) => msg.id === messageId);
1399
+ if (!message) return null;
1400
+ if (message.threadId) await this.db.set({
1401
+ tableName: TABLE_MESSAGES,
1402
+ key: getMessageIndexKey(messageId),
1403
+ value: { threadId: message.threadId }
1404
+ });
1405
+ return message.threadId || null;
1406
+ }
1407
+ /**
1408
+ * Fetches the messages named by `include` together with their surrounding context.
1409
+ *
1410
+ * @param include - Message ids to pin, each with an optional before/after window.
1411
+ * @param resourceId - When set, drops any pinned or context message owned by another
1412
+ * resource so an id from another resource returns nothing.
1413
+ */
1414
+ async getIncludedMessages(include, resourceId) {
1415
+ if (!include?.length) return [];
1416
+ const messagesById = /* @__PURE__ */ new Map();
1417
+ for (const item of include) {
1418
+ const itemThreadId = await this.getThreadIdForMessage(item.id);
1419
+ if (!itemThreadId) continue;
1420
+ let threadMessages = await this.listThreadMessages(itemThreadId);
1421
+ if (resourceId !== void 0) threadMessages = threadMessages.filter((message) => message.resourceId === resourceId);
1422
+ const targetIndex = threadMessages.findIndex((message) => message.id === item.id);
1423
+ if (targetIndex === -1) continue;
1424
+ const start = Math.max(0, targetIndex - (item.withPreviousMessages ?? 0));
1425
+ const end = Math.min(threadMessages.length, targetIndex + (item.withNextMessages ?? 0) + 1);
1426
+ for (const message of threadMessages.slice(start, end)) messagesById.set(message.id, message);
1427
+ }
1428
+ return Array.from(messagesById.values());
1429
+ }
1430
+ parseStoredMessage(storedMessage) {
1431
+ const defaultMessageContent = {
1432
+ format: 2,
1433
+ parts: [{
1434
+ type: "text",
1435
+ text: ""
1436
+ }]
1437
+ };
1438
+ const { _index, ...rest } = storedMessage;
1439
+ return {
1440
+ ...rest,
1441
+ createdAt: new Date(rest.createdAt),
1442
+ content: rest.content || defaultMessageContent
1443
+ };
1444
+ }
1445
+ async listMessagesById({ messageIds }) {
1446
+ if (messageIds.length === 0) return { messages: [] };
1447
+ try {
1448
+ const rawMessages = [];
1449
+ const unindexedIds = [];
1450
+ for (const id of messageIds) {
1451
+ const indexed = await this.db.getByKey({
1452
+ tableName: TABLE_MESSAGES,
1453
+ key: getMessageIndexKey(id)
1454
+ });
1455
+ if (!indexed?.threadId) {
1456
+ unindexedIds.push(id);
1457
+ continue;
1458
+ }
1459
+ const message = await this.db.getByKey({
1460
+ tableName: TABLE_MESSAGES,
1461
+ key: getMessageKey(indexed.threadId, id)
1462
+ });
1463
+ if (message) rawMessages.push(message);
1464
+ else unindexedIds.push(id);
1465
+ }
1466
+ if (unindexedIds.length > 0) {
1467
+ const allMessages = await this.listAllMessages();
1468
+ const unindexedSet = new Set(unindexedIds);
1469
+ const foundMessages = allMessages.filter((msg) => unindexedSet.has(msg.id));
1470
+ rawMessages.push(...foundMessages);
1471
+ if (foundMessages.length > 0) await this.db.bulkSet({
1472
+ tableName: TABLE_MESSAGES,
1473
+ entries: foundMessages.filter((msg) => msg.threadId).map((msg) => ({
1474
+ key: getMessageIndexKey(msg.id),
1475
+ value: { threadId: msg.threadId }
1476
+ }))
1477
+ });
1478
+ }
1479
+ return { messages: new MessageList().add(rawMessages.map(this.parseStoredMessage), "memory").get.all.db() };
1480
+ } catch (error) {
1481
+ throw new MastraError({
1482
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_MESSAGES_BY_ID", "FAILED"),
1483
+ domain: ErrorDomain.STORAGE,
1484
+ category: ErrorCategory.THIRD_PARTY,
1485
+ details: { messageIds: JSON.stringify(messageIds) }
1486
+ }, error);
1487
+ }
1488
+ }
1489
+ async listMessages(args) {
1490
+ const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
1491
+ const threadIds = Array.isArray(threadId) ? threadId : [threadId];
1492
+ const threadIdsSet = new Set(threadIds);
1493
+ if (threadIds.length === 0 || threadIds.some((id) => !id.trim())) throw new MastraError({
1494
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_MESSAGES", "INVALID_THREAD_ID"),
1495
+ domain: ErrorDomain.STORAGE,
1496
+ category: ErrorCategory.USER,
1497
+ details: { threadId: Array.isArray(threadId) ? threadId.join(",") : threadId }
1498
+ }, /* @__PURE__ */ new Error("threadId must be a non-empty string or array of non-empty strings"));
1499
+ const perPage = normalizePerPage(perPageInput, 40);
1500
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1501
+ const metadataFilter = validateStorageMetadataFilter(filter?.metadata);
1502
+ try {
1503
+ if (page < 0) throw new MastraError({
1504
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_MESSAGES", "INVALID_PAGE"),
1505
+ domain: ErrorDomain.STORAGE,
1506
+ category: ErrorCategory.USER,
1507
+ details: { page }
1508
+ }, /* @__PURE__ */ new Error("page must be >= 0"));
1509
+ const { field, direction } = this.parseOrderBy(orderBy, "ASC");
1510
+ const getFieldValue = (msg) => {
1511
+ if (field === "createdAt") return new Date(msg.createdAt).getTime();
1512
+ const value = msg[field];
1513
+ if (typeof value === "number") return value;
1514
+ if (value instanceof Date) return value.getTime();
1515
+ return 0;
1516
+ };
1517
+ if (perPage === 0 && (!include || include.length === 0)) return {
1518
+ messages: [],
1519
+ total: 0,
1520
+ page,
1521
+ perPage: perPageForResponse,
1522
+ hasMore: false
1523
+ };
1524
+ let includedMessages = [];
1525
+ if (include && include.length > 0) includedMessages = (await this.getIncludedMessages(include, resourceId)).map(this.parseStoredMessage);
1526
+ if (perPage === 0 && include && include.length > 0) return {
1527
+ messages: new MessageList().add(includedMessages, "memory").get.all.db().sort((a, b) => {
1528
+ const aValue = getFieldValue(a);
1529
+ const bValue = getFieldValue(b);
1530
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
1531
+ }),
1532
+ total: 0,
1533
+ page,
1534
+ perPage: perPageForResponse,
1535
+ hasMore: false
1536
+ };
1537
+ let messagesData = [];
1538
+ for (const tid of threadIds) {
1539
+ const threadMessages = await this.listThreadMessages(tid);
1540
+ messagesData.push(...threadMessages.map(this.parseStoredMessage));
1541
+ }
1542
+ if (messagesData.length === 0) return {
1543
+ messages: [],
1544
+ total: 0,
1545
+ page,
1546
+ perPage: perPageForResponse,
1547
+ hasMore: false
1548
+ };
1549
+ if (resourceId) messagesData = messagesData.filter((msg) => msg.resourceId === resourceId);
1550
+ messagesData = filterByDateRange(messagesData, (msg) => new Date(msg.createdAt), filter?.dateRange);
1551
+ messagesData = messagesData.filter((message) => storageMessageMatchesMetadataFilter(message.content, metadataFilter));
1552
+ messagesData.sort((a, b) => {
1553
+ const aValue = getFieldValue(a);
1554
+ const bValue = getFieldValue(b);
1555
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
1556
+ });
1557
+ const total = messagesData.length;
1558
+ const start = offset;
1559
+ const end = perPageInput === false ? total : start + perPage;
1560
+ const paginatedMessages = messagesData.slice(start, end);
1561
+ const messageIdsSet = /* @__PURE__ */ new Set();
1562
+ const allMessages = [];
1563
+ for (const msg of paginatedMessages) {
1564
+ if (messageIdsSet.has(msg.id)) continue;
1565
+ allMessages.push(msg);
1566
+ messageIdsSet.add(msg.id);
1567
+ }
1568
+ for (const msg of includedMessages) {
1569
+ if (messageIdsSet.has(msg.id)) continue;
1570
+ allMessages.push(msg);
1571
+ messageIdsSet.add(msg.id);
1572
+ }
1573
+ let finalMessages = new MessageList().add(allMessages, "memory").get.all.db();
1574
+ finalMessages = finalMessages.sort((a, b) => {
1575
+ const aValue = getFieldValue(a);
1576
+ const bValue = getFieldValue(b);
1577
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
1578
+ });
1579
+ const returnedThreadMessageIds = new Set(finalMessages.filter((message) => message.threadId && threadIdsSet.has(message.threadId)).map((message) => message.id));
1580
+ const hasMore = perPageInput !== false && (metadataFilter || returnedThreadMessageIds.size < total) && offset + paginatedMessages.length < total;
1581
+ return {
1582
+ messages: finalMessages,
1583
+ total,
1584
+ page,
1585
+ perPage: perPageForResponse,
1586
+ hasMore
1587
+ };
1588
+ } catch (error) {
1589
+ if (error instanceof MastraError && error.category === ErrorCategory.USER) throw error;
1590
+ const mastraError = new MastraError({
1591
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_MESSAGES", "FAILED"),
1592
+ domain: ErrorDomain.STORAGE,
1593
+ category: ErrorCategory.THIRD_PARTY,
1594
+ details: {
1595
+ threadId: Array.isArray(threadId) ? threadId.join(",") : threadId,
1596
+ resourceId: resourceId ?? ""
1597
+ }
1598
+ }, error);
1599
+ this.logger.error(mastraError.toString());
1600
+ this.logger.trackException(mastraError);
1601
+ throw mastraError;
1602
+ }
1603
+ }
1604
+ async getResourceById({ resourceId }) {
1605
+ try {
1606
+ const resource = await this.db.getByKey({
1607
+ tableName: TABLE_RESOURCES,
1608
+ key: `${TABLE_RESOURCES}:${resourceId}`
1609
+ });
1610
+ if (!resource) return null;
1611
+ return {
1612
+ ...resource,
1613
+ createdAt: new Date(resource.createdAt),
1614
+ updatedAt: new Date(resource.updatedAt),
1615
+ workingMemory: typeof resource.workingMemory === "object" ? JSON.stringify(resource.workingMemory) : resource.workingMemory,
1616
+ metadata: typeof resource.metadata === "string" ? JSON.parse(resource.metadata) : resource.metadata
1617
+ };
1618
+ } catch (error) {
1619
+ this.logger.error("Error getting resource by ID:", error);
1620
+ throw error;
1621
+ }
1622
+ }
1623
+ async saveResource({ resource }) {
1624
+ try {
1625
+ const serializedResource = {
1626
+ ...resource,
1627
+ metadata: JSON.stringify(resource.metadata),
1628
+ createdAt: resource.createdAt.toISOString(),
1629
+ updatedAt: resource.updatedAt.toISOString()
1630
+ };
1631
+ await this.db.set({
1632
+ tableName: TABLE_RESOURCES,
1633
+ key: `${TABLE_RESOURCES}:${resource.id}`,
1634
+ value: serializedResource
1635
+ });
1636
+ return resource;
1637
+ } catch (error) {
1638
+ this.logger.error("Error saving resource:", error);
1639
+ throw error;
1640
+ }
1641
+ }
1642
+ async updateResource({ resourceId, workingMemory, metadata }) {
1643
+ try {
1644
+ const existingResource = await this.getResourceById({ resourceId });
1645
+ if (!existingResource) {
1646
+ const newResource = {
1647
+ id: resourceId,
1648
+ workingMemory,
1649
+ metadata: metadata || {},
1650
+ createdAt: /* @__PURE__ */ new Date(),
1651
+ updatedAt: /* @__PURE__ */ new Date()
1652
+ };
1653
+ return this.saveResource({ resource: newResource });
1654
+ }
1655
+ const updatedResource = {
1656
+ ...existingResource,
1657
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1658
+ metadata: {
1659
+ ...existingResource.metadata,
1660
+ ...metadata
1661
+ },
1662
+ updatedAt: /* @__PURE__ */ new Date()
1663
+ };
1664
+ await this.saveResource({ resource: updatedResource });
1665
+ return updatedResource;
1666
+ } catch (error) {
1667
+ this.logger.error("Error updating resource:", error);
1668
+ throw error;
1669
+ }
1670
+ }
1671
+ async updateMessages(args) {
1672
+ const { messages } = args;
1673
+ if (messages.length === 0) return [];
1674
+ try {
1675
+ const messageIds = messages.map((m) => m.id);
1676
+ const allMessages = await this.listAllMessages();
1677
+ const existingMessages = [];
1678
+ const messageIdToKey = {};
1679
+ for (const messageId of messageIds) {
1680
+ const message = allMessages.find((msg) => msg.id === messageId);
1681
+ if (message?.threadId) {
1682
+ existingMessages.push(message);
1683
+ messageIdToKey[messageId] = getMessageKey(message.threadId, messageId);
1684
+ }
1685
+ }
1686
+ if (existingMessages.length === 0) return [];
1687
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
1688
+ const keysToDelete = [];
1689
+ const entries = [];
1690
+ for (const existingMessage of existingMessages) {
1691
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
1692
+ if (!updatePayload) continue;
1693
+ const { id, ...fieldsToUpdate } = updatePayload;
1694
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
1695
+ threadIdsToUpdate.add(existingMessage.threadId);
1696
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) threadIdsToUpdate.add(updatePayload.threadId);
1697
+ const updatedMessage = { ...existingMessage };
1698
+ if (fieldsToUpdate.content) {
1699
+ const existingContent = existingMessage.content;
1700
+ updatedMessage.content = {
1701
+ ...existingContent,
1702
+ ...fieldsToUpdate.content,
1703
+ ...existingContent?.metadata && fieldsToUpdate.content.metadata ? { metadata: {
1704
+ ...existingContent.metadata,
1705
+ ...fieldsToUpdate.content.metadata
1706
+ } } : {}
1707
+ };
1708
+ }
1709
+ for (const key in fieldsToUpdate) if (Object.prototype.hasOwnProperty.call(fieldsToUpdate, key) && key !== "content") updatedMessage[key] = fieldsToUpdate[key];
1710
+ const key = messageIdToKey[id];
1711
+ if (!key) continue;
1712
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1713
+ keysToDelete.push(key);
1714
+ const newKey = getMessageKey(updatePayload.threadId, id);
1715
+ entries.push({
1716
+ key: newKey,
1717
+ value: updatedMessage
1718
+ });
1719
+ entries.push({
1720
+ key: getMessageIndexKey(id),
1721
+ value: { threadId: updatePayload.threadId }
1722
+ });
1723
+ messageIdToKey[id] = newKey;
1724
+ continue;
1725
+ }
1726
+ entries.push({
1727
+ key,
1728
+ value: updatedMessage
1729
+ });
1730
+ }
1731
+ const now = /* @__PURE__ */ new Date();
1732
+ const threadEntries = [];
1733
+ for (const threadId of threadIdsToUpdate) if (threadId) {
1734
+ const existingThread = await this.db.get({
1735
+ tableName: TABLE_THREADS,
1736
+ keys: { id: threadId }
1737
+ });
1738
+ if (existingThread) {
1739
+ const updatedThread = {
1740
+ ...existingThread,
1741
+ updatedAt: now
1742
+ };
1743
+ threadEntries.push({
1744
+ key: getKey(TABLE_THREADS, { id: threadId }),
1745
+ value: processRecord(TABLE_THREADS, updatedThread).processedRecord
1746
+ });
1747
+ }
1748
+ }
1749
+ await this.db.deleteMany({
1750
+ tableName: TABLE_MESSAGES,
1751
+ keys: keysToDelete
1752
+ });
1753
+ await this.db.bulkSet({
1754
+ tableName: TABLE_MESSAGES,
1755
+ entries
1756
+ });
1757
+ await this.db.bulkSet({
1758
+ tableName: TABLE_THREADS,
1759
+ entries: threadEntries
1760
+ });
1761
+ const updatedMessages = [];
1762
+ for (const messageId of messageIds) {
1763
+ const key = messageIdToKey[messageId];
1764
+ if (key) {
1765
+ const message = await this.db.getByKey({
1766
+ tableName: TABLE_MESSAGES,
1767
+ key
1768
+ });
1769
+ if (message) updatedMessages.push(message);
1770
+ }
1771
+ }
1772
+ return updatedMessages;
1773
+ } catch (error) {
1774
+ throw new MastraError({
1775
+ id: createStorageErrorId("ELASTICSEARCH", "UPDATE_MESSAGES", "FAILED"),
1776
+ domain: ErrorDomain.STORAGE,
1777
+ category: ErrorCategory.THIRD_PARTY,
1778
+ details: { messageIds: messages.map((m) => m.id).join(",") }
1779
+ }, error);
1780
+ }
1781
+ }
1782
+ async deleteMessages(messageIds) {
1783
+ if (!messageIds || messageIds.length === 0) return;
1784
+ try {
1785
+ const allMessages = await this.listAllMessages();
1786
+ const idsToDelete = new Set(messageIds);
1787
+ const threadIds = /* @__PURE__ */ new Set();
1788
+ const keysToDelete = [];
1789
+ for (const message of allMessages) {
1790
+ if (!idsToDelete.has(message.id) || !message.threadId) continue;
1791
+ keysToDelete.push(getMessageKey(message.threadId, message.id));
1792
+ keysToDelete.push(getMessageIndexKey(message.id));
1793
+ threadIds.add(message.threadId);
1794
+ }
1795
+ if (keysToDelete.length === 0) return;
1796
+ await this.db.deleteMany({
1797
+ tableName: TABLE_MESSAGES,
1798
+ keys: keysToDelete
1799
+ });
1800
+ const threadEntries = [];
1801
+ for (const threadId of threadIds) {
1802
+ const thread = await this.db.get({
1803
+ tableName: TABLE_THREADS,
1804
+ keys: { id: threadId }
1805
+ });
1806
+ if (!thread) continue;
1807
+ const updatedThread = {
1808
+ ...thread,
1809
+ updatedAt: /* @__PURE__ */ new Date()
1810
+ };
1811
+ threadEntries.push({
1812
+ key: getKey(TABLE_THREADS, { id: threadId }),
1813
+ value: processRecord(TABLE_THREADS, updatedThread).processedRecord
1814
+ });
1815
+ }
1816
+ await this.db.bulkSet({
1817
+ tableName: TABLE_THREADS,
1818
+ entries: threadEntries
1819
+ });
1820
+ } catch (error) {
1821
+ throw new MastraError({
1822
+ id: createStorageErrorId("ELASTICSEARCH", "DELETE_MESSAGES", "FAILED"),
1823
+ domain: ErrorDomain.STORAGE,
1824
+ category: ErrorCategory.THIRD_PARTY,
1825
+ details: { messageIds: messageIds.join(", ") }
1826
+ }, error);
1827
+ }
1828
+ }
1829
+ sortThreads(threads, field, direction) {
1830
+ return threads.sort((a, b) => {
1831
+ const aValue = new Date(a[field]).getTime();
1832
+ const bValue = new Date(b[field]).getTime();
1833
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
1834
+ });
1835
+ }
1836
+ async cloneThread(args) {
1837
+ const { sourceThreadId, newThreadId: providedThreadId, resourceId, title, metadata, options } = args;
1838
+ const sourceThread = await this.getThreadById({ threadId: sourceThreadId });
1839
+ if (!sourceThread) throw new MastraError({
1840
+ id: createStorageErrorId("ELASTICSEARCH", "CLONE_THREAD", "SOURCE_NOT_FOUND"),
1841
+ domain: ErrorDomain.STORAGE,
1842
+ category: ErrorCategory.USER,
1843
+ text: `Source thread with id ${sourceThreadId} not found`,
1844
+ details: { sourceThreadId }
1845
+ });
1846
+ const newThreadId = providedThreadId || crypto.randomUUID();
1847
+ if (await this.getThreadById({ threadId: newThreadId })) throw new MastraError({
1848
+ id: createStorageErrorId("ELASTICSEARCH", "CLONE_THREAD", "THREAD_EXISTS"),
1849
+ domain: ErrorDomain.STORAGE,
1850
+ category: ErrorCategory.USER,
1851
+ text: `Thread with id ${newThreadId} already exists`,
1852
+ details: { newThreadId }
1853
+ });
1854
+ try {
1855
+ let sourceMessages = (await this.listThreadMessages(sourceThreadId)).map((msg) => ({
1856
+ ...msg,
1857
+ createdAt: new Date(msg.createdAt)
1858
+ }));
1859
+ if (options?.messageFilter?.startDate || options?.messageFilter?.endDate) sourceMessages = filterByDateRange(sourceMessages, (msg) => new Date(msg.createdAt), {
1860
+ start: options.messageFilter?.startDate,
1861
+ end: options.messageFilter?.endDate
1862
+ });
1863
+ if (options?.messageFilter?.messageIds && options.messageFilter.messageIds.length > 0) {
1864
+ const messageIdSet = new Set(options.messageFilter.messageIds);
1865
+ sourceMessages = sourceMessages.filter((msg) => messageIdSet.has(msg.id));
1866
+ }
1867
+ sourceMessages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
1868
+ if (options?.messageLimit && options.messageLimit > 0 && sourceMessages.length > options.messageLimit) sourceMessages = sourceMessages.slice(-options.messageLimit);
1869
+ const now = /* @__PURE__ */ new Date();
1870
+ const lastMessageId = sourceMessages.length > 0 ? sourceMessages[sourceMessages.length - 1].id : void 0;
1871
+ const cloneMetadata = {
1872
+ sourceThreadId,
1873
+ clonedAt: now,
1874
+ ...lastMessageId && { lastMessageId }
1875
+ };
1876
+ const newThread = {
1877
+ id: newThreadId,
1878
+ resourceId: resourceId || sourceThread.resourceId,
1879
+ title: title || (sourceThread.title ? `Clone of ${sourceThread.title}` : void 0),
1880
+ metadata: {
1881
+ ...metadata,
1882
+ clone: cloneMetadata
1883
+ },
1884
+ createdAt: now,
1885
+ updatedAt: now
1886
+ };
1887
+ const clonedMessages = [];
1888
+ const targetResourceId = resourceId || sourceThread.resourceId;
1889
+ const entries = [];
1890
+ for (let i = 0; i < sourceMessages.length; i++) {
1891
+ const sourceMsg = sourceMessages[i];
1892
+ const newMessageId = crypto.randomUUID();
1893
+ const { _index, ...restMsg } = sourceMsg;
1894
+ const newMessage = {
1895
+ ...restMsg,
1896
+ id: newMessageId,
1897
+ threadId: newThreadId,
1898
+ resourceId: targetResourceId
1899
+ };
1900
+ entries.push({
1901
+ key: getMessageKey(newThreadId, newMessageId),
1902
+ value: {
1903
+ ...newMessage,
1904
+ _index: i
1905
+ }
1906
+ });
1907
+ entries.push({
1908
+ key: getMessageIndexKey(newMessageId),
1909
+ value: { threadId: newThreadId }
1910
+ });
1911
+ clonedMessages.push(newMessage);
1912
+ }
1913
+ await this.db.insert({
1914
+ tableName: TABLE_THREADS,
1915
+ record: newThread
1916
+ });
1917
+ await this.db.bulkSet({
1918
+ tableName: TABLE_MESSAGES,
1919
+ entries
1920
+ });
1921
+ return {
1922
+ thread: newThread,
1923
+ clonedMessages
1924
+ };
1925
+ } catch (error) {
1926
+ if (error instanceof MastraError) throw error;
1927
+ throw new MastraError({
1928
+ id: createStorageErrorId("ELASTICSEARCH", "CLONE_THREAD", "FAILED"),
1929
+ domain: ErrorDomain.STORAGE,
1930
+ category: ErrorCategory.THIRD_PARTY,
1931
+ details: {
1932
+ sourceThreadId,
1933
+ newThreadId
1934
+ }
1935
+ }, error);
1936
+ }
1937
+ }
1938
+ };
1939
+ function threadMessagesPrefix(threadId) {
1940
+ return `${TABLE_MESSAGES}:threadId:${threadId}:id:`;
1941
+ }
1942
+ function getMessageKey(threadId, messageId) {
1943
+ return getKey(TABLE_MESSAGES, {
1944
+ threadId,
1945
+ id: messageId
1946
+ });
1947
+ }
1948
+ function getMessageIndexKey(messageId) {
1949
+ return `msg-idx:${messageId}`;
1950
+ }
1951
+ function getMessageScore(message) {
1952
+ const createdAtScore = new Date(message.createdAt).getTime();
1953
+ const index = typeof message._index === "number" ? message._index : 0;
1954
+ return createdAtScore * 1e3 + index;
1955
+ }
1956
+ //#endregion
1957
+ //#region src/storage/domains/scores/index.ts
1958
+ /** Returns true when a row matches the multi-tenant scope filters (or none provided). */
1959
+ function matchesTenancy(row, filters) {
1960
+ if (filters?.organizationId !== void 0 && row.organizationId !== filters.organizationId) return false;
1961
+ if (filters?.projectId !== void 0 && row.projectId !== filters.projectId) return false;
1962
+ return true;
1963
+ }
1964
+ var ScoresElasticSearch = class extends ScoresStorage {
1965
+ db;
1966
+ constructor(config) {
1967
+ super();
1968
+ this.db = new ElasticSearchDB({ client: config.client });
1969
+ }
1970
+ async dangerouslyClearAll() {
1971
+ await this.db.deleteData({ tableName: TABLE_SCORERS });
1972
+ }
1973
+ async getScoreById({ id }) {
1974
+ try {
1975
+ const data = await this.db.get({
1976
+ tableName: TABLE_SCORERS,
1977
+ keys: { id }
1978
+ });
1979
+ if (!data) return null;
1980
+ return transformScoreRow(data);
1981
+ } catch (error) {
1982
+ throw new MastraError({
1983
+ id: createStorageErrorId("ELASTICSEARCH", "GET_SCORE_BY_ID", "FAILED"),
1984
+ domain: ErrorDomain.STORAGE,
1985
+ category: ErrorCategory.THIRD_PARTY,
1986
+ details: { ...id && { id } }
1987
+ }, error);
1988
+ }
1989
+ }
1990
+ async listScoresByScorerId({ scorerId, entityId, entityType, source, pagination = {
1991
+ page: 0,
1992
+ perPage: 20
1993
+ }, filters }) {
1994
+ return this.fetchAndFilterScores(pagination, (row) => {
1995
+ if (row.scorerId !== scorerId) return false;
1996
+ if (entityId && row.entityId !== entityId) return false;
1997
+ if (entityType && row.entityType !== entityType) return false;
1998
+ if (source && row.source !== source) return false;
1999
+ if (!matchesTenancy(row, filters)) return false;
2000
+ return true;
2001
+ });
2002
+ }
2003
+ async saveScore(score) {
2004
+ let validatedScore;
2005
+ try {
2006
+ validatedScore = saveScorePayloadSchema.parse(score);
2007
+ } catch (error) {
2008
+ throw new MastraError({
2009
+ id: createStorageErrorId("ELASTICSEARCH", "SAVE_SCORE", "VALIDATION_FAILED"),
2010
+ domain: ErrorDomain.STORAGE,
2011
+ category: ErrorCategory.USER,
2012
+ details: {
2013
+ scorer: typeof score.scorer?.id === "string" ? score.scorer.id : String(score.scorer?.id ?? "unknown"),
2014
+ entityId: score.entityId ?? "unknown",
2015
+ entityType: score.entityType ?? "unknown",
2016
+ traceId: score.traceId ?? "",
2017
+ spanId: score.spanId ?? ""
2018
+ }
2019
+ }, error);
2020
+ }
2021
+ const now = /* @__PURE__ */ new Date();
2022
+ const id = crypto$1.randomUUID();
2023
+ const { key, processedRecord } = processRecord(TABLE_SCORERS, {
2024
+ ...validatedScore,
2025
+ id,
2026
+ createdAt: now,
2027
+ updatedAt: now
2028
+ });
2029
+ try {
2030
+ await this.db.set({
2031
+ tableName: TABLE_SCORERS,
2032
+ key,
2033
+ value: processedRecord
2034
+ });
2035
+ return { score: {
2036
+ ...validatedScore,
2037
+ id,
2038
+ createdAt: now,
2039
+ updatedAt: now
2040
+ } };
2041
+ } catch (error) {
2042
+ throw new MastraError({
2043
+ id: createStorageErrorId("ELASTICSEARCH", "SAVE_SCORE", "FAILED"),
2044
+ domain: ErrorDomain.STORAGE,
2045
+ category: ErrorCategory.THIRD_PARTY,
2046
+ details: { id }
2047
+ }, error);
2048
+ }
2049
+ }
2050
+ async listScoresByRunId({ runId, pagination = {
2051
+ page: 0,
2052
+ perPage: 20
2053
+ }, filters }) {
2054
+ return this.fetchAndFilterScores(pagination, (row) => row.runId === runId && matchesTenancy(row, filters));
2055
+ }
2056
+ async listScoresByEntityId({ entityId, entityType, pagination = {
2057
+ page: 0,
2058
+ perPage: 20
2059
+ }, filters }) {
2060
+ return this.fetchAndFilterScores(pagination, (row) => {
2061
+ if (row.entityId !== entityId) return false;
2062
+ if (entityType && row.entityType !== entityType) return false;
2063
+ if (!matchesTenancy(row, filters)) return false;
2064
+ return true;
2065
+ });
2066
+ }
2067
+ async listScoresBySpan({ traceId, spanId, pagination = {
2068
+ page: 0,
2069
+ perPage: 20
2070
+ }, filters }) {
2071
+ return this.fetchAndFilterScores(pagination, (row) => row.traceId === traceId && row.spanId === spanId && matchesTenancy(row, filters));
2072
+ }
2073
+ async fetchAndFilterScores(pagination, filterFn) {
2074
+ const { page, perPage: perPageInput } = pagination;
2075
+ const filtered = (await this.db.listAll({ tableName: TABLE_SCORERS })).filter((row) => !!row && typeof row === "object" && filterFn(row));
2076
+ const total = filtered.length;
2077
+ const perPage = normalizePerPage(perPageInput, 100);
2078
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
2079
+ const end = perPageInput === false ? total : start + perPage;
2080
+ return {
2081
+ scores: filtered.slice(start, end).map((row) => transformScoreRow(row)),
2082
+ pagination: {
2083
+ total,
2084
+ page,
2085
+ perPage: perPageForResponse,
2086
+ hasMore: end < total
2087
+ }
2088
+ };
2089
+ }
2090
+ };
2091
+ //#endregion
2092
+ //#region src/storage/domains/workflows/index.ts
2093
+ function parseWorkflowRun(row) {
2094
+ let parsedSnapshot = row.snapshot;
2095
+ if (typeof parsedSnapshot === "string") try {
2096
+ parsedSnapshot = JSON.parse(row.snapshot);
2097
+ } catch (e) {
2098
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
2099
+ }
2100
+ return {
2101
+ workflowName: row.workflow_name,
2102
+ runId: row.run_id,
2103
+ snapshot: parsedSnapshot,
2104
+ createdAt: ensureDate(row.createdAt),
2105
+ updatedAt: ensureDate(row.updatedAt),
2106
+ resourceId: row.resourceId
2107
+ };
2108
+ }
2109
+ var WorkflowsElasticSearch = class extends WorkflowsStorage {
2110
+ db;
2111
+ constructor(config) {
2112
+ super();
2113
+ this.db = new ElasticSearchDB({ client: config.client });
2114
+ }
2115
+ supportsConcurrentUpdates() {
2116
+ return false;
2117
+ }
2118
+ async dangerouslyClearAll() {
2119
+ await this.db.deleteData({ tableName: TABLE_WORKFLOW_SNAPSHOT });
2120
+ }
2121
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext }) {
2122
+ try {
2123
+ const existingRecord = await this.db.get({
2124
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2125
+ keys: {
2126
+ namespace: "workflows",
2127
+ workflow_name: workflowName,
2128
+ run_id: runId
2129
+ }
2130
+ });
2131
+ let snapshot = existingRecord?.snapshot;
2132
+ if (!snapshot) snapshot = {
2133
+ context: {},
2134
+ activePaths: [],
2135
+ timestamp: Date.now(),
2136
+ suspendedPaths: {},
2137
+ activeStepsPath: {},
2138
+ resumeLabels: {},
2139
+ serializedStepGraph: [],
2140
+ status: "pending",
2141
+ value: {},
2142
+ waitingPaths: {},
2143
+ runId,
2144
+ requestContext: {}
2145
+ };
2146
+ snapshot.context[stepId] = result;
2147
+ snapshot.requestContext = {
2148
+ ...snapshot.requestContext,
2149
+ ...requestContext
2150
+ };
2151
+ await this.persistWorkflowSnapshot({
2152
+ namespace: "workflows",
2153
+ workflowName,
2154
+ runId,
2155
+ snapshot,
2156
+ createdAt: existingRecord?.createdAt ? ensureDate(existingRecord.createdAt) : void 0
2157
+ });
2158
+ return snapshot.context;
2159
+ } catch (error) {
2160
+ if (error instanceof MastraError) throw error;
2161
+ throw new MastraError({
2162
+ id: createStorageErrorId("ELASTICSEARCH", "UPDATE_WORKFLOW_RESULTS", "FAILED"),
2163
+ domain: ErrorDomain.STORAGE,
2164
+ category: ErrorCategory.THIRD_PARTY,
2165
+ details: {
2166
+ workflowName,
2167
+ runId,
2168
+ stepId
2169
+ }
2170
+ }, error);
2171
+ }
2172
+ }
2173
+ async updateWorkflowState({ workflowName, runId, opts }) {
2174
+ try {
2175
+ const existingRecord = await this.db.get({
2176
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2177
+ keys: {
2178
+ namespace: "workflows",
2179
+ workflow_name: workflowName,
2180
+ run_id: runId
2181
+ }
2182
+ });
2183
+ const existingSnapshot = existingRecord?.snapshot;
2184
+ if (!existingSnapshot || !existingSnapshot.context) return;
2185
+ const { expectedStatus, ...state } = opts;
2186
+ if (!matchesExpectedWorkflowStatus(existingSnapshot.status, expectedStatus)) return;
2187
+ const updatedSnapshot = {
2188
+ ...existingSnapshot,
2189
+ ...state
2190
+ };
2191
+ await this.persistWorkflowSnapshot({
2192
+ namespace: "workflows",
2193
+ workflowName,
2194
+ runId,
2195
+ snapshot: updatedSnapshot,
2196
+ createdAt: existingRecord?.createdAt ? ensureDate(existingRecord.createdAt) : void 0
2197
+ });
2198
+ return updatedSnapshot;
2199
+ } catch (error) {
2200
+ if (error instanceof MastraError) throw error;
2201
+ throw new MastraError({
2202
+ id: createStorageErrorId("ELASTICSEARCH", "UPDATE_WORKFLOW_STATE", "FAILED"),
2203
+ domain: ErrorDomain.STORAGE,
2204
+ category: ErrorCategory.THIRD_PARTY,
2205
+ details: {
2206
+ workflowName,
2207
+ runId
2208
+ }
2209
+ }, error);
2210
+ }
2211
+ }
2212
+ async persistWorkflowSnapshot(params) {
2213
+ const { namespace = "workflows", workflowName, runId, resourceId, snapshot, createdAt, updatedAt } = params;
2214
+ try {
2215
+ let finalCreatedAt = createdAt;
2216
+ if (!finalCreatedAt) {
2217
+ const existing = await this.db.get({
2218
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2219
+ keys: {
2220
+ namespace,
2221
+ workflow_name: workflowName,
2222
+ run_id: runId
2223
+ }
2224
+ });
2225
+ finalCreatedAt = existing?.createdAt ? ensureDate(existing.createdAt) : /* @__PURE__ */ new Date();
2226
+ }
2227
+ await this.db.insert({
2228
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2229
+ record: {
2230
+ namespace,
2231
+ workflow_name: workflowName,
2232
+ run_id: runId,
2233
+ resourceId,
2234
+ snapshot,
2235
+ createdAt: finalCreatedAt,
2236
+ updatedAt: updatedAt ?? /* @__PURE__ */ new Date()
2237
+ }
2238
+ });
2239
+ } catch (error) {
2240
+ throw new MastraError({
2241
+ id: createStorageErrorId("ELASTICSEARCH", "PERSIST_WORKFLOW_SNAPSHOT", "FAILED"),
2242
+ domain: ErrorDomain.STORAGE,
2243
+ category: ErrorCategory.THIRD_PARTY,
2244
+ details: {
2245
+ namespace,
2246
+ workflowName,
2247
+ runId
2248
+ }
2249
+ }, error);
2250
+ }
2251
+ }
2252
+ async loadWorkflowSnapshot(params) {
2253
+ const { namespace = "workflows", workflowName, runId } = params;
2254
+ try {
2255
+ return (await this.db.get({
2256
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2257
+ keys: {
2258
+ namespace,
2259
+ workflow_name: workflowName,
2260
+ run_id: runId
2261
+ }
2262
+ }))?.snapshot ?? null;
2263
+ } catch (error) {
2264
+ throw new MastraError({
2265
+ id: createStorageErrorId("ELASTICSEARCH", "LOAD_WORKFLOW_SNAPSHOT", "FAILED"),
2266
+ domain: ErrorDomain.STORAGE,
2267
+ category: ErrorCategory.THIRD_PARTY,
2268
+ details: {
2269
+ namespace,
2270
+ workflowName,
2271
+ runId
2272
+ }
2273
+ }, error);
2274
+ }
2275
+ }
2276
+ async getWorkflowRunById({ runId, workflowName }) {
2277
+ try {
2278
+ const data = (await this.db.listAll({
2279
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2280
+ keyPrefix: getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace: "workflows" })
2281
+ })).find((workflow) => {
2282
+ if (!workflow) return false;
2283
+ const runIdMatch = workflow.run_id === runId;
2284
+ if (workflowName) return runIdMatch && workflow.workflow_name === workflowName;
2285
+ return runIdMatch;
2286
+ });
2287
+ if (!data) return null;
2288
+ return parseWorkflowRun(data);
2289
+ } catch (error) {
2290
+ throw new MastraError({
2291
+ id: createStorageErrorId("ELASTICSEARCH", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
2292
+ domain: ErrorDomain.STORAGE,
2293
+ category: ErrorCategory.THIRD_PARTY,
2294
+ details: {
2295
+ namespace: "workflows",
2296
+ runId,
2297
+ workflowName: workflowName || ""
2298
+ }
2299
+ }, error);
2300
+ }
2301
+ }
2302
+ async deleteWorkflowRunById({ runId, workflowName }) {
2303
+ const key = getKey(TABLE_WORKFLOW_SNAPSHOT, {
2304
+ namespace: "workflows",
2305
+ workflow_name: workflowName,
2306
+ run_id: runId
2307
+ });
2308
+ try {
2309
+ await this.db.delete({
2310
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2311
+ key
2312
+ });
2313
+ } catch (error) {
2314
+ throw new MastraError({
2315
+ id: createStorageErrorId("ELASTICSEARCH", "DELETE_WORKFLOW_RUN_BY_ID", "FAILED"),
2316
+ domain: ErrorDomain.STORAGE,
2317
+ category: ErrorCategory.THIRD_PARTY,
2318
+ details: {
2319
+ namespace: "workflows",
2320
+ runId,
2321
+ workflowName
2322
+ }
2323
+ }, error);
2324
+ }
2325
+ }
2326
+ async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, status } = {}) {
2327
+ try {
2328
+ if (page !== void 0 && page < 0) throw new MastraError({
2329
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_WORKFLOW_RUNS", "INVALID_PAGE"),
2330
+ domain: ErrorDomain.STORAGE,
2331
+ category: ErrorCategory.USER,
2332
+ details: { page }
2333
+ }, /* @__PURE__ */ new Error("page must be >= 0"));
2334
+ const normalizedFrom = fromDate ? ensureDate(fromDate) : void 0;
2335
+ const normalizedTo = toDate ? ensureDate(toDate) : void 0;
2336
+ let keyPrefix = getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace: "workflows" });
2337
+ if (workflowName) keyPrefix = getKey(TABLE_WORKFLOW_SNAPSHOT, {
2338
+ namespace: "workflows",
2339
+ workflow_name: workflowName
2340
+ }) + ":";
2341
+ let runs = (await this.db.listAll({
2342
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2343
+ keyPrefix
2344
+ })).filter((record) => record !== null && record !== void 0 && typeof record === "object" && "workflow_name" in record).filter((record) => !workflowName || record.workflow_name === workflowName).filter((record) => !resourceId || record.resourceId === resourceId).map((w) => parseWorkflowRun(w)).filter((w) => {
2345
+ if (normalizedFrom && w.createdAt < normalizedFrom) return false;
2346
+ if (normalizedTo && w.createdAt > normalizedTo) return false;
2347
+ if (status) {
2348
+ let snapshot = w.snapshot;
2349
+ if (typeof snapshot === "string") try {
2350
+ snapshot = JSON.parse(snapshot);
2351
+ } catch (e) {
2352
+ console.warn(`Failed to parse snapshot for workflow ${w.workflowName}: ${e}`);
2353
+ return false;
2354
+ }
2355
+ return snapshot.status === status;
2356
+ }
2357
+ return true;
2358
+ }).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
2359
+ const total = runs.length;
2360
+ if (typeof perPage === "number" && typeof page === "number") {
2361
+ const normalizedPerPage = normalizePerPage(perPage, Number.MAX_SAFE_INTEGER);
2362
+ const offset = page * normalizedPerPage;
2363
+ runs = runs.slice(offset, offset + normalizedPerPage);
2364
+ }
2365
+ return {
2366
+ runs,
2367
+ total
2368
+ };
2369
+ } catch (error) {
2370
+ if (error instanceof MastraError) throw error;
2371
+ throw new MastraError({
2372
+ id: createStorageErrorId("ELASTICSEARCH", "LIST_WORKFLOW_RUNS", "FAILED"),
2373
+ domain: ErrorDomain.STORAGE,
2374
+ category: ErrorCategory.THIRD_PARTY,
2375
+ details: {
2376
+ namespace: "workflows",
2377
+ workflowName: workflowName || "",
2378
+ resourceId: resourceId || ""
2379
+ }
2380
+ }, error);
2381
+ }
2382
+ }
2383
+ };
2384
+ //#endregion
2385
+ //#region src/storage/store.ts
2386
+ /**
2387
+ * ElasticSearch storage adapter for Mastra.
2388
+ *
2389
+ * Implements the memory, workflows, and scores storage domains on top of
2390
+ * ElasticSearch. Shares the same connection config surface as
2391
+ * `ElasticSearchVector`, so both can reuse one client or connection config.
2392
+ *
2393
+ * @example
2394
+ * ```typescript
2395
+ * // Using connection parameters
2396
+ * const storage = new ElasticSearchStore({
2397
+ * id: 'my-store',
2398
+ * url: 'http://localhost:9200',
2399
+ * auth: { apiKey: '...' },
2400
+ * });
2401
+ *
2402
+ * // Access memory domain
2403
+ * const memory = await storage.getStore('memory');
2404
+ * await memory?.saveThread({ thread });
2405
+ * ```
2406
+ *
2407
+ * @example
2408
+ * ```typescript
2409
+ * // Using a pre-configured client shared with ElasticSearchVector
2410
+ * import { Client } from '@elastic/elasticsearch';
2411
+ *
2412
+ * const client = new Client({ node: 'http://localhost:9200' });
2413
+ * const storage = new ElasticSearchStore({ id: 'my-store', client });
2414
+ * const vector = new ElasticSearchVector({ id: 'my-vector', client });
2415
+ * ```
2416
+ */
2417
+ var ElasticSearchStore = class extends MastraStorage {
2418
+ client;
2419
+ shouldManageConnection;
2420
+ stores;
2421
+ constructor(config) {
2422
+ super({
2423
+ id: config.id,
2424
+ name: "ElasticSearch",
2425
+ disableInit: config.disableInit
2426
+ });
2427
+ if ("client" in config && config.client) {
2428
+ this.client = config.client;
2429
+ this.shouldManageConnection = false;
2430
+ } else if ("url" in config && config.url) {
2431
+ this.client = new Client({
2432
+ node: config.url,
2433
+ ...config.auth && { auth: config.auth },
2434
+ name: "mastra-elasticsearch",
2435
+ headers: { "user-agent": `mastra-es/${version}` }
2436
+ });
2437
+ this.shouldManageConnection = true;
2438
+ } else throw new MastraError({
2439
+ id: "ELASTIC_SEARCH_STORE_CONSTRUCTOR_ERROR",
2440
+ domain: ErrorDomain.STORAGE,
2441
+ category: ErrorCategory.USER,
2442
+ text: "Invalid config: provide either { client } or { url }."
2443
+ });
2444
+ this.stores = {
2445
+ memory: new MemoryElasticSearch({ client: this.client }),
2446
+ workflows: new WorkflowsElasticSearch({ client: this.client }),
2447
+ scores: new ScoresElasticSearch({ client: this.client })
2448
+ };
2449
+ }
2450
+ getClient() {
2451
+ return this.client;
2452
+ }
2453
+ async close() {
2454
+ if (this.shouldManageConnection) await this.client.close();
2455
+ }
2456
+ };
2457
+ //#endregion
2458
+ export { ElasticSearchStore, ElasticSearchVector, MemoryElasticSearch, ScoresElasticSearch, WorkflowsElasticSearch };
844
2459
 
845
2460
  //# sourceMappingURL=index.js.map