@mastra/mongodb 1.0.0-beta.13 → 1.0.0-beta.14
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/CHANGELOG.md +58 -0
- package/dist/docs/README.md +1 -1
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/SOURCE_MAP.json +1 -1
- package/dist/index.cjs +235 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +237 -20
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/observability/index.d.ts +46 -0
- package/dist/storage/domains/observability/index.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/vector/index.d.ts.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# @mastra/mongodb
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixed duplicate spans migration issue across all storage backends. When upgrading from older versions, existing duplicate (traceId, spanId) combinations in the spans table could prevent the unique constraint from being created. The migration deduplicates spans before adding the constraint. ([#12073](https://github.com/mastra-ai/mastra/pull/12073))
|
|
8
|
+
|
|
9
|
+
**Deduplication rules (in priority order):**
|
|
10
|
+
1. Keep completed spans (those with `endedAt` set) over incomplete spans
|
|
11
|
+
2. Among spans with the same completion status, keep the one with the newest `updatedAt`
|
|
12
|
+
3. Use `createdAt` as the final tiebreaker
|
|
13
|
+
|
|
14
|
+
**What changed:**
|
|
15
|
+
- Added `migrateSpans()` method to observability stores for manual migration
|
|
16
|
+
- Added `checkSpansMigrationStatus()` method to check if migration is needed
|
|
17
|
+
- All stores use optimized single-query deduplication to avoid memory issues on large tables
|
|
18
|
+
|
|
19
|
+
**Usage example:**
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
const observability = await storage.getStore('observability');
|
|
23
|
+
const status = await observability.checkSpansMigrationStatus();
|
|
24
|
+
if (status.needsMigration) {
|
|
25
|
+
const result = await observability.migrateSpans();
|
|
26
|
+
console.log(`Migration complete: ${result.duplicatesRemoved} duplicates removed`);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Fixes #11840
|
|
31
|
+
|
|
32
|
+
- Renamed MastraStorage to MastraCompositeStore for better clarity. The old MastraStorage name remains available as a deprecated alias for backward compatibility, but will be removed in a future version. ([#12093](https://github.com/mastra-ai/mastra/pull/12093))
|
|
33
|
+
|
|
34
|
+
**Migration:**
|
|
35
|
+
|
|
36
|
+
Update your imports and usage:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Before
|
|
40
|
+
import { MastraStorage } from '@mastra/core/storage';
|
|
41
|
+
|
|
42
|
+
const storage = new MastraStorage({
|
|
43
|
+
id: 'composite',
|
|
44
|
+
domains: { ... }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// After
|
|
48
|
+
import { MastraCompositeStore } from '@mastra/core/storage';
|
|
49
|
+
|
|
50
|
+
const storage = new MastraCompositeStore({
|
|
51
|
+
id: 'composite',
|
|
52
|
+
domains: { ... }
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The new name better reflects that this is a composite storage implementation that routes different domains (workflows, traces, messages) to different underlying stores, avoiding confusion with the general "Mastra Storage" concept.
|
|
57
|
+
|
|
58
|
+
- Updated dependencies [[`026b848`](https://github.com/mastra-ai/mastra/commit/026b8483fbf5b6d977be8f7e6aac8d15c75558ac), [`ffa553a`](https://github.com/mastra-ai/mastra/commit/ffa553a3edc1bd17d73669fba66d6b6f4ac10897)]:
|
|
59
|
+
- @mastra/core@1.0.0-beta.26
|
|
60
|
+
|
|
3
61
|
## 1.0.0-beta.13
|
|
4
62
|
|
|
5
63
|
### Patch Changes
|
package/dist/docs/README.md
CHANGED
package/dist/docs/SKILL.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -13,7 +13,7 @@ var evals = require('@mastra/core/evals');
|
|
|
13
13
|
|
|
14
14
|
// package.json
|
|
15
15
|
var package_default = {
|
|
16
|
-
version: "1.0.0-beta.
|
|
16
|
+
version: "1.0.0-beta.14"};
|
|
17
17
|
var MongoDBFilterTranslator = class extends filter.BaseFilterTranslator {
|
|
18
18
|
getSupportedOperators() {
|
|
19
19
|
return {
|
|
@@ -280,6 +280,8 @@ var MongoDBVector = class extends vector.MastraVector {
|
|
|
280
280
|
throw new Error(`Index "${indexNameInternal}" did not become ready within timeout`);
|
|
281
281
|
}
|
|
282
282
|
async upsert({ indexName, vectors, metadata, ids, documents }) {
|
|
283
|
+
vector.validateUpsertInput("MONGODB", vectors, metadata, ids);
|
|
284
|
+
vector.validateVectorValues("MONGODB", vectors);
|
|
283
285
|
try {
|
|
284
286
|
const collection = await this.getCollection(indexName);
|
|
285
287
|
this.collectionForValidation = collection;
|
|
@@ -355,8 +357,8 @@ var MongoDBVector = class extends vector.MastraVector {
|
|
|
355
357
|
index: indexNameInternal,
|
|
356
358
|
queryVector,
|
|
357
359
|
path: this.embeddingFieldName,
|
|
358
|
-
numCandidates: 100,
|
|
359
|
-
limit: topK
|
|
360
|
+
numCandidates: Math.min(1e4, Math.max(100, topK)),
|
|
361
|
+
limit: Math.min(1e4, topK)
|
|
360
362
|
};
|
|
361
363
|
if (Object.keys(combinedFilter).length > 0) {
|
|
362
364
|
const filterWithExclusion = {
|
|
@@ -1950,9 +1952,221 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
|
|
|
1950
1952
|
}
|
|
1951
1953
|
}
|
|
1952
1954
|
async init() {
|
|
1955
|
+
const uniqueIndexExists = await this.spansUniqueIndexExists();
|
|
1956
|
+
if (!uniqueIndexExists) {
|
|
1957
|
+
const duplicateInfo = await this.checkForDuplicateSpans();
|
|
1958
|
+
if (duplicateInfo.hasDuplicates) {
|
|
1959
|
+
const errorMessage = `
|
|
1960
|
+
===========================================================================
|
|
1961
|
+
MIGRATION REQUIRED: Duplicate spans detected in ${storage.TABLE_SPANS} collection
|
|
1962
|
+
===========================================================================
|
|
1963
|
+
|
|
1964
|
+
Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
|
|
1965
|
+
|
|
1966
|
+
The spans collection requires a unique index on (traceId, spanId), but your
|
|
1967
|
+
database contains duplicate entries that must be resolved first.
|
|
1968
|
+
|
|
1969
|
+
To fix this, run the manual migration command:
|
|
1970
|
+
|
|
1971
|
+
npx mastra migrate
|
|
1972
|
+
|
|
1973
|
+
This command will:
|
|
1974
|
+
1. Remove duplicate spans (keeping the most complete/recent version)
|
|
1975
|
+
2. Add the required unique index
|
|
1976
|
+
|
|
1977
|
+
Note: This migration may take some time for large collections.
|
|
1978
|
+
===========================================================================
|
|
1979
|
+
`;
|
|
1980
|
+
throw new error.MastraError({
|
|
1981
|
+
id: storage.createStorageErrorId("MONGODB", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
|
|
1982
|
+
domain: error.ErrorDomain.STORAGE,
|
|
1983
|
+
category: error.ErrorCategory.USER,
|
|
1984
|
+
text: errorMessage
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1953
1988
|
await this.createDefaultIndexes();
|
|
1954
1989
|
await this.createCustomIndexes();
|
|
1955
1990
|
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Checks if the unique index on (spanId, traceId) already exists on the spans collection.
|
|
1993
|
+
* Used to skip deduplication when the index already exists (migration already complete).
|
|
1994
|
+
*/
|
|
1995
|
+
async spansUniqueIndexExists() {
|
|
1996
|
+
try {
|
|
1997
|
+
const collection = await this.getCollection(storage.TABLE_SPANS);
|
|
1998
|
+
const indexes = await collection.indexes();
|
|
1999
|
+
return indexes.some((idx) => idx.unique === true && idx.key?.spanId === 1 && idx.key?.traceId === 1);
|
|
2000
|
+
} catch {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Checks for duplicate (traceId, spanId) combinations in the spans collection.
|
|
2006
|
+
* Returns information about duplicates for logging/CLI purposes.
|
|
2007
|
+
*/
|
|
2008
|
+
async checkForDuplicateSpans() {
|
|
2009
|
+
try {
|
|
2010
|
+
const collection = await this.getCollection(storage.TABLE_SPANS);
|
|
2011
|
+
const result = await collection.aggregate([
|
|
2012
|
+
{
|
|
2013
|
+
$group: {
|
|
2014
|
+
_id: { traceId: "$traceId", spanId: "$spanId" },
|
|
2015
|
+
count: { $sum: 1 }
|
|
2016
|
+
}
|
|
2017
|
+
},
|
|
2018
|
+
{ $match: { count: { $gt: 1 } } },
|
|
2019
|
+
{ $count: "duplicateCount" }
|
|
2020
|
+
]).toArray();
|
|
2021
|
+
const duplicateCount = result[0]?.duplicateCount ?? 0;
|
|
2022
|
+
return {
|
|
2023
|
+
hasDuplicates: duplicateCount > 0,
|
|
2024
|
+
duplicateCount
|
|
2025
|
+
};
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
this.logger?.debug?.(`Could not check for duplicates: ${error}`);
|
|
2028
|
+
return { hasDuplicates: false, duplicateCount: 0 };
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Manually run the spans migration to deduplicate and add the unique index.
|
|
2033
|
+
* This is intended to be called from the CLI when duplicates are detected.
|
|
2034
|
+
*
|
|
2035
|
+
* @returns Migration result with status and details
|
|
2036
|
+
*/
|
|
2037
|
+
async migrateSpans() {
|
|
2038
|
+
const indexExists = await this.spansUniqueIndexExists();
|
|
2039
|
+
if (indexExists) {
|
|
2040
|
+
return {
|
|
2041
|
+
success: true,
|
|
2042
|
+
alreadyMigrated: true,
|
|
2043
|
+
duplicatesRemoved: 0,
|
|
2044
|
+
message: `Migration already complete. Unique index exists on ${storage.TABLE_SPANS} collection.`
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
const duplicateInfo = await this.checkForDuplicateSpans();
|
|
2048
|
+
if (duplicateInfo.hasDuplicates) {
|
|
2049
|
+
this.logger?.info?.(
|
|
2050
|
+
`Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
|
|
2051
|
+
);
|
|
2052
|
+
await this.deduplicateSpans();
|
|
2053
|
+
} else {
|
|
2054
|
+
this.logger?.info?.(`No duplicate spans found.`);
|
|
2055
|
+
}
|
|
2056
|
+
await this.createDefaultIndexes();
|
|
2057
|
+
await this.createCustomIndexes();
|
|
2058
|
+
return {
|
|
2059
|
+
success: true,
|
|
2060
|
+
alreadyMigrated: false,
|
|
2061
|
+
duplicatesRemoved: duplicateInfo.duplicateCount,
|
|
2062
|
+
message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added unique index to ${storage.TABLE_SPANS} collection.` : `Migration complete. Added unique index to ${storage.TABLE_SPANS} collection.`
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Check migration status for the spans collection.
|
|
2067
|
+
* Returns information about whether migration is needed.
|
|
2068
|
+
*/
|
|
2069
|
+
async checkSpansMigrationStatus() {
|
|
2070
|
+
const indexExists = await this.spansUniqueIndexExists();
|
|
2071
|
+
if (indexExists) {
|
|
2072
|
+
return {
|
|
2073
|
+
needsMigration: false,
|
|
2074
|
+
hasDuplicates: false,
|
|
2075
|
+
duplicateCount: 0,
|
|
2076
|
+
constraintExists: true,
|
|
2077
|
+
tableName: storage.TABLE_SPANS
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
const duplicateInfo = await this.checkForDuplicateSpans();
|
|
2081
|
+
return {
|
|
2082
|
+
needsMigration: true,
|
|
2083
|
+
hasDuplicates: duplicateInfo.hasDuplicates,
|
|
2084
|
+
duplicateCount: duplicateInfo.duplicateCount,
|
|
2085
|
+
constraintExists: false,
|
|
2086
|
+
tableName: storage.TABLE_SPANS
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Deduplicates spans with the same (traceId, spanId) combination.
|
|
2091
|
+
* This is needed for databases that existed before the unique constraint was added.
|
|
2092
|
+
*
|
|
2093
|
+
* Priority for keeping spans:
|
|
2094
|
+
* 1. Completed spans (endedAt IS NOT NULL) over incomplete spans
|
|
2095
|
+
* 2. Most recent updatedAt
|
|
2096
|
+
* 3. Most recent createdAt (as tiebreaker)
|
|
2097
|
+
*
|
|
2098
|
+
* Note: This prioritizes migration completion over perfect data preservation.
|
|
2099
|
+
* Old trace data may be lost, which is acceptable for this use case.
|
|
2100
|
+
*/
|
|
2101
|
+
async deduplicateSpans() {
|
|
2102
|
+
try {
|
|
2103
|
+
const collection = await this.getCollection(storage.TABLE_SPANS);
|
|
2104
|
+
const duplicateCheck = await collection.aggregate([
|
|
2105
|
+
{
|
|
2106
|
+
$group: {
|
|
2107
|
+
_id: { traceId: "$traceId", spanId: "$spanId" },
|
|
2108
|
+
count: { $sum: 1 }
|
|
2109
|
+
}
|
|
2110
|
+
},
|
|
2111
|
+
{ $match: { count: { $gt: 1 } } },
|
|
2112
|
+
{ $limit: 1 }
|
|
2113
|
+
]).toArray();
|
|
2114
|
+
if (duplicateCheck.length === 0) {
|
|
2115
|
+
this.logger?.debug?.("No duplicate spans found");
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
this.logger?.info?.("Duplicate spans detected, starting deduplication...");
|
|
2119
|
+
const idsToDelete = await collection.aggregate([
|
|
2120
|
+
// Sort by priority (affects which document $first picks within each group)
|
|
2121
|
+
{
|
|
2122
|
+
$sort: {
|
|
2123
|
+
// Completed spans first (endedAt exists and is not null)
|
|
2124
|
+
endedAt: -1,
|
|
2125
|
+
updatedAt: -1,
|
|
2126
|
+
createdAt: -1
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
2129
|
+
// Group by (traceId, spanId), keeping the first (best) _id and all _ids
|
|
2130
|
+
{
|
|
2131
|
+
$group: {
|
|
2132
|
+
_id: { traceId: "$traceId", spanId: "$spanId" },
|
|
2133
|
+
keepId: { $first: "$_id" },
|
|
2134
|
+
// The best one to keep (after sort)
|
|
2135
|
+
allIds: { $push: "$_id" },
|
|
2136
|
+
// All ObjectIds (just 12 bytes each, not full docs)
|
|
2137
|
+
count: { $sum: 1 }
|
|
2138
|
+
}
|
|
2139
|
+
},
|
|
2140
|
+
// Only consider groups with duplicates
|
|
2141
|
+
{ $match: { count: { $gt: 1 } } },
|
|
2142
|
+
// Get IDs to delete (allIds minus keepId)
|
|
2143
|
+
{
|
|
2144
|
+
$project: {
|
|
2145
|
+
idsToDelete: {
|
|
2146
|
+
$filter: {
|
|
2147
|
+
input: "$allIds",
|
|
2148
|
+
cond: { $ne: ["$$this", "$keepId"] }
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
},
|
|
2153
|
+
// Unwind to get flat list of IDs
|
|
2154
|
+
{ $unwind: "$idsToDelete" },
|
|
2155
|
+
// Just output the ID
|
|
2156
|
+
{ $project: { _id: "$idsToDelete" } }
|
|
2157
|
+
]).toArray();
|
|
2158
|
+
if (idsToDelete.length === 0) {
|
|
2159
|
+
this.logger?.debug?.("No duplicates to delete after aggregation");
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
const deleteResult = await collection.deleteMany({
|
|
2163
|
+
_id: { $in: idsToDelete.map((d) => d._id) }
|
|
2164
|
+
});
|
|
2165
|
+
this.logger?.info?.(`Deduplication complete: removed ${deleteResult.deletedCount} duplicate spans`);
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
this.logger?.warn?.("Failed to deduplicate spans:", error);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
1956
2170
|
async dangerouslyClearAll() {
|
|
1957
2171
|
const collection = await this.getCollection(storage.TABLE_SPANS);
|
|
1958
2172
|
await collection.deleteMany({});
|
|
@@ -2233,7 +2447,7 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
|
|
|
2233
2447
|
// No children with errors
|
|
2234
2448
|
}
|
|
2235
2449
|
];
|
|
2236
|
-
const countResult = await collection.aggregate([...pipeline, { $count: "total" }]).toArray();
|
|
2450
|
+
const countResult = await collection.aggregate([...pipeline, { $count: "total" }], { allowDiskUse: true }).toArray();
|
|
2237
2451
|
const count2 = countResult[0]?.total || 0;
|
|
2238
2452
|
if (count2 === 0) {
|
|
2239
2453
|
return {
|
|
@@ -2270,7 +2484,7 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
|
|
|
2270
2484
|
{ $project: { _errorSpans: 0 } }
|
|
2271
2485
|
];
|
|
2272
2486
|
}
|
|
2273
|
-
const spans2 = await collection.aggregate(aggregationPipeline).toArray();
|
|
2487
|
+
const spans2 = await collection.aggregate(aggregationPipeline, { allowDiskUse: true }).toArray();
|
|
2274
2488
|
return {
|
|
2275
2489
|
pagination: {
|
|
2276
2490
|
total: count2,
|
|
@@ -2296,18 +2510,21 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
|
|
|
2296
2510
|
let spans;
|
|
2297
2511
|
if (sortField === "endedAt") {
|
|
2298
2512
|
const nullSortValue = sortDirection === -1 ? 0 : 1;
|
|
2299
|
-
spans = await collection.aggregate(
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2513
|
+
spans = await collection.aggregate(
|
|
2514
|
+
[
|
|
2515
|
+
{ $match: mongoFilter },
|
|
2516
|
+
{
|
|
2517
|
+
$addFields: {
|
|
2518
|
+
_endedAtNull: { $cond: [{ $eq: ["$endedAt", null] }, nullSortValue, sortDirection === -1 ? 1 : 0] }
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
{ $sort: { _endedAtNull: 1, [sortField]: sortDirection } },
|
|
2522
|
+
{ $skip: page * perPage },
|
|
2523
|
+
{ $limit: perPage },
|
|
2524
|
+
{ $project: { _endedAtNull: 0 } }
|
|
2525
|
+
],
|
|
2526
|
+
{ allowDiskUse: true }
|
|
2527
|
+
).toArray();
|
|
2311
2528
|
} else {
|
|
2312
2529
|
spans = await collection.find(mongoFilter).sort({ [sortField]: sortDirection }).skip(page * perPage).limit(perPage).toArray();
|
|
2313
2530
|
}
|
|
@@ -3081,7 +3298,7 @@ var WorkflowsStorageMongoDB = class _WorkflowsStorageMongoDB extends storage.Wor
|
|
|
3081
3298
|
};
|
|
3082
3299
|
|
|
3083
3300
|
// src/storage/index.ts
|
|
3084
|
-
var MongoDBStore = class extends storage.
|
|
3301
|
+
var MongoDBStore = class extends storage.MastraCompositeStore {
|
|
3085
3302
|
#connector;
|
|
3086
3303
|
stores;
|
|
3087
3304
|
constructor(config) {
|