@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/mongodb",
3
- "version": "0.11.1-alpha.0",
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.28.0",
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
- "@mastra/core": "0.10.7-alpha.0",
36
- "@internal/storage-test-utils": "0.0.9"
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 { TABLE_EVALS, TABLE_MESSAGES, TABLE_THREADS, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
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