@mastra/pg 0.11.0 → 0.11.1-alpha.1
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 +24 -0
- package/dist/_tsup-dts-rollup.d.cts +11 -0
- package/dist/_tsup-dts-rollup.d.ts +11 -0
- package/dist/index.cjs +809 -247
- package/dist/index.js +785 -223
- package/package.json +3 -3
- package/src/storage/index.test.ts +242 -12
- package/src/storage/index.ts +606 -191
- package/src/vector/index.ts +279 -86
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/pg",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1-alpha.1",
|
|
4
4
|
"description": "Postgres provider for Mastra - includes both vector and db storage capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"tsup": "^8.5.0",
|
|
34
34
|
"typescript": "^5.8.3",
|
|
35
35
|
"vitest": "^3.2.3",
|
|
36
|
+
"@internal/storage-test-utils": "0.0.9",
|
|
36
37
|
"@internal/lint": "0.0.13",
|
|
37
|
-
"@mastra/core": "0.10.
|
|
38
|
-
"@internal/storage-test-utils": "0.0.9"
|
|
38
|
+
"@mastra/core": "0.10.7-alpha.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@mastra/core": ">=0.10.4-0 <0.11.0"
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
resetRole,
|
|
10
10
|
checkWorkflowSnapshot,
|
|
11
11
|
} from '@internal/storage-test-utils';
|
|
12
|
-
import type {
|
|
12
|
+
import type { MastraMessageV2 } from '@mastra/core/agent';
|
|
13
|
+
import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
|
|
13
14
|
import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
|
|
14
15
|
import {
|
|
15
16
|
TABLE_WORKFLOW_SNAPSHOT,
|
|
@@ -229,7 +230,9 @@ describe('PostgresStore', () => {
|
|
|
229
230
|
|
|
230
231
|
const messageContent = ['First', 'Second', 'Third'];
|
|
231
232
|
|
|
232
|
-
const messages = messageContent.map(content =>
|
|
233
|
+
const messages = messageContent.map(content =>
|
|
234
|
+
createSampleMessageV2({ threadId: thread.id, content: { content, parts: [{ type: 'text', text: content }] } }),
|
|
235
|
+
);
|
|
233
236
|
|
|
234
237
|
await store.saveMessages({ messages, format: 'v2' });
|
|
235
238
|
|
|
@@ -269,16 +272,48 @@ describe('PostgresStore', () => {
|
|
|
269
272
|
await store.saveThread({ thread: thread3 });
|
|
270
273
|
|
|
271
274
|
const messages: MastraMessageV2[] = [
|
|
272
|
-
createSampleMessageV2({
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
createSampleMessageV2({
|
|
276
|
+
threadId: 'thread-one',
|
|
277
|
+
content: { content: 'First' },
|
|
278
|
+
resourceId: 'cross-thread-resource',
|
|
279
|
+
}),
|
|
280
|
+
createSampleMessageV2({
|
|
281
|
+
threadId: 'thread-one',
|
|
282
|
+
content: { content: 'Second' },
|
|
283
|
+
resourceId: 'cross-thread-resource',
|
|
284
|
+
}),
|
|
285
|
+
createSampleMessageV2({
|
|
286
|
+
threadId: 'thread-one',
|
|
287
|
+
content: { content: 'Third' },
|
|
288
|
+
resourceId: 'cross-thread-resource',
|
|
289
|
+
}),
|
|
275
290
|
|
|
276
|
-
createSampleMessageV2({
|
|
277
|
-
|
|
278
|
-
|
|
291
|
+
createSampleMessageV2({
|
|
292
|
+
threadId: 'thread-two',
|
|
293
|
+
content: { content: 'Fourth' },
|
|
294
|
+
resourceId: 'cross-thread-resource',
|
|
295
|
+
}),
|
|
296
|
+
createSampleMessageV2({
|
|
297
|
+
threadId: 'thread-two',
|
|
298
|
+
content: { content: 'Fifth' },
|
|
299
|
+
resourceId: 'cross-thread-resource',
|
|
300
|
+
}),
|
|
301
|
+
createSampleMessageV2({
|
|
302
|
+
threadId: 'thread-two',
|
|
303
|
+
content: { content: 'Sixth' },
|
|
304
|
+
resourceId: 'cross-thread-resource',
|
|
305
|
+
}),
|
|
279
306
|
|
|
280
|
-
createSampleMessageV2({
|
|
281
|
-
|
|
307
|
+
createSampleMessageV2({
|
|
308
|
+
threadId: 'thread-three',
|
|
309
|
+
content: { content: 'Seventh' },
|
|
310
|
+
resourceId: 'other-resource',
|
|
311
|
+
}),
|
|
312
|
+
createSampleMessageV2({
|
|
313
|
+
threadId: 'thread-three',
|
|
314
|
+
content: { content: 'Eighth' },
|
|
315
|
+
resourceId: 'other-resource',
|
|
316
|
+
}),
|
|
282
317
|
];
|
|
283
318
|
|
|
284
319
|
await store.saveMessages({ messages: messages, format: 'v2' });
|
|
@@ -361,6 +396,201 @@ describe('PostgresStore', () => {
|
|
|
361
396
|
expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
|
|
362
397
|
expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
|
|
363
398
|
});
|
|
399
|
+
|
|
400
|
+
it('should return messages using both last and include (cross-thread, deduped)', async () => {
|
|
401
|
+
const thread = createSampleThread({ id: 'thread-one' });
|
|
402
|
+
await store.saveThread({ thread });
|
|
403
|
+
|
|
404
|
+
const thread2 = createSampleThread({ id: 'thread-two' });
|
|
405
|
+
await store.saveThread({ thread: thread2 });
|
|
406
|
+
|
|
407
|
+
const now = new Date();
|
|
408
|
+
|
|
409
|
+
// Setup: create messages in two threads
|
|
410
|
+
const messages = [
|
|
411
|
+
createSampleMessageV2({
|
|
412
|
+
threadId: 'thread-one',
|
|
413
|
+
content: { content: 'A' },
|
|
414
|
+
createdAt: new Date(now.getTime()),
|
|
415
|
+
}),
|
|
416
|
+
createSampleMessageV2({
|
|
417
|
+
threadId: 'thread-one',
|
|
418
|
+
content: { content: 'B' },
|
|
419
|
+
createdAt: new Date(now.getTime() + 1000),
|
|
420
|
+
}),
|
|
421
|
+
createSampleMessageV2({
|
|
422
|
+
threadId: 'thread-one',
|
|
423
|
+
content: { content: 'C' },
|
|
424
|
+
createdAt: new Date(now.getTime() + 2000),
|
|
425
|
+
}),
|
|
426
|
+
createSampleMessageV2({
|
|
427
|
+
threadId: 'thread-two',
|
|
428
|
+
content: { content: 'D' },
|
|
429
|
+
createdAt: new Date(now.getTime() + 3000),
|
|
430
|
+
}),
|
|
431
|
+
createSampleMessageV2({
|
|
432
|
+
threadId: 'thread-two',
|
|
433
|
+
content: { content: 'E' },
|
|
434
|
+
createdAt: new Date(now.getTime() + 4000),
|
|
435
|
+
}),
|
|
436
|
+
createSampleMessageV2({
|
|
437
|
+
threadId: 'thread-two',
|
|
438
|
+
content: { content: 'F' },
|
|
439
|
+
createdAt: new Date(now.getTime() + 5000),
|
|
440
|
+
}),
|
|
441
|
+
];
|
|
442
|
+
await store.saveMessages({ messages, format: 'v2' });
|
|
443
|
+
|
|
444
|
+
// Use last: 2 and include a message from another thread with context
|
|
445
|
+
const result = await store.getMessages({
|
|
446
|
+
threadId: 'thread-one',
|
|
447
|
+
format: 'v2',
|
|
448
|
+
selectBy: {
|
|
449
|
+
last: 2,
|
|
450
|
+
include: [
|
|
451
|
+
{
|
|
452
|
+
id: messages[4].id, // 'E' from thread-bar
|
|
453
|
+
threadId: 'thread-two',
|
|
454
|
+
withPreviousMessages: 1,
|
|
455
|
+
withNextMessages: 1,
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Should include last 2 from thread-one and 3 from thread-two (D, E, F)
|
|
462
|
+
expect(result.map(m => m.content.content).sort()).toEqual(['B', 'C', 'D', 'E', 'F']);
|
|
463
|
+
// Should include 2 from thread-one
|
|
464
|
+
expect(result.filter(m => m.threadId === 'thread-one').map(m => m.content.content)).toEqual(['B', 'C']);
|
|
465
|
+
// Should include 3 from thread-two
|
|
466
|
+
expect(result.filter(m => m.threadId === 'thread-two').map(m => m.content.content)).toEqual(['D', 'E', 'F']);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe('updateMessages', () => {
|
|
471
|
+
let thread: StorageThreadType;
|
|
472
|
+
|
|
473
|
+
beforeEach(async () => {
|
|
474
|
+
const threadData = createSampleThread();
|
|
475
|
+
thread = await store.saveThread({ thread: threadData as StorageThreadType });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should update a single field of a message (e.g., role)', async () => {
|
|
479
|
+
const originalMessage = createSampleMessageV2({ threadId: thread.id, role: 'user', thread });
|
|
480
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
481
|
+
|
|
482
|
+
const updatedMessages = await store.updateMessages({
|
|
483
|
+
messages: [{ id: originalMessage.id, role: 'assistant' }],
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(updatedMessages).toHaveLength(1);
|
|
487
|
+
expect(updatedMessages[0].role).toBe('assistant');
|
|
488
|
+
expect(updatedMessages[0].content).toEqual(originalMessage.content); // Ensure content is unchanged
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should update only the metadata within the content field, preserving other content', async () => {
|
|
492
|
+
const originalMessage = createSampleMessageV2({
|
|
493
|
+
threadId: thread.id,
|
|
494
|
+
content: { content: 'hello world', parts: [{ type: 'text', text: 'hello world' }] },
|
|
495
|
+
thread,
|
|
496
|
+
});
|
|
497
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
498
|
+
|
|
499
|
+
const newMetadata = { someKey: 'someValue' };
|
|
500
|
+
await store.updateMessages({
|
|
501
|
+
messages: [{ id: originalMessage.id, content: { metadata: newMetadata } as any }],
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
505
|
+
expect(fromDb[0].content.metadata).toEqual(newMetadata);
|
|
506
|
+
expect(fromDb[0].content.content).toBe('hello world');
|
|
507
|
+
expect(fromDb[0].content.parts).toEqual([{ type: 'text', text: 'hello world' }]);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should deep merge metadata, not overwrite it', async () => {
|
|
511
|
+
const originalMessage = createSampleMessageV2({
|
|
512
|
+
threadId: thread.id,
|
|
513
|
+
content: { metadata: { initial: true }, content: 'old content' },
|
|
514
|
+
thread,
|
|
515
|
+
});
|
|
516
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
517
|
+
|
|
518
|
+
const newMetadata = { updated: true };
|
|
519
|
+
await store.updateMessages({
|
|
520
|
+
messages: [{ id: originalMessage.id, content: { metadata: newMetadata } as any }],
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
524
|
+
expect(fromDb[0].content.metadata).toEqual({ initial: true, updated: true });
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should update multiple messages at once', async () => {
|
|
528
|
+
const msg1 = createSampleMessageV2({ threadId: thread.id, role: 'user', thread });
|
|
529
|
+
const msg2 = createSampleMessageV2({ threadId: thread.id, content: { content: 'original' }, thread });
|
|
530
|
+
await store.saveMessages({ messages: [msg1, msg2], format: 'v2' });
|
|
531
|
+
|
|
532
|
+
await store.updateMessages({
|
|
533
|
+
messages: [
|
|
534
|
+
{ id: msg1.id, role: 'assistant' },
|
|
535
|
+
{ id: msg2.id, content: { content: 'updated' } as any },
|
|
536
|
+
],
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const fromDb = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
540
|
+
const updatedMsg1 = fromDb.find(m => m.id === msg1.id)!;
|
|
541
|
+
const updatedMsg2 = fromDb.find(m => m.id === msg2.id)!;
|
|
542
|
+
|
|
543
|
+
expect(updatedMsg1.role).toBe('assistant');
|
|
544
|
+
expect(updatedMsg2.content.content).toBe('updated');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should update the parent thread updatedAt timestamp', async () => {
|
|
548
|
+
const originalMessage = createSampleMessageV2({ threadId: thread.id, thread });
|
|
549
|
+
await store.saveMessages({ messages: [originalMessage], format: 'v2' });
|
|
550
|
+
const initialThread = await store.getThreadById({ threadId: thread.id });
|
|
551
|
+
|
|
552
|
+
await new Promise(r => setTimeout(r, 10));
|
|
553
|
+
|
|
554
|
+
await store.updateMessages({ messages: [{ id: originalMessage.id, role: 'assistant' }] });
|
|
555
|
+
|
|
556
|
+
const updatedThread = await store.getThreadById({ threadId: thread.id });
|
|
557
|
+
|
|
558
|
+
expect(new Date(updatedThread!.updatedAt).getTime()).toBeGreaterThan(
|
|
559
|
+
new Date(initialThread!.updatedAt).getTime(),
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should update timestamps on both threads when moving a message', async () => {
|
|
564
|
+
const thread2 = await store.saveThread({ thread: createSampleThread() });
|
|
565
|
+
const message = createSampleMessageV2({ threadId: thread.id, thread });
|
|
566
|
+
await store.saveMessages({ messages: [message], format: 'v2' });
|
|
567
|
+
|
|
568
|
+
const initialThread1 = await store.getThreadById({ threadId: thread.id });
|
|
569
|
+
const initialThread2 = await store.getThreadById({ threadId: thread2.id });
|
|
570
|
+
|
|
571
|
+
await new Promise(r => setTimeout(r, 10));
|
|
572
|
+
|
|
573
|
+
await store.updateMessages({
|
|
574
|
+
messages: [{ id: message.id, threadId: thread2.id }],
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const updatedThread1 = await store.getThreadById({ threadId: thread.id });
|
|
578
|
+
const updatedThread2 = await store.getThreadById({ threadId: thread2.id });
|
|
579
|
+
|
|
580
|
+
expect(new Date(updatedThread1!.updatedAt).getTime()).toBeGreaterThan(
|
|
581
|
+
new Date(initialThread1!.updatedAt).getTime(),
|
|
582
|
+
);
|
|
583
|
+
expect(new Date(updatedThread2!.updatedAt).getTime()).toBeGreaterThan(
|
|
584
|
+
new Date(initialThread2!.updatedAt).getTime(),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
// Verify the message was moved
|
|
588
|
+
const thread1Messages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
589
|
+
const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
|
|
590
|
+
expect(thread1Messages).toHaveLength(0);
|
|
591
|
+
expect(thread2Messages).toHaveLength(1);
|
|
592
|
+
expect(thread2Messages[0].id).toBe(message.id);
|
|
593
|
+
});
|
|
364
594
|
});
|
|
365
595
|
|
|
366
596
|
describe('Edge Cases and Error Handling', () => {
|
|
@@ -1539,7 +1769,7 @@ describe('PostgresStore', () => {
|
|
|
1539
1769
|
record: { id: '1', name: 'Alice' },
|
|
1540
1770
|
});
|
|
1541
1771
|
|
|
1542
|
-
const row = await store.load({
|
|
1772
|
+
const row: any = await store.load({
|
|
1543
1773
|
tableName: camelCaseTable as TABLE_NAMES,
|
|
1544
1774
|
keys: { id: '1' },
|
|
1545
1775
|
});
|
|
@@ -1559,7 +1789,7 @@ describe('PostgresStore', () => {
|
|
|
1559
1789
|
record: { id: '2', name: 'Bob' },
|
|
1560
1790
|
});
|
|
1561
1791
|
|
|
1562
|
-
const row = await store.load({
|
|
1792
|
+
const row: any = await store.load({
|
|
1563
1793
|
tableName: snakeCaseTable as TABLE_NAMES,
|
|
1564
1794
|
keys: { id: '2' },
|
|
1565
1795
|
});
|