@atscript/db-mongo 0.1.101 → 0.1.103
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/agg.cjs +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/index-Bl_O47fp.d.cts +6 -0
- package/dist/index-Bl_O47fp.d.mts +6 -0
- package/dist/index.cjs +147 -29
- package/dist/index.d.cts +49 -12
- package/dist/index.d.mts +49 -12
- package/dist/index.mjs +147 -30
- package/dist/plugin-Bq6hZMBA.cjs +242 -0
- package/dist/plugin-KVFAwoGw.mjs +237 -0
- package/dist/plugin.cjs +3 -174
- package/dist/plugin.d.cts +1 -5
- package/dist/plugin.d.mts +1 -5
- package/dist/plugin.mjs +1 -172
- package/package.json +7 -7
- /package/dist/{mongo-filter-AihWWQXp.cjs → mongo-filter-1EpqdD-T.cjs} +0 -0
- /package/dist/{mongo-filter-BsocUQG3.mjs → mongo-filter-DBYaF9aH.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { t as buildMongoFilter } from "./mongo-filter-
|
|
1
|
+
import { t as buildMongoFilter } from "./mongo-filter-DBYaF9aH.mjs";
|
|
2
|
+
import { t as MongoPlugin } from "./plugin-KVFAwoGw.mjs";
|
|
2
3
|
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, computeInsights, getDbFieldOp, getKeyProps, resolveDesignType } from "@atscript/db";
|
|
3
4
|
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
4
5
|
//#region src/lib/projection-dedupe.ts
|
|
@@ -71,6 +72,9 @@ function containsAggregationExpr(v) {
|
|
|
71
72
|
* `$set` map.
|
|
72
73
|
*/
|
|
73
74
|
var CollectionPatcher = class {
|
|
75
|
+
collection;
|
|
76
|
+
payload;
|
|
77
|
+
ops;
|
|
74
78
|
constructor(collection, payload, ops) {
|
|
75
79
|
this.collection = collection;
|
|
76
80
|
this.payload = payload;
|
|
@@ -629,13 +633,13 @@ function isVectorSearchableImpl(host) {
|
|
|
629
633
|
}
|
|
630
634
|
/** Text search via $search aggregation stage. */
|
|
631
635
|
async function searchImpl(host, text, query, indexName) {
|
|
632
|
-
const plan = buildSearchStage(host, text, indexName);
|
|
636
|
+
const plan = buildSearchStage(host, text, indexName, query.controls);
|
|
633
637
|
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
634
638
|
return runSearchPipeline(host, plan.stage, query, "search", void 0, plan.classicText);
|
|
635
639
|
}
|
|
636
640
|
/** Text search with faceted count. */
|
|
637
641
|
async function searchWithCountImpl(host, text, query, indexName) {
|
|
638
|
-
const plan = buildSearchStage(host, text, indexName);
|
|
642
|
+
const plan = buildSearchStage(host, text, indexName, query.controls);
|
|
639
643
|
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
640
644
|
return runSearchWithCountPipeline(host, plan.stage, query, "searchWithCount", void 0, plan.classicText);
|
|
641
645
|
}
|
|
@@ -671,7 +675,7 @@ function resolveThreshold(host, controls, indexName) {
|
|
|
671
675
|
* MongoDB the stage is unsupported entirely — even though the table reports
|
|
672
676
|
* `searchable: true`. The discriminator is the resolved index's `type`.
|
|
673
677
|
*/
|
|
674
|
-
function buildSearchStage(host, text, indexName) {
|
|
678
|
+
function buildSearchStage(host, text, indexName, controls) {
|
|
675
679
|
const index = host.getMongoSearchIndex(indexName);
|
|
676
680
|
if (!index) return;
|
|
677
681
|
if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
|
|
@@ -679,17 +683,58 @@ function buildSearchStage(host, text, indexName) {
|
|
|
679
683
|
stage: { $match: { $text: { $search: text } } },
|
|
680
684
|
classicText: true
|
|
681
685
|
};
|
|
686
|
+
const searchIndex = index;
|
|
687
|
+
const fuzzy = resolveSearchFuzzy(searchIndex, controls);
|
|
688
|
+
const strategy = searchIndex.strategy ?? "compound";
|
|
689
|
+
const autocompletePaths = autocompleteFieldPaths(searchIndex);
|
|
690
|
+
const textClause = () => ({ text: {
|
|
691
|
+
query: text,
|
|
692
|
+
path: { wildcard: "*" },
|
|
693
|
+
...fuzzy ? { fuzzy } : {}
|
|
694
|
+
} });
|
|
695
|
+
const autocompleteClause = (path) => ({ autocomplete: {
|
|
696
|
+
query: text,
|
|
697
|
+
path,
|
|
698
|
+
...fuzzy ? { fuzzy } : {}
|
|
699
|
+
} });
|
|
700
|
+
let body;
|
|
701
|
+
if (strategy === "text" || autocompletePaths.length === 0) body = textClause();
|
|
702
|
+
else if (strategy === "autocomplete") {
|
|
703
|
+
const clauses = autocompletePaths.map(autocompleteClause);
|
|
704
|
+
body = clauses.length === 1 ? clauses[0] : { compound: {
|
|
705
|
+
should: clauses,
|
|
706
|
+
minimumShouldMatch: 1
|
|
707
|
+
} };
|
|
708
|
+
} else body = { compound: {
|
|
709
|
+
should: [textClause(), ...autocompletePaths.map(autocompleteClause)],
|
|
710
|
+
minimumShouldMatch: 1
|
|
711
|
+
} };
|
|
682
712
|
return {
|
|
683
713
|
stage: { $search: {
|
|
684
714
|
index: index.key,
|
|
685
|
-
|
|
686
|
-
query: text,
|
|
687
|
-
path: { wildcard: "*" }
|
|
688
|
-
}
|
|
715
|
+
...body
|
|
689
716
|
} },
|
|
690
717
|
classicText: false
|
|
691
718
|
};
|
|
692
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Resolves query-time fuzzy (typo tolerance): the `$fuzzy` request control
|
|
722
|
+
* overrides the schema-declared `@db.mongo.search.*` fuzzy. Only an edit distance
|
|
723
|
+
* of 1 or 2 is emitted (Atlas rejects 0) — anything else means "no fuzzy".
|
|
724
|
+
*/
|
|
725
|
+
function resolveSearchFuzzy(index, controls) {
|
|
726
|
+
const override = controls?.$fuzzy;
|
|
727
|
+
const maxEdits = override === void 0 ? index.fuzzy?.maxEdits : Number(override);
|
|
728
|
+
return maxEdits === 1 || maxEdits === 2 ? { maxEdits } : void 0;
|
|
729
|
+
}
|
|
730
|
+
/** Field paths in this index that carry an `autocomplete` mapping. */
|
|
731
|
+
function autocompleteFieldPaths(index) {
|
|
732
|
+
const fields = index.definition.mappings?.fields;
|
|
733
|
+
if (!fields) return [];
|
|
734
|
+
const paths = [];
|
|
735
|
+
for (const [path, mapping] of Object.entries(fields)) if ((Array.isArray(mapping) ? mapping : [mapping]).some((m) => m.type === "autocomplete")) paths.push(path);
|
|
736
|
+
return paths;
|
|
737
|
+
}
|
|
693
738
|
/** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
|
|
694
739
|
function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
695
740
|
let index;
|
|
@@ -1288,10 +1333,26 @@ function fieldsMatch(left, right) {
|
|
|
1288
1333
|
if (leftKeys.length !== rightKeys.length) return false;
|
|
1289
1334
|
for (const key of leftKeys) {
|
|
1290
1335
|
if (!(key in right)) return false;
|
|
1291
|
-
if (left[key]
|
|
1336
|
+
if (!fieldMappingEqual(left[key], right[key])) return false;
|
|
1292
1337
|
}
|
|
1293
1338
|
return true;
|
|
1294
1339
|
}
|
|
1340
|
+
/** Order-independent structural compare of a field's Atlas type mapping(s). */
|
|
1341
|
+
function fieldMappingEqual(a, b) {
|
|
1342
|
+
const am = mappingsByType(a);
|
|
1343
|
+
const bm = mappingsByType(b);
|
|
1344
|
+
if (am.size !== bm.size) return false;
|
|
1345
|
+
for (const [type, av] of am) {
|
|
1346
|
+
const bv = bm.get(type);
|
|
1347
|
+
if (!bv || av.analyzer !== bv.analyzer || av.tokenization !== bv.tokenization || av.minGrams !== bv.minGrams || av.maxGrams !== bv.maxGrams || av.foldDiacritics !== bv.foldDiacritics) return false;
|
|
1348
|
+
}
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
function mappingsByType(m) {
|
|
1352
|
+
const map = /* @__PURE__ */ new Map();
|
|
1353
|
+
for (const x of Array.isArray(m) ? m : [m]) map.set(x.type, x);
|
|
1354
|
+
return map;
|
|
1355
|
+
}
|
|
1295
1356
|
function vectorFieldsMatch(left, right) {
|
|
1296
1357
|
if (left.length !== (right || []).length) return false;
|
|
1297
1358
|
const rightMap = /* @__PURE__ */ new Map();
|
|
@@ -1317,6 +1378,8 @@ const validateMongoIdPlugin = (ctx, def, value) => {
|
|
|
1317
1378
|
//#endregion
|
|
1318
1379
|
//#region src/lib/mongo-adapter.ts
|
|
1319
1380
|
var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
1381
|
+
db;
|
|
1382
|
+
client;
|
|
1320
1383
|
_collection;
|
|
1321
1384
|
/** MongoDB-specific indexes (search, vector) — separate from table.indexes. */
|
|
1322
1385
|
_mongoIndexes = /* @__PURE__ */ new Map();
|
|
@@ -1507,13 +1570,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1507
1570
|
const dynamicText = typeMeta.get("db.mongo.search.dynamic");
|
|
1508
1571
|
if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
|
|
1509
1572
|
mappings: { dynamic: true },
|
|
1510
|
-
analyzer: dynamicText.analyzer
|
|
1511
|
-
|
|
1512
|
-
});
|
|
1573
|
+
analyzer: dynamicText.analyzer
|
|
1574
|
+
}, { fuzzy: normalizeSearchFuzzy(dynamicText.fuzzy) });
|
|
1513
1575
|
for (const textSearch of typeMeta.get("db.mongo.search.static") || []) this._setSearchIndex("search_text", textSearch.indexName, {
|
|
1514
1576
|
mappings: { fields: {} },
|
|
1515
|
-
analyzer: textSearch.analyzer
|
|
1516
|
-
|
|
1577
|
+
analyzer: textSearch.analyzer
|
|
1578
|
+
}, {
|
|
1579
|
+
fuzzy: normalizeSearchFuzzy(textSearch.fuzzy),
|
|
1580
|
+
strategy: normalizeSearchStrategy(textSearch.strategy)
|
|
1517
1581
|
});
|
|
1518
1582
|
}
|
|
1519
1583
|
onFieldScanned(field, _type, metadata) {
|
|
@@ -1527,7 +1591,24 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1527
1591
|
const startValue = metadata.get("db.default.increment");
|
|
1528
1592
|
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
|
|
1529
1593
|
}
|
|
1530
|
-
for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, index.analyzer
|
|
1594
|
+
for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, [index.analyzer ? {
|
|
1595
|
+
type: "string",
|
|
1596
|
+
analyzer: index.analyzer
|
|
1597
|
+
} : { type: "string" }]);
|
|
1598
|
+
for (const ac of metadata.get("db.mongo.search.autocomplete") || []) {
|
|
1599
|
+
const autocomplete = {
|
|
1600
|
+
type: "autocomplete",
|
|
1601
|
+
tokenization: ac.tokenization || "edgeGram",
|
|
1602
|
+
minGrams: ac.minGrams ?? 2,
|
|
1603
|
+
maxGrams: ac.maxGrams ?? 15,
|
|
1604
|
+
foldDiacritics: ac.foldDiacritics ?? true
|
|
1605
|
+
};
|
|
1606
|
+
const companion = ac.analyzer ? {
|
|
1607
|
+
type: "string",
|
|
1608
|
+
analyzer: ac.analyzer
|
|
1609
|
+
} : { type: "string" };
|
|
1610
|
+
this._addFieldToSearchIndex("search_text", ac.indexName, field, [autocomplete, companion]);
|
|
1611
|
+
}
|
|
1531
1612
|
const vectorIndex = metadata.get("db.search.vector");
|
|
1532
1613
|
if (vectorIndex) {
|
|
1533
1614
|
const indexName = vectorIndex.indexName || field;
|
|
@@ -1988,32 +2069,68 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1988
2069
|
}
|
|
1989
2070
|
if (weight) index.weights[field] = weight;
|
|
1990
2071
|
}
|
|
1991
|
-
_setSearchIndex(type, name, definition) {
|
|
2072
|
+
_setSearchIndex(type, name, definition, meta) {
|
|
1992
2073
|
const key = mongoIndexKey(type, name || "DEFAULT");
|
|
1993
|
-
|
|
2074
|
+
const index = {
|
|
1994
2075
|
key,
|
|
1995
2076
|
name: name || "DEFAULT",
|
|
1996
2077
|
type,
|
|
1997
|
-
definition
|
|
1998
|
-
|
|
2078
|
+
definition,
|
|
2079
|
+
fuzzy: meta?.fuzzy,
|
|
2080
|
+
strategy: meta?.strategy
|
|
2081
|
+
};
|
|
2082
|
+
this._mongoIndexes.set(key, index);
|
|
2083
|
+
return index;
|
|
1999
2084
|
}
|
|
2000
|
-
|
|
2085
|
+
/**
|
|
2086
|
+
* Adds (and merges, by mapping `type`) one or more Atlas field-type mappings to
|
|
2087
|
+
* a `search_text` index. Multiple annotations on the same field — e.g.
|
|
2088
|
+
* `@db.mongo.search.text` (string) plus `@db.mongo.search.autocomplete`
|
|
2089
|
+
* (autocomplete + string) — accumulate into a single multi-type mapping
|
|
2090
|
+
* instead of overwriting one another.
|
|
2091
|
+
*/
|
|
2092
|
+
_addFieldToSearchIndex(type, _name, fieldName, mappings) {
|
|
2001
2093
|
const name = _name || "DEFAULT";
|
|
2002
2094
|
let index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2003
|
-
if (!index && type === "search_text") {
|
|
2004
|
-
this._setSearchIndex(type, name, {
|
|
2005
|
-
mappings: { fields: {} },
|
|
2006
|
-
text: { fuzzy: { maxEdits: 0 } }
|
|
2007
|
-
});
|
|
2008
|
-
index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
2009
|
-
}
|
|
2095
|
+
if (!index && type === "search_text") index = this._setSearchIndex(type, name, { mappings: { fields: {} } });
|
|
2010
2096
|
if (index) {
|
|
2011
|
-
index.definition.mappings.fields
|
|
2012
|
-
|
|
2097
|
+
const fields = index.definition.mappings.fields;
|
|
2098
|
+
fields[fieldName] = mergeFieldMappings(fields[fieldName], mappings);
|
|
2013
2099
|
}
|
|
2014
2100
|
}
|
|
2015
2101
|
};
|
|
2016
2102
|
/**
|
|
2103
|
+
* Normalizes a declared `fuzzy` arg (`0-2`) into the query-time metadata Atlas
|
|
2104
|
+
* accepts. Atlas only honors an edit distance of `1` or `2`; `0`/undefined means
|
|
2105
|
+
* "no fuzzy", returned as `undefined` so no `fuzzy` clause is ever emitted.
|
|
2106
|
+
*/
|
|
2107
|
+
function normalizeSearchFuzzy(fuzzy) {
|
|
2108
|
+
return fuzzy === 1 || fuzzy === 2 ? { maxEdits: fuzzy } : void 0;
|
|
2109
|
+
}
|
|
2110
|
+
/** Narrows the declared `strategy` arg to a known value (undefined → query layer defaults to "compound"). */
|
|
2111
|
+
function normalizeSearchStrategy(strategy) {
|
|
2112
|
+
return strategy === "compound" || strategy === "autocomplete" || strategy === "text" ? strategy : void 0;
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Merges incoming Atlas field-type mappings into a field's existing mapping,
|
|
2116
|
+
* keyed by `type` (a later mapping of the same type replaces the earlier one).
|
|
2117
|
+
* Collapses to a single object when one type remains, else an array (Atlas's
|
|
2118
|
+
* multi-type field form).
|
|
2119
|
+
*/
|
|
2120
|
+
function mergeFieldMappings(existing, incoming) {
|
|
2121
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2122
|
+
const order = [];
|
|
2123
|
+
const add = (m) => {
|
|
2124
|
+
if (!byType.has(m.type)) order.push(m.type);
|
|
2125
|
+
byType.set(m.type, m);
|
|
2126
|
+
};
|
|
2127
|
+
if (Array.isArray(existing)) existing.forEach(add);
|
|
2128
|
+
else if (existing) add(existing);
|
|
2129
|
+
incoming.forEach(add);
|
|
2130
|
+
const merged = order.map((t) => byType.get(t));
|
|
2131
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2017
2134
|
* Builds a MongoDB update document from a data object that may contain
|
|
2018
2135
|
* field ops (`{ $inc: N }`, `{ $dec: N }`, `{ $mul: N }`).
|
|
2019
2136
|
* Regular fields go into `$set`, ops go into `$inc` / `$mul`. When
|
|
@@ -2044,4 +2161,4 @@ function createAdapter(connection, _options) {
|
|
|
2044
2161
|
return new DbSpace(() => new MongoAdapter(db, client));
|
|
2045
2162
|
}
|
|
2046
2163
|
//#endregion
|
|
2047
|
-
export { CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
|
2164
|
+
export { CollectionPatcher, MongoAdapter, MongoPlugin, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
let _atscript_core = require("@atscript/core");
|
|
2
|
+
//#region src/plugin/annotations.ts
|
|
3
|
+
const analyzers = [
|
|
4
|
+
"lucene.standard",
|
|
5
|
+
"lucene.simple",
|
|
6
|
+
"lucene.whitespace",
|
|
7
|
+
"lucene.english",
|
|
8
|
+
"lucene.french",
|
|
9
|
+
"lucene.german",
|
|
10
|
+
"lucene.italian",
|
|
11
|
+
"lucene.portuguese",
|
|
12
|
+
"lucene.spanish",
|
|
13
|
+
"lucene.chinese",
|
|
14
|
+
"lucene.hindi",
|
|
15
|
+
"lucene.bengali",
|
|
16
|
+
"lucene.russian",
|
|
17
|
+
"lucene.arabic"
|
|
18
|
+
];
|
|
19
|
+
const tokenizations = [
|
|
20
|
+
"edgeGram",
|
|
21
|
+
"rightEdgeGram",
|
|
22
|
+
"nGram"
|
|
23
|
+
];
|
|
24
|
+
const searchStrategies = [
|
|
25
|
+
"compound",
|
|
26
|
+
"autocomplete",
|
|
27
|
+
"text"
|
|
28
|
+
];
|
|
29
|
+
const strategyDescription = "How `search()` matches a term against this index. Locks the query shape into the index — there is no query-time mode switching.\n\n- `compound` (default) → rank exact-word hits above prefix hits: a wildcard `text` clause **plus** one `autocomplete` clause per autocomplete field. Degrades to plain `text` when the index has no autocomplete field.\n- `autocomplete` → **prefix/typeahead only** — query just the autocomplete fields, no word-match ranking clause.\n- `text` → **word matching only** — a single `text` operator over all string-mapped fields (autocomplete fields are matched via their companion `string` mapping).\n\nTo use the same data with a different strategy, declare a second index and select it per request with `$index`.";
|
|
30
|
+
const fuzzyDescription = "Maximum typo tolerance, applied **at query time** to the search operator.\n\n- `0` (default) → no fuzzy matching (exact tokens).\n- `1` → allows small typos (e.g., `\"mongo\"` ≈ `\"mango\"`).\n- `2` → more typo tolerance (e.g., `\"mongodb\"` ≈ `\"mangodb\"`).\n\nAtlas only accepts an edit distance of `1` or `2`; `0` simply disables fuzzy. Can be overridden per request via the `$fuzzy` query control.";
|
|
31
|
+
/**
|
|
32
|
+
* MongoDB-specific annotations.
|
|
33
|
+
*
|
|
34
|
+
* Merged into the global config under `{ db: { mongo: ... } }` so they
|
|
35
|
+
* live alongside core's `@db.table`, `@db.index.*`, etc.
|
|
36
|
+
*
|
|
37
|
+
* Annotations removed (now in core):
|
|
38
|
+
* - `@mongo.index.plain` → use `@db.index.plain`
|
|
39
|
+
* - `@mongo.index.unique` → use `@db.index.unique`
|
|
40
|
+
* - `@db.mongo.index.text` → use `@db.index.fulltext` (with optional weight arg)
|
|
41
|
+
* - `@db.mongo.patch.strategy` → use `@db.patch.strategy`
|
|
42
|
+
* - `@db.mongo.array.uniqueItems` → use `@expect.array.uniqueItems`
|
|
43
|
+
* - `@db.mongo.autoIndexes` → removed (use explicit syncIndexes() calls)
|
|
44
|
+
* - `@db.mongo.search.vector` → use `@db.search.vector` (generic, in @atscript/db/plugin)
|
|
45
|
+
* - `@db.mongo.search.filter` → use `@db.search.filter` (generic, in @atscript/db/plugin)
|
|
46
|
+
*/
|
|
47
|
+
const annotations = {
|
|
48
|
+
collection: new _atscript_core.AnnotationSpec({
|
|
49
|
+
description: "Marks an interface as a **MongoDB collection**.\n\n- Use together with `@db.table \"name\"` which provides the collection name.\n- Automatically injects a **non-optional** `_id` field if not explicitly defined.\n- `_id` must be of type **`string`**, **`number`**, or **`mongo.objectId`**.\n\n**Example:**\n```atscript\n@db.table \"users\"\n@db.mongo.collection\nexport interface User {\n _id: mongo.objectId\n email: string.email\n}\n```\n",
|
|
50
|
+
nodeType: ["interface"],
|
|
51
|
+
validate(token, args, doc) {
|
|
52
|
+
const parent = token.parentNode;
|
|
53
|
+
const struc = parent?.getDefinition();
|
|
54
|
+
const errors = [];
|
|
55
|
+
if ((0, _atscript_core.isInterface)(parent) && parent.props.has("_id") && (0, _atscript_core.isStructure)(struc)) {
|
|
56
|
+
const _id = parent.props.get("_id");
|
|
57
|
+
if (!!_id.token("optional")) errors.push({
|
|
58
|
+
message: `[db.mongo] _id can't be optional in Mongo Collection`,
|
|
59
|
+
severity: 1,
|
|
60
|
+
range: _id.token("identifier").range
|
|
61
|
+
});
|
|
62
|
+
const definition = _id.getDefinition();
|
|
63
|
+
if (!definition) return errors;
|
|
64
|
+
let wrongType = false;
|
|
65
|
+
if ((0, _atscript_core.isRef)(definition)) {
|
|
66
|
+
const def = doc.unwindType(definition.id, definition.chain)?.def;
|
|
67
|
+
if ((0, _atscript_core.isPrimitive)(def) && !["string", "number"].includes(def.config.type)) wrongType = true;
|
|
68
|
+
} else wrongType = true;
|
|
69
|
+
if (wrongType) errors.push({
|
|
70
|
+
message: `[db.mongo] _id must be of type string, number or mongo.objectId`,
|
|
71
|
+
severity: 1,
|
|
72
|
+
range: _id.token("identifier").range
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return errors;
|
|
76
|
+
},
|
|
77
|
+
modify(token, _args, _doc) {
|
|
78
|
+
const parent = token.parentNode;
|
|
79
|
+
const struc = parent?.getDefinition();
|
|
80
|
+
if ((0, _atscript_core.isInterface)(parent) && !parent.props.has("_id") && (0, _atscript_core.isStructure)(struc)) struc.addVirtualProp({
|
|
81
|
+
name: "_id",
|
|
82
|
+
type: "mongo.objectId",
|
|
83
|
+
documentation: "Mongodb Primary Key ObjectId"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
capped: new _atscript_core.AnnotationSpec({
|
|
88
|
+
description: "Creates a **capped collection** with a fixed maximum size.\n\n- Capped collections have fixed size and maintain insertion order.\n- Ideal for logs, event streams, and cache-like data.\n- Changing the cap size requires dropping and recreating the collection — use `@db.sync.method \"drop\"` to allow this.\n\n**Example:**\n```atscript\n@db.table \"logs\"\n@db.mongo.collection\n@db.mongo.capped 10485760, 10000\n@db.sync.method \"drop\"\nexport interface LogEntry {\n message: string\n timestamp: number\n}\n```\n",
|
|
89
|
+
nodeType: ["interface"],
|
|
90
|
+
multiple: false,
|
|
91
|
+
argument: [{
|
|
92
|
+
optional: false,
|
|
93
|
+
name: "size",
|
|
94
|
+
type: "number",
|
|
95
|
+
description: "Maximum size of the collection in **bytes**."
|
|
96
|
+
}, {
|
|
97
|
+
optional: true,
|
|
98
|
+
name: "max",
|
|
99
|
+
type: "number",
|
|
100
|
+
description: "Maximum number of documents in the collection. If omitted, only the byte size limit applies."
|
|
101
|
+
}]
|
|
102
|
+
}),
|
|
103
|
+
search: {
|
|
104
|
+
dynamic: new _atscript_core.AnnotationSpec({
|
|
105
|
+
description: "Creates a **dynamic MongoDB Search Index** that applies to the entire collection.\n\n- **Indexes all text fields automatically** (no need to specify fields).\n- Supports **language analyzers** for text tokenization.\n- Enables **fuzzy search** (typo tolerance) if needed.\n\n**Example:**\n```atscript\n@db.mongo.search.dynamic \"lucene.english\", 1\nexport interface MongoCollection {}\n```\n",
|
|
106
|
+
nodeType: ["interface"],
|
|
107
|
+
multiple: false,
|
|
108
|
+
argument: [{
|
|
109
|
+
optional: true,
|
|
110
|
+
name: "analyzer",
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "The **text analyzer** for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, etc.",
|
|
113
|
+
values: analyzers
|
|
114
|
+
}, {
|
|
115
|
+
optional: true,
|
|
116
|
+
name: "fuzzy",
|
|
117
|
+
type: "number",
|
|
118
|
+
description: fuzzyDescription
|
|
119
|
+
}]
|
|
120
|
+
}),
|
|
121
|
+
static: new _atscript_core.AnnotationSpec({
|
|
122
|
+
description: "Defines a **MongoDB Atlas Search Index** for the collection. The props can refer to this index using `@db.mongo.search.text` annotation.\n\n- **Creates a named search index** for full-text search.\n- **Specify analyzers and fuzzy search** behavior at the index level.\n- **Fields must explicitly use `@db.mongo.search.text`** to be included in this search index.\n\n**Example:**\n```atscript\n@db.mongo.search.static \"lucene.english\", 1, \"mySearchIndex\"\nexport interface MongoCollection {}\n```\n",
|
|
123
|
+
nodeType: ["interface"],
|
|
124
|
+
multiple: true,
|
|
125
|
+
argument: [
|
|
126
|
+
{
|
|
127
|
+
optional: true,
|
|
128
|
+
name: "analyzer",
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
|
|
131
|
+
values: analyzers
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
optional: true,
|
|
135
|
+
name: "fuzzy",
|
|
136
|
+
type: "number",
|
|
137
|
+
description: fuzzyDescription
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
optional: true,
|
|
141
|
+
name: "indexName",
|
|
142
|
+
type: "string",
|
|
143
|
+
description: "The name of the search index. Fields must reference this name using `@db.mongo.search.text` or `@db.mongo.search.autocomplete`. If not set, defaults to `\"DEFAULT\"`."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
optional: true,
|
|
147
|
+
name: "strategy",
|
|
148
|
+
type: "string",
|
|
149
|
+
description: strategyDescription,
|
|
150
|
+
values: searchStrategies
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}),
|
|
154
|
+
text: new _atscript_core.AnnotationSpec({
|
|
155
|
+
description: "Marks a field to be **included in a MongoDB Atlas Search Index** defined by `@db.mongo.search.static`.\n\n- **The field has to reference an existing search index name**.\n- If index name is not defined, a new search index with default attributes will be created.\n\n**Example:**\n```atscript\n@db.mongo.search.text \"lucene.english\", \"mySearchIndex\"\nfirstName: string\n```\n",
|
|
156
|
+
nodeType: ["prop"],
|
|
157
|
+
multiple: true,
|
|
158
|
+
argument: [{
|
|
159
|
+
optional: true,
|
|
160
|
+
name: "analyzer",
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
|
|
163
|
+
values: analyzers
|
|
164
|
+
}, {
|
|
165
|
+
optional: true,
|
|
166
|
+
name: "indexName",
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "The **name of the search index** defined in `@db.mongo.search.static`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
|
|
169
|
+
}]
|
|
170
|
+
}),
|
|
171
|
+
autocomplete: new _atscript_core.AnnotationSpec({
|
|
172
|
+
description: "Marks a field for **prefix / typeahead (as-you-type)** matching in a MongoDB Atlas Search Index.\n\n- Indexes the field as the Atlas **`autocomplete`** type, **and** double-maps it as `string` so exact-word hits still rank.\n- Lets `search()` match partial words: with the default `edgeGram` tokenization, `\"art\"` matches `\"Artem\"` **as you type** (no whole word required).\n- Use `nGram` tokenization for true mid-word (infix/substring) matching at higher index cost.\n- Like `@db.mongo.search.text`, the field joins the index named by `indexName` (or the default index).\n\n**Example:**\n```atscript\n@db.mongo.search.autocomplete \"users\"\nusername: string\n```\n",
|
|
173
|
+
nodeType: ["prop"],
|
|
174
|
+
multiple: true,
|
|
175
|
+
argument: [
|
|
176
|
+
{
|
|
177
|
+
optional: true,
|
|
178
|
+
name: "indexName",
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "The **name of the search index** (defined by `@db.mongo.search.static`) this field joins. If not set, defaults to `\"DEFAULT\"`."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
optional: true,
|
|
184
|
+
name: "tokenization",
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "How the field is tokenized for partial matching:\n\n- `\"edgeGram\"` (default) → **prefix** matching from the start of each word (`\"art\"` → `\"Artem\"`).\n- `\"nGram\"` → **substring/infix** matching anywhere inside a word (`\"tem\"` → `\"Artem\"`); larger index, slower builds.\n- `\"rightEdgeGram\"` → **suffix** matching from the end of each word.",
|
|
187
|
+
values: tokenizations
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
optional: true,
|
|
191
|
+
name: "minGrams",
|
|
192
|
+
type: "number",
|
|
193
|
+
description: "Minimum number of characters per indexed sequence. Defaults to `2`."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
optional: true,
|
|
197
|
+
name: "maxGrams",
|
|
198
|
+
type: "number",
|
|
199
|
+
description: "Maximum number of characters per indexed sequence. Defaults to `15`."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
optional: true,
|
|
203
|
+
name: "foldDiacritics",
|
|
204
|
+
type: "boolean",
|
|
205
|
+
description: "Whether to fold (ignore) diacritics so `\"café\"` matches `\"cafe\"`. Defaults to `true`."
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
optional: true,
|
|
209
|
+
name: "analyzer",
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "The text analyzer for the companion `string` mapping. Defaults to `\"lucene.standard\"`.",
|
|
212
|
+
values: analyzers
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/plugin/primitives.ts
|
|
220
|
+
const primitives = { mongo: { extensions: { objectId: {
|
|
221
|
+
type: "string",
|
|
222
|
+
documentation: "Represents a **MongoDB ObjectId**.\n\n- Stored as a **string** but can be converted to an ObjectId at runtime.\n- Useful for handling `_id` fields and queries that require ObjectId conversion.\n- Automatically converts string `_id` values into **MongoDB ObjectId** when needed.\n\n**Example:**\n```atscript\nuserId: mongo.objectId\n```\n",
|
|
223
|
+
annotations: { "expect.pattern": { pattern: "^[a-fA-F0-9]{24}$" } }
|
|
224
|
+
} } } };
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/plugin/index.ts
|
|
227
|
+
const MongoPlugin = () => ({
|
|
228
|
+
name: "mongo",
|
|
229
|
+
config() {
|
|
230
|
+
return {
|
|
231
|
+
primitives,
|
|
232
|
+
annotations: { db: { mongo: annotations } }
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
//#endregion
|
|
237
|
+
Object.defineProperty(exports, "MongoPlugin", {
|
|
238
|
+
enumerable: true,
|
|
239
|
+
get: function() {
|
|
240
|
+
return MongoPlugin;
|
|
241
|
+
}
|
|
242
|
+
});
|