@mastra/mongodb 0.11.1-alpha.0 → 0.11.1-alpha.2
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +28 -0
- package/dist/_tsup-dts-rollup.d.cts +17 -7
- package/dist/_tsup-dts-rollup.d.ts +17 -7
- package/dist/index.cjs +560 -226
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +539 -205
- package/package.json +4 -4
- package/src/storage/index.test.ts +239 -4
- package/src/storage/index.ts +295 -106
- package/src/vector/filter.test.ts +40 -30
- package/src/vector/filter.ts +25 -4
- package/src/vector/index.test.ts +1 -2
- package/src/vector/index.ts +275 -130
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/mongodb",
|
|
3
|
-
"version": "0.11.1-alpha.
|
|
3
|
+
"version": "0.11.1-alpha.2",
|
|
4
4
|
"description": "MongoDB provider for Mastra - includes vector store capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@microsoft/api-extractor": "^7.52.8",
|
|
29
29
|
"@types/node": "^20.19.0",
|
|
30
|
-
"eslint": "^9.
|
|
30
|
+
"eslint": "^9.29.0",
|
|
31
31
|
"tsup": "^8.5.0",
|
|
32
32
|
"typescript": "^5.8.3",
|
|
33
33
|
"vitest": "^3.2.3",
|
|
34
34
|
"@internal/lint": "0.0.13",
|
|
35
|
-
"@
|
|
36
|
-
"@
|
|
35
|
+
"@internal/storage-test-utils": "0.0.9",
|
|
36
|
+
"@mastra/core": "0.10.7-alpha.2"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@mastra/core": ">=0.10.4-0 <0.11.0"
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import type { MastraMessageV1, MastraMessageV2, MetricResult, WorkflowRunState } from '@mastra/core';
|
|
3
3
|
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TABLE_EVALS,
|
|
6
|
+
TABLE_MESSAGES,
|
|
7
|
+
TABLE_THREADS,
|
|
8
|
+
TABLE_TRACES,
|
|
9
|
+
TABLE_WORKFLOW_SNAPSHOT,
|
|
10
|
+
} from '@mastra/core/storage';
|
|
5
11
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
6
12
|
import type { MongoDBConfig } from './index';
|
|
7
13
|
import { MongoDBStore } from './index';
|
|
@@ -23,6 +29,7 @@ class Test {
|
|
|
23
29
|
await this.store.clearTable({ tableName: TABLE_MESSAGES });
|
|
24
30
|
await this.store.clearTable({ tableName: TABLE_THREADS });
|
|
25
31
|
await this.store.clearTable({ tableName: TABLE_EVALS });
|
|
32
|
+
await this.store.clearTable({ tableName: TABLE_TRACES });
|
|
26
33
|
} catch (error) {
|
|
27
34
|
// Ignore errors during table clearing
|
|
28
35
|
console.warn('Error clearing tables:', error);
|
|
@@ -78,6 +85,7 @@ class Test {
|
|
|
78
85
|
content: {
|
|
79
86
|
format: 2,
|
|
80
87
|
parts: [{ type: 'text', text: content }],
|
|
88
|
+
content: content,
|
|
81
89
|
},
|
|
82
90
|
createdAt: new Date(),
|
|
83
91
|
resourceId,
|
|
@@ -354,6 +362,83 @@ describe('MongoDBStore', () => {
|
|
|
354
362
|
expect((msg as any).content.parts).toEqual(messages[idx]!.content.parts);
|
|
355
363
|
});
|
|
356
364
|
});
|
|
365
|
+
it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
|
|
366
|
+
const test = new Test(store).build();
|
|
367
|
+
await test.clearTables();
|
|
368
|
+
const thread = test.generateSampleThread();
|
|
369
|
+
await store.saveThread({ thread });
|
|
370
|
+
const baseMessage = test.generateSampleMessageV2({
|
|
371
|
+
threadId: thread.id,
|
|
372
|
+
content: 'Original',
|
|
373
|
+
resourceId: thread.resourceId,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Insert the message for the first time
|
|
377
|
+
await store.saveMessages({ messages: [baseMessage], format: 'v2' });
|
|
378
|
+
|
|
379
|
+
// Insert again with the same id and threadId but different content
|
|
380
|
+
const updatedMessage = {
|
|
381
|
+
...test.generateSampleMessageV2({
|
|
382
|
+
threadId: thread.id,
|
|
383
|
+
content: 'Updated',
|
|
384
|
+
resourceId: thread.resourceId,
|
|
385
|
+
}),
|
|
386
|
+
createdAt: baseMessage.createdAt,
|
|
387
|
+
id: baseMessage.id,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
|
|
391
|
+
|
|
392
|
+
// Retrieve messages for the thread
|
|
393
|
+
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
394
|
+
|
|
395
|
+
// Only one message should exist for that id+threadId
|
|
396
|
+
expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
|
|
397
|
+
|
|
398
|
+
// The content should be the updated one
|
|
399
|
+
expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should upsert messages: duplicate id and different threadid', async () => {
|
|
403
|
+
const test = new Test(store).build();
|
|
404
|
+
const thread1 = test.generateSampleThread();
|
|
405
|
+
const thread2 = test.generateSampleThread();
|
|
406
|
+
await store.saveThread({ thread: thread1 });
|
|
407
|
+
await store.saveThread({ thread: thread2 });
|
|
408
|
+
|
|
409
|
+
const message = test.generateSampleMessageV2({
|
|
410
|
+
threadId: thread1.id,
|
|
411
|
+
content: 'Thread1 Content',
|
|
412
|
+
resourceId: thread1.resourceId,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Insert message into thread1
|
|
416
|
+
await store.saveMessages({ messages: [message], format: 'v2' });
|
|
417
|
+
|
|
418
|
+
// Attempt to insert a message with the same id but different threadId
|
|
419
|
+
const conflictingMessage = {
|
|
420
|
+
...test.generateSampleMessageV2({
|
|
421
|
+
threadId: thread2.id, // different thread
|
|
422
|
+
content: 'Thread2 Content',
|
|
423
|
+
resourceId: thread2.resourceId,
|
|
424
|
+
}),
|
|
425
|
+
createdAt: message.createdAt,
|
|
426
|
+
id: message.id,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Save should move the message to the new thread
|
|
430
|
+
await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
|
|
431
|
+
|
|
432
|
+
// Retrieve messages for both threads
|
|
433
|
+
const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
|
|
434
|
+
const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
|
|
435
|
+
|
|
436
|
+
// Thread 1 should NOT have the message with that id
|
|
437
|
+
expect(thread1Messages.find(m => m.id === message.id)).toBeUndefined();
|
|
438
|
+
|
|
439
|
+
// Thread 2 should have the message with that id
|
|
440
|
+
expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
|
|
441
|
+
});
|
|
357
442
|
|
|
358
443
|
// it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
|
|
359
444
|
// const test = new Test(store).build();
|
|
@@ -846,6 +931,152 @@ describe('MongoDBStore', () => {
|
|
|
846
931
|
});
|
|
847
932
|
});
|
|
848
933
|
|
|
934
|
+
describe('Trace Operations', () => {
|
|
935
|
+
const sampleTrace = (
|
|
936
|
+
name: string,
|
|
937
|
+
scope: string,
|
|
938
|
+
startTime = Date.now(),
|
|
939
|
+
attributes: Record<string, string> = {},
|
|
940
|
+
) => ({
|
|
941
|
+
id: `trace-${randomUUID()}`,
|
|
942
|
+
parentSpanId: `span-${randomUUID()}`,
|
|
943
|
+
traceId: `traceid-${randomUUID()}`,
|
|
944
|
+
name,
|
|
945
|
+
scope,
|
|
946
|
+
kind: 1,
|
|
947
|
+
startTime: startTime,
|
|
948
|
+
endTime: startTime + 100,
|
|
949
|
+
status: JSON.stringify({ code: 0 }),
|
|
950
|
+
attributes: JSON.stringify({ key: 'value', scopeAttr: scope, ...attributes }),
|
|
951
|
+
events: JSON.stringify([{ name: 'event1', timestamp: startTime + 50 }]),
|
|
952
|
+
links: JSON.stringify([]),
|
|
953
|
+
createdAt: new Date(startTime).toISOString(),
|
|
954
|
+
updatedAt: new Date(startTime).toISOString(),
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
beforeEach(async () => {
|
|
958
|
+
const test = new Test(store).build();
|
|
959
|
+
await test.clearTables();
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('should batch insert and retrieve traces', async () => {
|
|
963
|
+
const trace1 = sampleTrace('trace-op-1', 'scope-A');
|
|
964
|
+
const trace2 = sampleTrace('trace-op-2', 'scope-A', Date.now() + 10);
|
|
965
|
+
const trace3 = sampleTrace('trace-op-3', 'scope-B', Date.now() + 20);
|
|
966
|
+
const records = [trace1, trace2, trace3];
|
|
967
|
+
|
|
968
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records });
|
|
969
|
+
|
|
970
|
+
const allTraces = await store.getTraces();
|
|
971
|
+
expect(allTraces.length).toBe(3);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('should handle Date objects for createdAt/updatedAt fields in batchInsert', async () => {
|
|
975
|
+
const now = new Date();
|
|
976
|
+
const traceWithDateObjects = {
|
|
977
|
+
id: `trace-${randomUUID()}`,
|
|
978
|
+
parentSpanId: `span-${randomUUID()}`,
|
|
979
|
+
traceId: `traceid-${randomUUID()}`,
|
|
980
|
+
name: 'test-trace-with-dates',
|
|
981
|
+
scope: 'default-tracer',
|
|
982
|
+
kind: 1,
|
|
983
|
+
startTime: now.getTime(),
|
|
984
|
+
endTime: now.getTime() + 100,
|
|
985
|
+
status: JSON.stringify({ code: 0 }),
|
|
986
|
+
attributes: JSON.stringify({ key: 'value' }),
|
|
987
|
+
events: JSON.stringify([]),
|
|
988
|
+
links: JSON.stringify([]),
|
|
989
|
+
createdAt: now,
|
|
990
|
+
updatedAt: now,
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: [traceWithDateObjects] });
|
|
994
|
+
|
|
995
|
+
const allTraces = await store.getTraces({ name: 'test-trace-with-dates', page: 0, perPage: 10 });
|
|
996
|
+
expect(allTraces.length).toBe(1);
|
|
997
|
+
expect(allTraces[0].name).toBe('test-trace-with-dates');
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should retrieve traces filtered by name', async () => {
|
|
1001
|
+
const now = Date.now();
|
|
1002
|
+
const trace1 = sampleTrace('trace-filter-name', 'scope-X', now);
|
|
1003
|
+
const trace2 = sampleTrace('trace-filter-name', 'scope-Y', now + 10);
|
|
1004
|
+
const trace3 = sampleTrace('other-name', 'scope-X', now + 20);
|
|
1005
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: [trace1, trace2, trace3] });
|
|
1006
|
+
|
|
1007
|
+
const filteredTraces = await store.getTraces({ name: 'trace-filter-name', page: 0, perPage: 10 });
|
|
1008
|
+
expect(filteredTraces.length).toBe(2);
|
|
1009
|
+
expect(filteredTraces.every(t => t.name === 'trace-filter-name')).toBe(true);
|
|
1010
|
+
expect(filteredTraces[0].scope).toBe('scope-Y');
|
|
1011
|
+
expect(filteredTraces[1].scope).toBe('scope-X');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should retrieve traces filtered by attributes', async () => {
|
|
1015
|
+
const now = Date.now();
|
|
1016
|
+
const trace1 = sampleTrace('trace-filter-attribute-A', 'scope-X', now, { componentName: 'component-TARGET' });
|
|
1017
|
+
const trace2 = sampleTrace('trace-filter-attribute-B', 'scope-Y', now + 10, { componentName: 'component-OTHER' });
|
|
1018
|
+
const trace3 = sampleTrace('trace-filter-attribute-C', 'scope-Z', now + 20, {
|
|
1019
|
+
componentName: 'component-TARGET',
|
|
1020
|
+
andFilterTest: 'TARGET',
|
|
1021
|
+
});
|
|
1022
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: [trace1, trace2, trace3] });
|
|
1023
|
+
|
|
1024
|
+
const filteredTraces = await store.getTraces({
|
|
1025
|
+
attributes: { componentName: 'component-TARGET' },
|
|
1026
|
+
page: 0,
|
|
1027
|
+
perPage: 10,
|
|
1028
|
+
});
|
|
1029
|
+
expect(filteredTraces.length).toBe(2);
|
|
1030
|
+
expect(filteredTraces[0].name).toBe('trace-filter-attribute-C');
|
|
1031
|
+
expect(filteredTraces[1].name).toBe('trace-filter-attribute-A');
|
|
1032
|
+
|
|
1033
|
+
const filteredTraces2 = await store.getTraces({
|
|
1034
|
+
attributes: { componentName: 'component-TARGET', andFilterTest: 'TARGET' },
|
|
1035
|
+
page: 0,
|
|
1036
|
+
perPage: 10,
|
|
1037
|
+
});
|
|
1038
|
+
expect(filteredTraces2.length).toBe(1);
|
|
1039
|
+
expect(filteredTraces2[0].name).toBe('trace-filter-attribute-C');
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('should retrieve traces filtered by scope', async () => {
|
|
1043
|
+
const now = Date.now();
|
|
1044
|
+
const trace1 = sampleTrace('trace-filter-scope-A', 'scope-TARGET', now);
|
|
1045
|
+
const trace2 = sampleTrace('trace-filter-scope-B', 'scope-OTHER', now + 10);
|
|
1046
|
+
const trace3 = sampleTrace('trace-filter-scope-C', 'scope-TARGET', now + 20);
|
|
1047
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: [trace1, trace2, trace3] });
|
|
1048
|
+
|
|
1049
|
+
const filteredTraces = await store.getTraces({ scope: 'scope-TARGET', page: 0, perPage: 10 });
|
|
1050
|
+
expect(filteredTraces.length).toBe(2);
|
|
1051
|
+
expect(filteredTraces.every(t => t.scope === 'scope-TARGET')).toBe(true);
|
|
1052
|
+
expect(filteredTraces[0].name).toBe('trace-filter-scope-C');
|
|
1053
|
+
expect(filteredTraces[1].name).toBe('trace-filter-scope-A');
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('should handle pagination for getTraces', async () => {
|
|
1057
|
+
const now = Date.now();
|
|
1058
|
+
const traceData = Array.from({ length: 5 }, (_, i) => sampleTrace('trace-page', `scope-page`, now + i * 10));
|
|
1059
|
+
await store.batchInsert({ tableName: TABLE_TRACES, records: traceData });
|
|
1060
|
+
|
|
1061
|
+
const page1 = await store.getTraces({ name: 'trace-page', page: 0, perPage: 2 });
|
|
1062
|
+
expect(page1.length).toBe(2);
|
|
1063
|
+
expect(page1[0]!.startTime).toBe(traceData[4]!.startTime);
|
|
1064
|
+
expect(page1[1]!.startTime).toBe(traceData[3]!.startTime);
|
|
1065
|
+
|
|
1066
|
+
const page2 = await store.getTraces({ name: 'trace-page', page: 1, perPage: 2 });
|
|
1067
|
+
expect(page2.length).toBe(2);
|
|
1068
|
+
expect(page2[0]!.startTime).toBe(traceData[2]!.startTime);
|
|
1069
|
+
expect(page2[1]!.startTime).toBe(traceData[1]!.startTime);
|
|
1070
|
+
|
|
1071
|
+
const page3 = await store.getTraces({ name: 'trace-page', page: 2, perPage: 2 });
|
|
1072
|
+
expect(page3.length).toBe(1);
|
|
1073
|
+
expect(page3[0]!.startTime).toBe(traceData[0]!.startTime);
|
|
1074
|
+
|
|
1075
|
+
const page4 = await store.getTraces({ name: 'trace-page', page: 3, perPage: 2 });
|
|
1076
|
+
expect(page4.length).toBe(0);
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
|
|
849
1080
|
describe('Eval Operations', () => {
|
|
850
1081
|
it('should retrieve evals by agent name', async () => {
|
|
851
1082
|
const test = new Test(store).build();
|
|
@@ -864,7 +1095,7 @@ describe('MongoDBStore', () => {
|
|
|
864
1095
|
agent_name: liveEval.agentName,
|
|
865
1096
|
input: liveEval.input,
|
|
866
1097
|
output: liveEval.output,
|
|
867
|
-
result: liveEval.result,
|
|
1098
|
+
result: JSON.stringify(liveEval.result),
|
|
868
1099
|
metric_name: liveEval.metricName,
|
|
869
1100
|
instructions: liveEval.instructions,
|
|
870
1101
|
test_info: null,
|
|
@@ -881,7 +1112,7 @@ describe('MongoDBStore', () => {
|
|
|
881
1112
|
agent_name: testEval.agentName,
|
|
882
1113
|
input: testEval.input,
|
|
883
1114
|
output: testEval.output,
|
|
884
|
-
result: testEval.result,
|
|
1115
|
+
result: JSON.stringify(testEval.result),
|
|
885
1116
|
metric_name: testEval.metricName,
|
|
886
1117
|
instructions: testEval.instructions,
|
|
887
1118
|
test_info: JSON.stringify(testEval.testInfo),
|
|
@@ -898,7 +1129,7 @@ describe('MongoDBStore', () => {
|
|
|
898
1129
|
agent_name: otherAgentEval.agentName,
|
|
899
1130
|
input: otherAgentEval.input,
|
|
900
1131
|
output: otherAgentEval.output,
|
|
901
|
-
result: otherAgentEval.result,
|
|
1132
|
+
result: JSON.stringify(otherAgentEval.result),
|
|
902
1133
|
metric_name: otherAgentEval.metricName,
|
|
903
1134
|
instructions: otherAgentEval.instructions,
|
|
904
1135
|
test_info: null,
|
|
@@ -913,16 +1144,20 @@ describe('MongoDBStore', () => {
|
|
|
913
1144
|
const allEvals = await store.getEvalsByAgentName(agentName);
|
|
914
1145
|
expect(allEvals).toHaveLength(2);
|
|
915
1146
|
expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.runId, testEval.runId]));
|
|
1147
|
+
expect(allEvals[0]!.result.score).toEqual(liveEval.result.score);
|
|
1148
|
+
expect(allEvals[1]!.result.score).toEqual(testEval.result.score);
|
|
916
1149
|
|
|
917
1150
|
// Test getting only live evals
|
|
918
1151
|
const liveEvals = await store.getEvalsByAgentName(agentName, 'live');
|
|
919
1152
|
expect(liveEvals).toHaveLength(1);
|
|
920
1153
|
expect(liveEvals[0]!.runId).toBe(liveEval.runId);
|
|
1154
|
+
expect(liveEvals[0]!.result.score).toEqual(liveEval.result.score);
|
|
921
1155
|
|
|
922
1156
|
// Test getting only test evals
|
|
923
1157
|
const testEvals = await store.getEvalsByAgentName(agentName, 'test');
|
|
924
1158
|
expect(testEvals).toHaveLength(1);
|
|
925
1159
|
expect(testEvals[0]!.runId).toBe(testEval.runId);
|
|
1160
|
+
expect(testEvals[0]!.result.score).toEqual(testEval.result.score);
|
|
926
1161
|
expect(testEvals[0]!.testInfo).toEqual(testEval.testInfo);
|
|
927
1162
|
|
|
928
1163
|
// Test getting evals for non-existent agent
|