@atscript/db-mongo 0.1.106 → 0.1.108
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.cjs +180 -45
- package/dist/index.d.cts +31 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.mjs +180 -45
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -2,15 +2,29 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_mongo_filter = require("./mongo-filter-z_hLPMyv.cjs");
|
|
3
3
|
let _atscript_db = require("@atscript/db");
|
|
4
4
|
let mongodb = require("mongodb");
|
|
5
|
+
//#region src/lib/path-utils.ts
|
|
6
|
+
function joinPath(prefix, segment) {
|
|
7
|
+
return prefix ? `${prefix}.${segment}` : segment;
|
|
8
|
+
}
|
|
9
|
+
function isArrayPath(flatMap, path) {
|
|
10
|
+
const type = flatMap.get(path);
|
|
11
|
+
return type !== void 0 && (0, _atscript_db.resolveDesignType)(type) === "array";
|
|
12
|
+
}
|
|
13
|
+
function hasAncestorIn(path, set) {
|
|
14
|
+
let dot = path.indexOf(".");
|
|
15
|
+
while (dot !== -1) {
|
|
16
|
+
if (set.has(path.slice(0, dot))) return true;
|
|
17
|
+
dot = path.indexOf(".", dot + 1);
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
5
22
|
//#region src/lib/projection-dedupe.ts
|
|
6
23
|
function dedupeProjection(projection) {
|
|
7
24
|
const includeKeys = Object.keys(projection).filter((k) => projection[k] === 1);
|
|
8
25
|
if (includeKeys.length < 2) return projection;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const prefix = parent + ".";
|
|
12
|
-
for (const other of includeKeys) if (other !== parent && other.startsWith(prefix)) toRemove.add(other);
|
|
13
|
-
}
|
|
26
|
+
const includes = new Set(includeKeys);
|
|
27
|
+
const toRemove = new Set(includeKeys.filter((k) => hasAncestorIn(k, includes)));
|
|
14
28
|
if (toRemove.size === 0) return projection;
|
|
15
29
|
const result = {};
|
|
16
30
|
for (const k of Object.keys(projection)) if (!toRemove.has(k)) result[k] = projection[k];
|
|
@@ -150,7 +164,7 @@ var CollectionPatcher = class {
|
|
|
150
164
|
* @private
|
|
151
165
|
*/
|
|
152
166
|
flattenPayload(payload, prefix = "") {
|
|
153
|
-
const evalKey = (k) => prefix
|
|
167
|
+
const evalKey = (k) => joinPath(prefix, k);
|
|
154
168
|
for (const [_key, value] of Object.entries(payload)) {
|
|
155
169
|
const key = evalKey(_key);
|
|
156
170
|
const flatType = this.collection.flatMap.get(key);
|
|
@@ -763,27 +777,44 @@ function buildSearchStage(host, text, indexName, controls) {
|
|
|
763
777
|
const searchIndex = index;
|
|
764
778
|
const fuzzy = resolveSearchFuzzy(searchIndex, controls);
|
|
765
779
|
const strategy = searchIndex.strategy ?? "compound";
|
|
766
|
-
const
|
|
767
|
-
const
|
|
780
|
+
const paths = collectSearchPathsCached(searchIndex);
|
|
781
|
+
const fuzzyOpt = fuzzy ? { fuzzy } : {};
|
|
782
|
+
const textOp = (path) => ({ text: {
|
|
768
783
|
query: text,
|
|
769
|
-
path
|
|
770
|
-
...
|
|
784
|
+
path,
|
|
785
|
+
...fuzzyOpt
|
|
771
786
|
} });
|
|
772
|
-
const
|
|
787
|
+
const autocompleteOp = (path) => ({ autocomplete: {
|
|
773
788
|
query: text,
|
|
774
789
|
path,
|
|
775
|
-
...
|
|
790
|
+
...fuzzyOpt
|
|
776
791
|
} });
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
minimumShouldMatch: 1
|
|
792
|
+
const textClause = () => textOp({ wildcard: "*" });
|
|
793
|
+
const wrapEmbedded = (chain, operator) => {
|
|
794
|
+
let wrapped = operator;
|
|
795
|
+
for (let i = chain.length - 1; i >= 0; i--) wrapped = { embeddedDocument: {
|
|
796
|
+
path: chain[i],
|
|
797
|
+
operator: wrapped
|
|
784
798
|
} };
|
|
785
|
-
|
|
786
|
-
|
|
799
|
+
return wrapped;
|
|
800
|
+
};
|
|
801
|
+
const embeddedTextClause = (chain, leaves) => wrapEmbedded(chain, textOp(leaves.length === 1 ? leaves[0] : leaves));
|
|
802
|
+
const embeddedAutocompleteClause = (chain, leaf) => wrapEmbedded(chain, autocompleteOp(leaf));
|
|
803
|
+
const wantWord = strategy === "text" || strategy === "compound";
|
|
804
|
+
const wantAutocomplete = strategy === "autocomplete" || strategy === "compound";
|
|
805
|
+
const clauses = [];
|
|
806
|
+
if (wantWord) {
|
|
807
|
+
clauses.push(textClause());
|
|
808
|
+
for (const group of paths.arrayGroups) if (group.textLeaves.length > 0) clauses.push(embeddedTextClause(group.chain, group.textLeaves));
|
|
809
|
+
}
|
|
810
|
+
if (wantAutocomplete) {
|
|
811
|
+
for (const path of paths.autocompleteOutside) clauses.push(autocompleteOp(path));
|
|
812
|
+
for (const group of paths.arrayGroups) for (const leaf of group.autocompleteLeaves) clauses.push(embeddedAutocompleteClause(group.chain, leaf));
|
|
813
|
+
}
|
|
814
|
+
let body;
|
|
815
|
+
if (clauses.length <= 1) body = clauses[0] ?? textClause();
|
|
816
|
+
else body = { compound: {
|
|
817
|
+
should: clauses,
|
|
787
818
|
minimumShouldMatch: 1
|
|
788
819
|
} };
|
|
789
820
|
return {
|
|
@@ -804,13 +835,65 @@ function resolveSearchFuzzy(index, controls) {
|
|
|
804
835
|
const maxEdits = override === void 0 ? index.fuzzy?.maxEdits : Number(override);
|
|
805
836
|
return maxEdits === 1 || maxEdits === 2 ? { maxEdits } : void 0;
|
|
806
837
|
}
|
|
807
|
-
/**
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
838
|
+
/**
|
|
839
|
+
* An index's mapping tree is immutable once schema sync builds it, and the index
|
|
840
|
+
* object is cached for the adapter's lifetime — so the walk runs once per index,
|
|
841
|
+
* not once per query (`buildSearchStage` is on the search hot path).
|
|
842
|
+
*/
|
|
843
|
+
const searchPathsCache = /* @__PURE__ */ new WeakMap();
|
|
844
|
+
function collectSearchPathsCached(index) {
|
|
845
|
+
let cached = searchPathsCache.get(index);
|
|
846
|
+
if (!cached) {
|
|
847
|
+
cached = collectSearchPaths(index.definition.mappings?.fields);
|
|
848
|
+
searchPathsCache.set(index, cached);
|
|
849
|
+
}
|
|
850
|
+
return cached;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Walks an index's nested mapping tree, classifying each searchable leaf by how
|
|
854
|
+
* it must be queried: a plain dotted path (top-level / `document`-nested) vs.
|
|
855
|
+
* scoped under an `embeddedDocuments` array root (queried via `embeddedDocument`).
|
|
856
|
+
* Word-matching for non-array fields rides the wildcard `text` clause, so only
|
|
857
|
+
* autocomplete leaves are tracked outside arrays.
|
|
858
|
+
*/
|
|
859
|
+
function collectSearchPaths(fields) {
|
|
860
|
+
const out = {
|
|
861
|
+
autocompleteOutside: [],
|
|
862
|
+
arrayGroups: []
|
|
863
|
+
};
|
|
864
|
+
const byChain = /* @__PURE__ */ new Map();
|
|
865
|
+
const walk = (map, prefix, chain) => {
|
|
866
|
+
if (!map) return;
|
|
867
|
+
for (const [key, mapping] of Object.entries(map)) {
|
|
868
|
+
const path = joinPath(prefix, key);
|
|
869
|
+
const list = Array.isArray(mapping) ? mapping : [mapping];
|
|
870
|
+
const embedded = list.find((m) => m.type === "embeddedDocuments");
|
|
871
|
+
const document = list.find((m) => m.type === "document");
|
|
872
|
+
if (embedded?.fields) walk(embedded.fields, path, [...chain, path]);
|
|
873
|
+
else if (document?.fields) walk(document.fields, path, chain);
|
|
874
|
+
else {
|
|
875
|
+
const isAutocomplete = list.some((m) => m.type === "autocomplete");
|
|
876
|
+
const isText = list.some((m) => m.type === "string");
|
|
877
|
+
if (chain.length > 0) {
|
|
878
|
+
const chainKey = chain.join(">");
|
|
879
|
+
let group = byChain.get(chainKey);
|
|
880
|
+
if (!group) {
|
|
881
|
+
group = {
|
|
882
|
+
chain,
|
|
883
|
+
textLeaves: [],
|
|
884
|
+
autocompleteLeaves: []
|
|
885
|
+
};
|
|
886
|
+
byChain.set(chainKey, group);
|
|
887
|
+
out.arrayGroups.push(group);
|
|
888
|
+
}
|
|
889
|
+
if (isText) group.textLeaves.push(path);
|
|
890
|
+
if (isAutocomplete) group.autocompleteLeaves.push(path);
|
|
891
|
+
} else if (isAutocomplete) out.autocompleteOutside.push(path);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
walk(fields, "", []);
|
|
896
|
+
return out;
|
|
814
897
|
}
|
|
815
898
|
/** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
|
|
816
899
|
function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
@@ -1144,8 +1227,10 @@ async function syncColumnsImpl(host, diff) {
|
|
|
1144
1227
|
}
|
|
1145
1228
|
async function dropColumnsImpl(host, columns) {
|
|
1146
1229
|
if (columns.length === 0) return;
|
|
1230
|
+
const dropped = new Set(columns);
|
|
1231
|
+
const minimal = columns.filter((col) => !hasAncestorIn(col, dropped));
|
|
1147
1232
|
const unsetSpec = {};
|
|
1148
|
-
for (const col of
|
|
1233
|
+
for (const col of minimal) unsetSpec[arraySafePath(host, col, col)] = "";
|
|
1149
1234
|
await host.collection.updateMany({}, { $unset: unsetSpec }, host._getSessionOpts());
|
|
1150
1235
|
}
|
|
1151
1236
|
/**
|
|
@@ -1167,11 +1252,8 @@ function arraySafePath(host, physicalPath, logicalPath) {
|
|
|
1167
1252
|
for (let i = 0; i < logicalSegments.length; i++) {
|
|
1168
1253
|
const isLeaf = i === logicalSegments.length - 1;
|
|
1169
1254
|
out.push(isLeaf ? physicalLeaf : logicalSegments[i]);
|
|
1170
|
-
prefix = prefix
|
|
1171
|
-
if (!isLeaf)
|
|
1172
|
-
const type = host._table.flatMap.get(prefix);
|
|
1173
|
-
if (type && (0, _atscript_db.resolveDesignType)(type) === "array") out.push("$[]");
|
|
1174
|
-
}
|
|
1255
|
+
prefix = joinPath(prefix, logicalSegments[i]);
|
|
1256
|
+
if (!isLeaf && isArrayPath(host._table.flatMap, prefix)) out.push("$[]");
|
|
1175
1257
|
}
|
|
1176
1258
|
return out.join(".");
|
|
1177
1259
|
}
|
|
@@ -1181,9 +1263,8 @@ function pathCrossesArray(host, logicalPath) {
|
|
|
1181
1263
|
if (segments.length < 2) return false;
|
|
1182
1264
|
let prefix = "";
|
|
1183
1265
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
1184
|
-
prefix = prefix
|
|
1185
|
-
|
|
1186
|
-
if (type && (0, _atscript_db.resolveDesignType)(type) === "array") return true;
|
|
1266
|
+
prefix = joinPath(prefix, segments[i]);
|
|
1267
|
+
if (isArrayPath(host._table.flatMap, prefix)) return true;
|
|
1187
1268
|
}
|
|
1188
1269
|
return false;
|
|
1189
1270
|
}
|
|
@@ -1431,6 +1512,7 @@ function fieldMappingEqual(a, b) {
|
|
|
1431
1512
|
for (const [type, av] of am) {
|
|
1432
1513
|
const bv = bm.get(type);
|
|
1433
1514
|
if (!bv || av.analyzer !== bv.analyzer || av.tokenization !== bv.tokenization || av.minGrams !== bv.minGrams || av.maxGrams !== bv.maxGrams || av.foldDiacritics !== bv.foldDiacritics) return false;
|
|
1515
|
+
if (!fieldsMatch(av.fields, bv.fields)) return false;
|
|
1434
1516
|
}
|
|
1435
1517
|
return true;
|
|
1436
1518
|
}
|
|
@@ -1475,6 +1557,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1475
1557
|
_vectorThresholds = /* @__PURE__ */ new Map();
|
|
1476
1558
|
/** Cached search index lookup. */
|
|
1477
1559
|
_searchIndexesMap;
|
|
1560
|
+
/**
|
|
1561
|
+
* Search-field mappings recorded during `onFieldScanned`, applied in
|
|
1562
|
+
* `onAfterFlatten`. Deferred because resolving a nested field's container
|
|
1563
|
+
* shape (`document` vs `embeddedDocuments`) needs `this._table.flatMap`, which
|
|
1564
|
+
* is only safe to read once the metadata build is marked complete — i.e. in
|
|
1565
|
+
* `onAfterFlatten`, not mid-scan (the `flatMap` getter would re-enter `build`).
|
|
1566
|
+
*/
|
|
1567
|
+
_pendingSearchFields = [];
|
|
1478
1568
|
/** Physical field names with @db.default.increment → optional start value. */
|
|
1479
1569
|
_incrementFields = /* @__PURE__ */ new Map();
|
|
1480
1570
|
/** Physical field names that have a non-binary collation (nocase/unicode). */
|
|
@@ -1699,10 +1789,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1699
1789
|
const startValue = metadata.get("db.default.increment");
|
|
1700
1790
|
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
|
|
1701
1791
|
}
|
|
1702
|
-
for (const index of metadata.get("db.mongo.search.text") || []) this.
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1792
|
+
for (const index of metadata.get("db.mongo.search.text") || []) this._pendingSearchFields.push({
|
|
1793
|
+
indexName: index.indexName,
|
|
1794
|
+
field,
|
|
1795
|
+
mappings: [index.analyzer ? {
|
|
1796
|
+
type: "string",
|
|
1797
|
+
analyzer: index.analyzer
|
|
1798
|
+
} : { type: "string" }]
|
|
1799
|
+
});
|
|
1706
1800
|
for (const ac of metadata.get("db.mongo.search.autocomplete") || []) {
|
|
1707
1801
|
const autocomplete = {
|
|
1708
1802
|
type: "autocomplete",
|
|
@@ -1715,7 +1809,11 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1715
1809
|
type: "string",
|
|
1716
1810
|
analyzer: ac.analyzer
|
|
1717
1811
|
} : { type: "string" };
|
|
1718
|
-
this.
|
|
1812
|
+
this._pendingSearchFields.push({
|
|
1813
|
+
indexName: ac.indexName,
|
|
1814
|
+
field,
|
|
1815
|
+
mappings: [autocomplete, companion]
|
|
1816
|
+
});
|
|
1719
1817
|
}
|
|
1720
1818
|
const vectorIndex = metadata.get("db.search.vector");
|
|
1721
1819
|
if (vectorIndex) {
|
|
@@ -1757,6 +1855,8 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1757
1855
|
};
|
|
1758
1856
|
}
|
|
1759
1857
|
onAfterFlatten() {
|
|
1858
|
+
for (const pending of this._pendingSearchFields) this._addFieldToSearchIndex("search_text", pending.indexName, pending.field, pending.mappings);
|
|
1859
|
+
this._pendingSearchFields = [];
|
|
1760
1860
|
for (const [key, value] of this._vectorFilters.entries()) {
|
|
1761
1861
|
const index = this._mongoIndexes.get(key);
|
|
1762
1862
|
if (index && index.type === "vector") index.definition.fields?.push({
|
|
@@ -2210,10 +2310,45 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2210
2310
|
const name = _name || "DEFAULT";
|
|
2211
2311
|
let index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2212
2312
|
if (!index && type === "search_text") index = this._setSearchIndex(type, name, { mappings: { fields: {} } });
|
|
2213
|
-
if (index)
|
|
2214
|
-
|
|
2215
|
-
|
|
2313
|
+
if (!index) return;
|
|
2314
|
+
const rootFields = index.definition.mappings.fields;
|
|
2315
|
+
const segments = fieldName.split(".");
|
|
2316
|
+
const topKey = this._table.columnMap.get(segments[0]) ?? segments[0];
|
|
2317
|
+
if (segments.length === 1) {
|
|
2318
|
+
rootFields[topKey] = mergeFieldMappings(rootFields[topKey], mappings);
|
|
2319
|
+
return;
|
|
2216
2320
|
}
|
|
2321
|
+
let cursor = rootFields;
|
|
2322
|
+
let prefix = "";
|
|
2323
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
2324
|
+
const segment = segments[i];
|
|
2325
|
+
prefix = joinPath(prefix, segment);
|
|
2326
|
+
const nodeType = isArrayPath(this._table.flatMap, prefix) ? "embeddedDocuments" : "document";
|
|
2327
|
+
cursor = this._descendSearchNode(cursor, i === 0 ? topKey : segment, nodeType, fieldName);
|
|
2328
|
+
}
|
|
2329
|
+
const leaf = segments[segments.length - 1];
|
|
2330
|
+
cursor[leaf] = mergeFieldMappings(cursor[leaf], mappings);
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Returns (creating if needed) the nested `fields` map of the container node
|
|
2334
|
+
* at `segment`, reusing a same-typed sibling node so multiple searchable
|
|
2335
|
+
* fields under one parent (e.g. `identity.name` + `identity.tagline`) merge
|
|
2336
|
+
* instead of clobbering.
|
|
2337
|
+
*/
|
|
2338
|
+
_descendSearchNode(fields, segment, nodeType, fullPath) {
|
|
2339
|
+
const existing = fields[segment];
|
|
2340
|
+
if (existing && !Array.isArray(existing) && (existing.type === "document" || existing.type === "embeddedDocuments")) {
|
|
2341
|
+
if (existing.type !== nodeType) this._log(`search index: conflicting container type for "${fullPath}" (${existing.type} vs ${nodeType}); keeping ${existing.type}`);
|
|
2342
|
+
existing.fields ??= {};
|
|
2343
|
+
return existing.fields;
|
|
2344
|
+
}
|
|
2345
|
+
if (existing) this._log(`search index: field "${segment}" used as both value and parent in "${fullPath}"`);
|
|
2346
|
+
const node = {
|
|
2347
|
+
type: nodeType,
|
|
2348
|
+
fields: {}
|
|
2349
|
+
};
|
|
2350
|
+
fields[segment] = node;
|
|
2351
|
+
return node.fields;
|
|
2217
2352
|
}
|
|
2218
2353
|
};
|
|
2219
2354
|
/**
|
package/dist/index.d.cts
CHANGED
|
@@ -191,6 +191,16 @@ type TVectorSimilarity = "cosine" | "euclidean" | "dotProduct";
|
|
|
191
191
|
* `type: "autocomplete"` enables prefix/typeahead (edgeGram) or substring (nGram).
|
|
192
192
|
* A field may carry several mappings at once (an array of these) — e.g. an
|
|
193
193
|
* autocomplete field double-mapped as `string` so exact-word hits still rank.
|
|
194
|
+
*
|
|
195
|
+
* Container nodes carry `fields` instead of leaf attributes:
|
|
196
|
+
* - `type: "document"` — a single embedded object (`identity: Identity`). Atlas
|
|
197
|
+
* does NOT accept a dotted mapping key, so each parent object on a nested path
|
|
198
|
+
* must be its own `document` node. Nested fields are reachable by the dotted
|
|
199
|
+
* query path and by a `{ wildcard: "*" }` `text` operator.
|
|
200
|
+
* - `type: "embeddedDocuments"` — an array of objects (`items: Item[]`). Atlas
|
|
201
|
+
* cannot index array-of-object fields under a `document` node; they MUST use
|
|
202
|
+
* `embeddedDocuments` and be queried via the `embeddedDocument` operator
|
|
203
|
+
* (the wildcard `text` operator does not reach them).
|
|
194
204
|
*/
|
|
195
205
|
interface TSearchFieldMapping {
|
|
196
206
|
type: string;
|
|
@@ -199,6 +209,8 @@ interface TSearchFieldMapping {
|
|
|
199
209
|
minGrams?: number;
|
|
200
210
|
maxGrams?: number;
|
|
201
211
|
foldDiacritics?: boolean;
|
|
212
|
+
/** Nested mappings for `document` / `embeddedDocuments` container nodes. */
|
|
213
|
+
fields?: Record<string, TSearchFieldMapping | TSearchFieldMapping[]>;
|
|
202
214
|
}
|
|
203
215
|
interface TMongoSearchIndexDefinition {
|
|
204
216
|
mappings?: {
|
|
@@ -227,6 +239,18 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
227
239
|
protected _vectorThresholds: Map<string, number>;
|
|
228
240
|
/** Cached search index lookup. */
|
|
229
241
|
protected _searchIndexesMap?: Map<string, TMongoIndex>;
|
|
242
|
+
/**
|
|
243
|
+
* Search-field mappings recorded during `onFieldScanned`, applied in
|
|
244
|
+
* `onAfterFlatten`. Deferred because resolving a nested field's container
|
|
245
|
+
* shape (`document` vs `embeddedDocuments`) needs `this._table.flatMap`, which
|
|
246
|
+
* is only safe to read once the metadata build is marked complete — i.e. in
|
|
247
|
+
* `onAfterFlatten`, not mid-scan (the `flatMap` getter would re-enter `build`).
|
|
248
|
+
*/
|
|
249
|
+
protected _pendingSearchFields: Array<{
|
|
250
|
+
indexName: string | undefined;
|
|
251
|
+
field: string;
|
|
252
|
+
mappings: TSearchFieldMapping[];
|
|
253
|
+
}>;
|
|
230
254
|
/** Physical field names with @db.default.increment → optional start value. */
|
|
231
255
|
protected _incrementFields: Map<string, number | undefined>;
|
|
232
256
|
/** Physical field names that have a non-binary collation (nocase/unicode). */
|
|
@@ -408,6 +432,13 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
408
432
|
* instead of overwriting one another.
|
|
409
433
|
*/
|
|
410
434
|
protected _addFieldToSearchIndex(type: TSearchIndex["type"], _name: string | undefined, fieldName: string, mappings: TSearchFieldMapping[]): void;
|
|
435
|
+
/**
|
|
436
|
+
* Returns (creating if needed) the nested `fields` map of the container node
|
|
437
|
+
* at `segment`, reusing a same-typed sibling node so multiple searchable
|
|
438
|
+
* fields under one parent (e.g. `identity.name` + `identity.tagline`) merge
|
|
439
|
+
* instead of clobbering.
|
|
440
|
+
*/
|
|
441
|
+
private _descendSearchNode;
|
|
411
442
|
}
|
|
412
443
|
//#endregion
|
|
413
444
|
//#region src/lib/mongo-filter.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -191,6 +191,16 @@ type TVectorSimilarity = "cosine" | "euclidean" | "dotProduct";
|
|
|
191
191
|
* `type: "autocomplete"` enables prefix/typeahead (edgeGram) or substring (nGram).
|
|
192
192
|
* A field may carry several mappings at once (an array of these) — e.g. an
|
|
193
193
|
* autocomplete field double-mapped as `string` so exact-word hits still rank.
|
|
194
|
+
*
|
|
195
|
+
* Container nodes carry `fields` instead of leaf attributes:
|
|
196
|
+
* - `type: "document"` — a single embedded object (`identity: Identity`). Atlas
|
|
197
|
+
* does NOT accept a dotted mapping key, so each parent object on a nested path
|
|
198
|
+
* must be its own `document` node. Nested fields are reachable by the dotted
|
|
199
|
+
* query path and by a `{ wildcard: "*" }` `text` operator.
|
|
200
|
+
* - `type: "embeddedDocuments"` — an array of objects (`items: Item[]`). Atlas
|
|
201
|
+
* cannot index array-of-object fields under a `document` node; they MUST use
|
|
202
|
+
* `embeddedDocuments` and be queried via the `embeddedDocument` operator
|
|
203
|
+
* (the wildcard `text` operator does not reach them).
|
|
194
204
|
*/
|
|
195
205
|
interface TSearchFieldMapping {
|
|
196
206
|
type: string;
|
|
@@ -199,6 +209,8 @@ interface TSearchFieldMapping {
|
|
|
199
209
|
minGrams?: number;
|
|
200
210
|
maxGrams?: number;
|
|
201
211
|
foldDiacritics?: boolean;
|
|
212
|
+
/** Nested mappings for `document` / `embeddedDocuments` container nodes. */
|
|
213
|
+
fields?: Record<string, TSearchFieldMapping | TSearchFieldMapping[]>;
|
|
202
214
|
}
|
|
203
215
|
interface TMongoSearchIndexDefinition {
|
|
204
216
|
mappings?: {
|
|
@@ -227,6 +239,18 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
227
239
|
protected _vectorThresholds: Map<string, number>;
|
|
228
240
|
/** Cached search index lookup. */
|
|
229
241
|
protected _searchIndexesMap?: Map<string, TMongoIndex>;
|
|
242
|
+
/**
|
|
243
|
+
* Search-field mappings recorded during `onFieldScanned`, applied in
|
|
244
|
+
* `onAfterFlatten`. Deferred because resolving a nested field's container
|
|
245
|
+
* shape (`document` vs `embeddedDocuments`) needs `this._table.flatMap`, which
|
|
246
|
+
* is only safe to read once the metadata build is marked complete — i.e. in
|
|
247
|
+
* `onAfterFlatten`, not mid-scan (the `flatMap` getter would re-enter `build`).
|
|
248
|
+
*/
|
|
249
|
+
protected _pendingSearchFields: Array<{
|
|
250
|
+
indexName: string | undefined;
|
|
251
|
+
field: string;
|
|
252
|
+
mappings: TSearchFieldMapping[];
|
|
253
|
+
}>;
|
|
230
254
|
/** Physical field names with @db.default.increment → optional start value. */
|
|
231
255
|
protected _incrementFields: Map<string, number | undefined>;
|
|
232
256
|
/** Physical field names that have a non-binary collation (nocase/unicode). */
|
|
@@ -408,6 +432,13 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
408
432
|
* instead of overwriting one another.
|
|
409
433
|
*/
|
|
410
434
|
protected _addFieldToSearchIndex(type: TSearchIndex["type"], _name: string | undefined, fieldName: string, mappings: TSearchFieldMapping[]): void;
|
|
435
|
+
/**
|
|
436
|
+
* Returns (creating if needed) the nested `fields` map of the container node
|
|
437
|
+
* at `segment`, reusing a same-typed sibling node so multiple searchable
|
|
438
|
+
* fields under one parent (e.g. `identity.name` + `identity.tagline`) merge
|
|
439
|
+
* instead of clobbering.
|
|
440
|
+
*/
|
|
441
|
+
private _descendSearchNode;
|
|
411
442
|
}
|
|
412
443
|
//#endregion
|
|
413
444
|
//#region src/lib/mongo-filter.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { t as buildMongoFilter } from "./mongo-filter-DceAGI-S.mjs";
|
|
2
2
|
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, computeInsights, getDbFieldOp, getKeyProps, resolveDesignType } from "@atscript/db";
|
|
3
3
|
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
4
|
+
//#region src/lib/path-utils.ts
|
|
5
|
+
function joinPath(prefix, segment) {
|
|
6
|
+
return prefix ? `${prefix}.${segment}` : segment;
|
|
7
|
+
}
|
|
8
|
+
function isArrayPath(flatMap, path) {
|
|
9
|
+
const type = flatMap.get(path);
|
|
10
|
+
return type !== void 0 && resolveDesignType(type) === "array";
|
|
11
|
+
}
|
|
12
|
+
function hasAncestorIn(path, set) {
|
|
13
|
+
let dot = path.indexOf(".");
|
|
14
|
+
while (dot !== -1) {
|
|
15
|
+
if (set.has(path.slice(0, dot))) return true;
|
|
16
|
+
dot = path.indexOf(".", dot + 1);
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
4
21
|
//#region src/lib/projection-dedupe.ts
|
|
5
22
|
function dedupeProjection(projection) {
|
|
6
23
|
const includeKeys = Object.keys(projection).filter((k) => projection[k] === 1);
|
|
7
24
|
if (includeKeys.length < 2) return projection;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const prefix = parent + ".";
|
|
11
|
-
for (const other of includeKeys) if (other !== parent && other.startsWith(prefix)) toRemove.add(other);
|
|
12
|
-
}
|
|
25
|
+
const includes = new Set(includeKeys);
|
|
26
|
+
const toRemove = new Set(includeKeys.filter((k) => hasAncestorIn(k, includes)));
|
|
13
27
|
if (toRemove.size === 0) return projection;
|
|
14
28
|
const result = {};
|
|
15
29
|
for (const k of Object.keys(projection)) if (!toRemove.has(k)) result[k] = projection[k];
|
|
@@ -149,7 +163,7 @@ var CollectionPatcher = class {
|
|
|
149
163
|
* @private
|
|
150
164
|
*/
|
|
151
165
|
flattenPayload(payload, prefix = "") {
|
|
152
|
-
const evalKey = (k) => prefix
|
|
166
|
+
const evalKey = (k) => joinPath(prefix, k);
|
|
153
167
|
for (const [_key, value] of Object.entries(payload)) {
|
|
154
168
|
const key = evalKey(_key);
|
|
155
169
|
const flatType = this.collection.flatMap.get(key);
|
|
@@ -762,27 +776,44 @@ function buildSearchStage(host, text, indexName, controls) {
|
|
|
762
776
|
const searchIndex = index;
|
|
763
777
|
const fuzzy = resolveSearchFuzzy(searchIndex, controls);
|
|
764
778
|
const strategy = searchIndex.strategy ?? "compound";
|
|
765
|
-
const
|
|
766
|
-
const
|
|
779
|
+
const paths = collectSearchPathsCached(searchIndex);
|
|
780
|
+
const fuzzyOpt = fuzzy ? { fuzzy } : {};
|
|
781
|
+
const textOp = (path) => ({ text: {
|
|
767
782
|
query: text,
|
|
768
|
-
path
|
|
769
|
-
...
|
|
783
|
+
path,
|
|
784
|
+
...fuzzyOpt
|
|
770
785
|
} });
|
|
771
|
-
const
|
|
786
|
+
const autocompleteOp = (path) => ({ autocomplete: {
|
|
772
787
|
query: text,
|
|
773
788
|
path,
|
|
774
|
-
...
|
|
789
|
+
...fuzzyOpt
|
|
775
790
|
} });
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
minimumShouldMatch: 1
|
|
791
|
+
const textClause = () => textOp({ wildcard: "*" });
|
|
792
|
+
const wrapEmbedded = (chain, operator) => {
|
|
793
|
+
let wrapped = operator;
|
|
794
|
+
for (let i = chain.length - 1; i >= 0; i--) wrapped = { embeddedDocument: {
|
|
795
|
+
path: chain[i],
|
|
796
|
+
operator: wrapped
|
|
783
797
|
} };
|
|
784
|
-
|
|
785
|
-
|
|
798
|
+
return wrapped;
|
|
799
|
+
};
|
|
800
|
+
const embeddedTextClause = (chain, leaves) => wrapEmbedded(chain, textOp(leaves.length === 1 ? leaves[0] : leaves));
|
|
801
|
+
const embeddedAutocompleteClause = (chain, leaf) => wrapEmbedded(chain, autocompleteOp(leaf));
|
|
802
|
+
const wantWord = strategy === "text" || strategy === "compound";
|
|
803
|
+
const wantAutocomplete = strategy === "autocomplete" || strategy === "compound";
|
|
804
|
+
const clauses = [];
|
|
805
|
+
if (wantWord) {
|
|
806
|
+
clauses.push(textClause());
|
|
807
|
+
for (const group of paths.arrayGroups) if (group.textLeaves.length > 0) clauses.push(embeddedTextClause(group.chain, group.textLeaves));
|
|
808
|
+
}
|
|
809
|
+
if (wantAutocomplete) {
|
|
810
|
+
for (const path of paths.autocompleteOutside) clauses.push(autocompleteOp(path));
|
|
811
|
+
for (const group of paths.arrayGroups) for (const leaf of group.autocompleteLeaves) clauses.push(embeddedAutocompleteClause(group.chain, leaf));
|
|
812
|
+
}
|
|
813
|
+
let body;
|
|
814
|
+
if (clauses.length <= 1) body = clauses[0] ?? textClause();
|
|
815
|
+
else body = { compound: {
|
|
816
|
+
should: clauses,
|
|
786
817
|
minimumShouldMatch: 1
|
|
787
818
|
} };
|
|
788
819
|
return {
|
|
@@ -803,13 +834,65 @@ function resolveSearchFuzzy(index, controls) {
|
|
|
803
834
|
const maxEdits = override === void 0 ? index.fuzzy?.maxEdits : Number(override);
|
|
804
835
|
return maxEdits === 1 || maxEdits === 2 ? { maxEdits } : void 0;
|
|
805
836
|
}
|
|
806
|
-
/**
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
837
|
+
/**
|
|
838
|
+
* An index's mapping tree is immutable once schema sync builds it, and the index
|
|
839
|
+
* object is cached for the adapter's lifetime — so the walk runs once per index,
|
|
840
|
+
* not once per query (`buildSearchStage` is on the search hot path).
|
|
841
|
+
*/
|
|
842
|
+
const searchPathsCache = /* @__PURE__ */ new WeakMap();
|
|
843
|
+
function collectSearchPathsCached(index) {
|
|
844
|
+
let cached = searchPathsCache.get(index);
|
|
845
|
+
if (!cached) {
|
|
846
|
+
cached = collectSearchPaths(index.definition.mappings?.fields);
|
|
847
|
+
searchPathsCache.set(index, cached);
|
|
848
|
+
}
|
|
849
|
+
return cached;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Walks an index's nested mapping tree, classifying each searchable leaf by how
|
|
853
|
+
* it must be queried: a plain dotted path (top-level / `document`-nested) vs.
|
|
854
|
+
* scoped under an `embeddedDocuments` array root (queried via `embeddedDocument`).
|
|
855
|
+
* Word-matching for non-array fields rides the wildcard `text` clause, so only
|
|
856
|
+
* autocomplete leaves are tracked outside arrays.
|
|
857
|
+
*/
|
|
858
|
+
function collectSearchPaths(fields) {
|
|
859
|
+
const out = {
|
|
860
|
+
autocompleteOutside: [],
|
|
861
|
+
arrayGroups: []
|
|
862
|
+
};
|
|
863
|
+
const byChain = /* @__PURE__ */ new Map();
|
|
864
|
+
const walk = (map, prefix, chain) => {
|
|
865
|
+
if (!map) return;
|
|
866
|
+
for (const [key, mapping] of Object.entries(map)) {
|
|
867
|
+
const path = joinPath(prefix, key);
|
|
868
|
+
const list = Array.isArray(mapping) ? mapping : [mapping];
|
|
869
|
+
const embedded = list.find((m) => m.type === "embeddedDocuments");
|
|
870
|
+
const document = list.find((m) => m.type === "document");
|
|
871
|
+
if (embedded?.fields) walk(embedded.fields, path, [...chain, path]);
|
|
872
|
+
else if (document?.fields) walk(document.fields, path, chain);
|
|
873
|
+
else {
|
|
874
|
+
const isAutocomplete = list.some((m) => m.type === "autocomplete");
|
|
875
|
+
const isText = list.some((m) => m.type === "string");
|
|
876
|
+
if (chain.length > 0) {
|
|
877
|
+
const chainKey = chain.join(">");
|
|
878
|
+
let group = byChain.get(chainKey);
|
|
879
|
+
if (!group) {
|
|
880
|
+
group = {
|
|
881
|
+
chain,
|
|
882
|
+
textLeaves: [],
|
|
883
|
+
autocompleteLeaves: []
|
|
884
|
+
};
|
|
885
|
+
byChain.set(chainKey, group);
|
|
886
|
+
out.arrayGroups.push(group);
|
|
887
|
+
}
|
|
888
|
+
if (isText) group.textLeaves.push(path);
|
|
889
|
+
if (isAutocomplete) group.autocompleteLeaves.push(path);
|
|
890
|
+
} else if (isAutocomplete) out.autocompleteOutside.push(path);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
walk(fields, "", []);
|
|
895
|
+
return out;
|
|
813
896
|
}
|
|
814
897
|
/** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
|
|
815
898
|
function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
@@ -1143,8 +1226,10 @@ async function syncColumnsImpl(host, diff) {
|
|
|
1143
1226
|
}
|
|
1144
1227
|
async function dropColumnsImpl(host, columns) {
|
|
1145
1228
|
if (columns.length === 0) return;
|
|
1229
|
+
const dropped = new Set(columns);
|
|
1230
|
+
const minimal = columns.filter((col) => !hasAncestorIn(col, dropped));
|
|
1146
1231
|
const unsetSpec = {};
|
|
1147
|
-
for (const col of
|
|
1232
|
+
for (const col of minimal) unsetSpec[arraySafePath(host, col, col)] = "";
|
|
1148
1233
|
await host.collection.updateMany({}, { $unset: unsetSpec }, host._getSessionOpts());
|
|
1149
1234
|
}
|
|
1150
1235
|
/**
|
|
@@ -1166,11 +1251,8 @@ function arraySafePath(host, physicalPath, logicalPath) {
|
|
|
1166
1251
|
for (let i = 0; i < logicalSegments.length; i++) {
|
|
1167
1252
|
const isLeaf = i === logicalSegments.length - 1;
|
|
1168
1253
|
out.push(isLeaf ? physicalLeaf : logicalSegments[i]);
|
|
1169
|
-
prefix = prefix
|
|
1170
|
-
if (!isLeaf)
|
|
1171
|
-
const type = host._table.flatMap.get(prefix);
|
|
1172
|
-
if (type && resolveDesignType(type) === "array") out.push("$[]");
|
|
1173
|
-
}
|
|
1254
|
+
prefix = joinPath(prefix, logicalSegments[i]);
|
|
1255
|
+
if (!isLeaf && isArrayPath(host._table.flatMap, prefix)) out.push("$[]");
|
|
1174
1256
|
}
|
|
1175
1257
|
return out.join(".");
|
|
1176
1258
|
}
|
|
@@ -1180,9 +1262,8 @@ function pathCrossesArray(host, logicalPath) {
|
|
|
1180
1262
|
if (segments.length < 2) return false;
|
|
1181
1263
|
let prefix = "";
|
|
1182
1264
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
1183
|
-
prefix = prefix
|
|
1184
|
-
|
|
1185
|
-
if (type && resolveDesignType(type) === "array") return true;
|
|
1265
|
+
prefix = joinPath(prefix, segments[i]);
|
|
1266
|
+
if (isArrayPath(host._table.flatMap, prefix)) return true;
|
|
1186
1267
|
}
|
|
1187
1268
|
return false;
|
|
1188
1269
|
}
|
|
@@ -1430,6 +1511,7 @@ function fieldMappingEqual(a, b) {
|
|
|
1430
1511
|
for (const [type, av] of am) {
|
|
1431
1512
|
const bv = bm.get(type);
|
|
1432
1513
|
if (!bv || av.analyzer !== bv.analyzer || av.tokenization !== bv.tokenization || av.minGrams !== bv.minGrams || av.maxGrams !== bv.maxGrams || av.foldDiacritics !== bv.foldDiacritics) return false;
|
|
1514
|
+
if (!fieldsMatch(av.fields, bv.fields)) return false;
|
|
1433
1515
|
}
|
|
1434
1516
|
return true;
|
|
1435
1517
|
}
|
|
@@ -1474,6 +1556,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1474
1556
|
_vectorThresholds = /* @__PURE__ */ new Map();
|
|
1475
1557
|
/** Cached search index lookup. */
|
|
1476
1558
|
_searchIndexesMap;
|
|
1559
|
+
/**
|
|
1560
|
+
* Search-field mappings recorded during `onFieldScanned`, applied in
|
|
1561
|
+
* `onAfterFlatten`. Deferred because resolving a nested field's container
|
|
1562
|
+
* shape (`document` vs `embeddedDocuments`) needs `this._table.flatMap`, which
|
|
1563
|
+
* is only safe to read once the metadata build is marked complete — i.e. in
|
|
1564
|
+
* `onAfterFlatten`, not mid-scan (the `flatMap` getter would re-enter `build`).
|
|
1565
|
+
*/
|
|
1566
|
+
_pendingSearchFields = [];
|
|
1477
1567
|
/** Physical field names with @db.default.increment → optional start value. */
|
|
1478
1568
|
_incrementFields = /* @__PURE__ */ new Map();
|
|
1479
1569
|
/** Physical field names that have a non-binary collation (nocase/unicode). */
|
|
@@ -1698,10 +1788,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1698
1788
|
const startValue = metadata.get("db.default.increment");
|
|
1699
1789
|
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
|
|
1700
1790
|
}
|
|
1701
|
-
for (const index of metadata.get("db.mongo.search.text") || []) this.
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1791
|
+
for (const index of metadata.get("db.mongo.search.text") || []) this._pendingSearchFields.push({
|
|
1792
|
+
indexName: index.indexName,
|
|
1793
|
+
field,
|
|
1794
|
+
mappings: [index.analyzer ? {
|
|
1795
|
+
type: "string",
|
|
1796
|
+
analyzer: index.analyzer
|
|
1797
|
+
} : { type: "string" }]
|
|
1798
|
+
});
|
|
1705
1799
|
for (const ac of metadata.get("db.mongo.search.autocomplete") || []) {
|
|
1706
1800
|
const autocomplete = {
|
|
1707
1801
|
type: "autocomplete",
|
|
@@ -1714,7 +1808,11 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1714
1808
|
type: "string",
|
|
1715
1809
|
analyzer: ac.analyzer
|
|
1716
1810
|
} : { type: "string" };
|
|
1717
|
-
this.
|
|
1811
|
+
this._pendingSearchFields.push({
|
|
1812
|
+
indexName: ac.indexName,
|
|
1813
|
+
field,
|
|
1814
|
+
mappings: [autocomplete, companion]
|
|
1815
|
+
});
|
|
1718
1816
|
}
|
|
1719
1817
|
const vectorIndex = metadata.get("db.search.vector");
|
|
1720
1818
|
if (vectorIndex) {
|
|
@@ -1756,6 +1854,8 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1756
1854
|
};
|
|
1757
1855
|
}
|
|
1758
1856
|
onAfterFlatten() {
|
|
1857
|
+
for (const pending of this._pendingSearchFields) this._addFieldToSearchIndex("search_text", pending.indexName, pending.field, pending.mappings);
|
|
1858
|
+
this._pendingSearchFields = [];
|
|
1759
1859
|
for (const [key, value] of this._vectorFilters.entries()) {
|
|
1760
1860
|
const index = this._mongoIndexes.get(key);
|
|
1761
1861
|
if (index && index.type === "vector") index.definition.fields?.push({
|
|
@@ -2209,10 +2309,45 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2209
2309
|
const name = _name || "DEFAULT";
|
|
2210
2310
|
let index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2211
2311
|
if (!index && type === "search_text") index = this._setSearchIndex(type, name, { mappings: { fields: {} } });
|
|
2212
|
-
if (index)
|
|
2213
|
-
|
|
2214
|
-
|
|
2312
|
+
if (!index) return;
|
|
2313
|
+
const rootFields = index.definition.mappings.fields;
|
|
2314
|
+
const segments = fieldName.split(".");
|
|
2315
|
+
const topKey = this._table.columnMap.get(segments[0]) ?? segments[0];
|
|
2316
|
+
if (segments.length === 1) {
|
|
2317
|
+
rootFields[topKey] = mergeFieldMappings(rootFields[topKey], mappings);
|
|
2318
|
+
return;
|
|
2215
2319
|
}
|
|
2320
|
+
let cursor = rootFields;
|
|
2321
|
+
let prefix = "";
|
|
2322
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
2323
|
+
const segment = segments[i];
|
|
2324
|
+
prefix = joinPath(prefix, segment);
|
|
2325
|
+
const nodeType = isArrayPath(this._table.flatMap, prefix) ? "embeddedDocuments" : "document";
|
|
2326
|
+
cursor = this._descendSearchNode(cursor, i === 0 ? topKey : segment, nodeType, fieldName);
|
|
2327
|
+
}
|
|
2328
|
+
const leaf = segments[segments.length - 1];
|
|
2329
|
+
cursor[leaf] = mergeFieldMappings(cursor[leaf], mappings);
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Returns (creating if needed) the nested `fields` map of the container node
|
|
2333
|
+
* at `segment`, reusing a same-typed sibling node so multiple searchable
|
|
2334
|
+
* fields under one parent (e.g. `identity.name` + `identity.tagline`) merge
|
|
2335
|
+
* instead of clobbering.
|
|
2336
|
+
*/
|
|
2337
|
+
_descendSearchNode(fields, segment, nodeType, fullPath) {
|
|
2338
|
+
const existing = fields[segment];
|
|
2339
|
+
if (existing && !Array.isArray(existing) && (existing.type === "document" || existing.type === "embeddedDocuments")) {
|
|
2340
|
+
if (existing.type !== nodeType) this._log(`search index: conflicting container type for "${fullPath}" (${existing.type} vs ${nodeType}); keeping ${existing.type}`);
|
|
2341
|
+
existing.fields ??= {};
|
|
2342
|
+
return existing.fields;
|
|
2343
|
+
}
|
|
2344
|
+
if (existing) this._log(`search index: field "${segment}" used as both value and parent in "${fullPath}"`);
|
|
2345
|
+
const node = {
|
|
2346
|
+
type: nodeType,
|
|
2347
|
+
fields: {}
|
|
2348
|
+
};
|
|
2349
|
+
fields[segment] = node;
|
|
2350
|
+
return node.fields;
|
|
2216
2351
|
}
|
|
2217
2352
|
};
|
|
2218
2353
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mongo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.108",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -46,17 +46,17 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@atscript/core": "^0.1.
|
|
50
|
-
"@atscript/typescript": "^0.1.
|
|
49
|
+
"@atscript/core": "^0.1.77",
|
|
50
|
+
"@atscript/typescript": "^0.1.77",
|
|
51
51
|
"mongodb": "^6.17.0",
|
|
52
52
|
"mongodb-memory-server-core": "^10.0.0",
|
|
53
|
-
"unplugin-atscript": "^0.1.
|
|
53
|
+
"unplugin-atscript": "^0.1.77"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@atscript/core": "^0.1.
|
|
57
|
-
"@atscript/typescript": "^0.1.
|
|
56
|
+
"@atscript/core": "^0.1.77",
|
|
57
|
+
"@atscript/typescript": "^0.1.77",
|
|
58
58
|
"mongodb": "^6.17.0",
|
|
59
|
-
"@atscript/db": "^0.1.
|
|
59
|
+
"@atscript/db": "^0.1.108"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"postinstall": "asc -f dts",
|