@atscript/db-mongo 0.1.102 → 0.1.104
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 +123 -4
- package/dist/index.d.cts +24 -4
- package/dist/index.d.mts +24 -4
- package/dist/index.mjs +123 -5
- package/dist/{mongo-filter-DBYaF9aH.mjs → mongo-filter-DceAGI-S.mjs} +9 -0
- package/dist/{mongo-filter-1EpqdD-T.cjs → mongo-filter-z_hLPMyv.cjs} +9 -0
- package/dist/plugin-Bq6hZMBA.cjs +242 -0
- package/dist/plugin-KVFAwoGw.mjs +237 -0
- package/dist/plugin.cjs +3 -238
- package/dist/plugin.d.cts +1 -5
- package/dist/plugin.d.mts +1 -5
- package/dist/plugin.mjs +1 -236
- package/package.json +7 -7
package/dist/agg.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_mongo_filter = require("./mongo-filter-
|
|
2
|
+
const require_mongo_filter = require("./mongo-filter-z_hLPMyv.cjs");
|
|
3
3
|
let _atscript_db_agg = require("@atscript/db/agg");
|
|
4
4
|
//#region src/agg.ts
|
|
5
5
|
/** Simple accumulators that map directly to `{ $<fn>: '$field' }`. */
|
package/dist/agg.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as buildMongoFilter } from "./mongo-filter-
|
|
1
|
+
import { t as buildMongoFilter } from "./mongo-filter-DceAGI-S.mjs";
|
|
2
2
|
import { resolveAlias } from "@atscript/db/agg";
|
|
3
3
|
//#region src/agg.ts
|
|
4
4
|
/** Simple accumulators that map directly to `{ $<fn>: '$field' }`. */
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_mongo_filter = require("./mongo-filter-
|
|
2
|
+
const require_mongo_filter = require("./mongo-filter-z_hLPMyv.cjs");
|
|
3
|
+
const require_plugin = require("./plugin-Bq6hZMBA.cjs");
|
|
3
4
|
let _atscript_db = require("@atscript/db");
|
|
4
5
|
let mongodb = require("mongodb");
|
|
5
6
|
//#region src/lib/projection-dedupe.ts
|
|
@@ -653,6 +654,83 @@ async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
|
653
654
|
const controls = query.controls || {};
|
|
654
655
|
return runSearchWithCountPipeline(host, buildVectorSearchStage(host, vector, indexName, controls.$limit), query, "vectorSearchWithCount", resolveThreshold(host, controls, indexName));
|
|
655
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Field name `$geoNear` writes the computed distance into. Renamed to the
|
|
659
|
+
* public `$distance` pseudo-field after fetch — Mongo field paths cannot
|
|
660
|
+
* start with `$`, so the public name can't be used as `distanceField`.
|
|
661
|
+
*/
|
|
662
|
+
const DISTANCE_FIELD = "__atscript_distance";
|
|
663
|
+
/** Distance-ranked geo search via a leading `$geoNear` aggregation stage. */
|
|
664
|
+
async function geoSearchImpl(host, point, query, indexName) {
|
|
665
|
+
const controls = query.controls || {};
|
|
666
|
+
const pipeline = [buildGeoNearStage(host, point, query, indexName)];
|
|
667
|
+
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
668
|
+
pipeline.push({ $limit: controls.$limit || 1e3 });
|
|
669
|
+
pushGeoProjection(pipeline, query.controls);
|
|
670
|
+
host._log("aggregate (geoSearch)", pipeline);
|
|
671
|
+
return (await wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray())).map((row) => renameDistance(row));
|
|
672
|
+
}
|
|
673
|
+
/** Geo search with faceted count (rows within the distance window). */
|
|
674
|
+
async function geoSearchWithCountImpl(host, point, query, indexName) {
|
|
675
|
+
const controls = query.controls || {};
|
|
676
|
+
const dataStages = [];
|
|
677
|
+
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
678
|
+
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
679
|
+
pushGeoProjection(dataStages, query.controls);
|
|
680
|
+
const pipeline = [buildGeoNearStage(host, point, query, indexName), { $facet: {
|
|
681
|
+
data: dataStages,
|
|
682
|
+
meta: [{ $count: "count" }]
|
|
683
|
+
} }];
|
|
684
|
+
host._log("aggregate (geoSearchWithCount)", pipeline);
|
|
685
|
+
const result = await wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
|
|
686
|
+
return {
|
|
687
|
+
data: (result[0]?.data || []).map((row) => renameDistance(row)),
|
|
688
|
+
count: result[0]?.meta?.[0]?.count || 0
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/** Builds the leading `$geoNear` stage; the filter rides in its `query` option. */
|
|
692
|
+
function buildGeoNearStage(host, point, query, indexName) {
|
|
693
|
+
const controls = query.controls || {};
|
|
694
|
+
const geoNear = {
|
|
695
|
+
near: {
|
|
696
|
+
type: "Point",
|
|
697
|
+
coordinates: point
|
|
698
|
+
},
|
|
699
|
+
distanceField: DISTANCE_FIELD,
|
|
700
|
+
spherical: true,
|
|
701
|
+
key: resolveGeoKeyPath(host, indexName),
|
|
702
|
+
query: require_mongo_filter.buildMongoFilter(query.filter)
|
|
703
|
+
};
|
|
704
|
+
if (typeof controls.$maxDistance === "number") geoNear.maxDistance = controls.$maxDistance;
|
|
705
|
+
if (typeof controls.$minDistance === "number") geoNear.minDistance = controls.$minDistance;
|
|
706
|
+
return { $geoNear: geoNear };
|
|
707
|
+
}
|
|
708
|
+
/** Resolves the physical field path of the targeted geo index. */
|
|
709
|
+
function resolveGeoKeyPath(host, indexName) {
|
|
710
|
+
const geoIndexes = [...host._table.indexes.values()].filter((index) => index.type === "geo");
|
|
711
|
+
const field = (indexName ? geoIndexes.find((candidate) => candidate.name === indexName) : geoIndexes[0])?.fields[0]?.name;
|
|
712
|
+
if (!field) throw new _atscript_db.DbError("GEO_INDEX_MISSING", [{
|
|
713
|
+
path: indexName ?? "",
|
|
714
|
+
message: `No geo index${indexName ? ` "${indexName}"` : ""} on "${host._table.tableName}"`
|
|
715
|
+
}]);
|
|
716
|
+
return field;
|
|
717
|
+
}
|
|
718
|
+
/** Appends a `$project` stage, keeping the computed distance in inclusion mode. */
|
|
719
|
+
function pushGeoProjection(stages, controls) {
|
|
720
|
+
const projection = controls?.$select?.asProjection;
|
|
721
|
+
if (!projection) return;
|
|
722
|
+
const deduped = dedupeProjection(projection);
|
|
723
|
+
if (Object.values(deduped).some((v) => v === 1)) deduped[DISTANCE_FIELD] = 1;
|
|
724
|
+
stages.push({ $project: deduped });
|
|
725
|
+
}
|
|
726
|
+
/** Renames the internal distance field to the public `$distance` pseudo-field. */
|
|
727
|
+
function renameDistance(row) {
|
|
728
|
+
if (DISTANCE_FIELD in row) {
|
|
729
|
+
row.$distance = row[DISTANCE_FIELD];
|
|
730
|
+
delete row[DISTANCE_FIELD];
|
|
731
|
+
}
|
|
732
|
+
return row;
|
|
733
|
+
}
|
|
656
734
|
/** Resolves the effective threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
657
735
|
function resolveThreshold(host, controls, indexName) {
|
|
658
736
|
const queryThreshold = controls.$threshold;
|
|
@@ -1128,6 +1206,9 @@ async function syncIndexesImpl(host) {
|
|
|
1128
1206
|
fields[f.name] = "text";
|
|
1129
1207
|
weights[f.name] = f.weight ?? 1;
|
|
1130
1208
|
}
|
|
1209
|
+
} else if (index.type === "geo") {
|
|
1210
|
+
mongoType = "2dsphere";
|
|
1211
|
+
for (const f of index.fields) fields[f.name] = "2dsphere";
|
|
1131
1212
|
} else {
|
|
1132
1213
|
mongoType = index.type;
|
|
1133
1214
|
for (const f of index.fields) fields[f.name] = 1;
|
|
@@ -1158,7 +1239,8 @@ async function syncIndexesImpl(host) {
|
|
|
1158
1239
|
switch (local.type) {
|
|
1159
1240
|
case "plain":
|
|
1160
1241
|
case "unique":
|
|
1161
|
-
case "text":
|
|
1242
|
+
case "text":
|
|
1243
|
+
case "2dsphere": {
|
|
1162
1244
|
const fieldsMatch = local.type === "text" || objMatch(local.fields, remote.key);
|
|
1163
1245
|
const weightsMatch = objMatch(local.weights || {}, remote.weights || {});
|
|
1164
1246
|
const optionsMatch = local.type === "text" || local.type === "unique" === (remote.unique === true) && partialFilterEqual(local.partialFilterExpression, remote.partialFilterExpression);
|
|
@@ -1199,6 +1281,11 @@ async function syncIndexesImpl(host) {
|
|
|
1199
1281
|
name: key
|
|
1200
1282
|
});
|
|
1201
1283
|
break;
|
|
1284
|
+
case "2dsphere":
|
|
1285
|
+
if (!indexesToCreate.has(key)) continue;
|
|
1286
|
+
host._log("createIndex (2dsphere)", key, value.fields);
|
|
1287
|
+
await host.collection.createIndex(value.fields, { name: key });
|
|
1288
|
+
break;
|
|
1202
1289
|
default:
|
|
1203
1290
|
}
|
|
1204
1291
|
try {
|
|
@@ -1520,9 +1607,31 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1520
1607
|
* filterability blocker the way it is for SQL adapters.
|
|
1521
1608
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
1522
1609
|
* sort-by-min/max-element on arrays is a footgun for generic UI sort headers.
|
|
1610
|
+
*
|
|
1611
|
+
* The `@db.encrypted` veto is core-supplied and absolute: ciphertext
|
|
1612
|
+
* envelopes cannot be filtered, no matter how permissive Mongo is.
|
|
1523
1613
|
*/
|
|
1524
|
-
canFilterField(
|
|
1525
|
-
return
|
|
1614
|
+
canFilterField(fd) {
|
|
1615
|
+
return !fd.encrypted;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Converts `db.geoPoint` tuples to/from GeoJSON Point storage:
|
|
1619
|
+
* write `[lng, lat]` → `{ type: 'Point', coordinates: [lng, lat] }`,
|
|
1620
|
+
* unwrap back to the tuple on read. Non-tuple values (e.g. `$geoWithin`
|
|
1621
|
+
* operator objects flowing through filter translation) pass through.
|
|
1622
|
+
*/
|
|
1623
|
+
formatValue(field) {
|
|
1624
|
+
if (!field.isGeoPoint) return;
|
|
1625
|
+
return {
|
|
1626
|
+
toStorage: (value) => Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number" ? {
|
|
1627
|
+
type: "Point",
|
|
1628
|
+
coordinates: value
|
|
1629
|
+
} : value,
|
|
1630
|
+
fromStorage: (value) => {
|
|
1631
|
+
const v = value;
|
|
1632
|
+
return v && typeof v === "object" && v.type === "Point" && Array.isArray(v.coordinates) ? v.coordinates : value;
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1526
1635
|
}
|
|
1527
1636
|
getAdapterTableName(_type) {}
|
|
1528
1637
|
supportsNativeRelations() {
|
|
@@ -1721,6 +1830,15 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1721
1830
|
async vectorSearchWithCount(vector, query, indexName) {
|
|
1722
1831
|
return vectorSearchWithCountImpl(this, vector, query, indexName);
|
|
1723
1832
|
}
|
|
1833
|
+
isGeoSearchable() {
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
async geoSearch(point, query, indexName) {
|
|
1837
|
+
return geoSearchImpl(this, point, query, indexName);
|
|
1838
|
+
}
|
|
1839
|
+
async geoSearchWithCount(point, query, indexName) {
|
|
1840
|
+
return geoSearchWithCountImpl(this, point, query, indexName);
|
|
1841
|
+
}
|
|
1724
1842
|
async findManyWithCount(query) {
|
|
1725
1843
|
const filter = require_mongo_filter.buildMongoFilter(query.filter);
|
|
1726
1844
|
const controls = query.controls || {};
|
|
@@ -2163,6 +2281,7 @@ function createAdapter(connection, _options) {
|
|
|
2163
2281
|
//#endregion
|
|
2164
2282
|
exports.CollectionPatcher = CollectionPatcher;
|
|
2165
2283
|
exports.MongoAdapter = MongoAdapter;
|
|
2284
|
+
exports.MongoPlugin = require_plugin.MongoPlugin;
|
|
2166
2285
|
exports.buildMongoFilter = require_mongo_filter.buildMongoFilter;
|
|
2167
2286
|
exports.createAdapter = createAdapter;
|
|
2168
2287
|
exports.validateMongoIdPlugin = validateMongoIdPlugin;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as MongoPlugin } from "./index-Bl_O47fp.cjs";
|
|
1
2
|
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertManyResult, TDbInsertResult, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
|
|
2
3
|
import { AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
|
|
3
4
|
import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
|
|
@@ -148,8 +149,8 @@ declare class CollectionPatcher {
|
|
|
148
149
|
interface TPlainIndex {
|
|
149
150
|
key: string;
|
|
150
151
|
name: string;
|
|
151
|
-
type: "plain" | "unique" | "text";
|
|
152
|
-
fields: Record<string, 1 | "text">;
|
|
152
|
+
type: "plain" | "unique" | "text" | "2dsphere";
|
|
153
|
+
fields: Record<string, 1 | "text" | "2dsphere">;
|
|
153
154
|
weights: Record<string, number>;
|
|
154
155
|
/**
|
|
155
156
|
* For "present-only" unique indexes on optional fields: a MongoDB
|
|
@@ -279,8 +280,21 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
279
280
|
* filterability blocker the way it is for SQL adapters.
|
|
280
281
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
281
282
|
* sort-by-min/max-element on arrays is a footgun for generic UI sort headers.
|
|
283
|
+
*
|
|
284
|
+
* The `@db.encrypted` veto is core-supplied and absolute: ciphertext
|
|
285
|
+
* envelopes cannot be filtered, no matter how permissive Mongo is.
|
|
286
|
+
*/
|
|
287
|
+
canFilterField(fd: TDbFieldMeta): boolean;
|
|
288
|
+
/**
|
|
289
|
+
* Converts `db.geoPoint` tuples to/from GeoJSON Point storage:
|
|
290
|
+
* write `[lng, lat]` → `{ type: 'Point', coordinates: [lng, lat] }`,
|
|
291
|
+
* unwrap back to the tuple on read. Non-tuple values (e.g. `$geoWithin`
|
|
292
|
+
* operator objects flowing through filter translation) pass through.
|
|
282
293
|
*/
|
|
283
|
-
|
|
294
|
+
formatValue(field: TDbFieldMeta): {
|
|
295
|
+
toStorage: (value: unknown) => unknown;
|
|
296
|
+
fromStorage: (value: unknown) => unknown;
|
|
297
|
+
} | undefined;
|
|
284
298
|
getAdapterTableName(_type: unknown): string | undefined;
|
|
285
299
|
supportsNativeRelations(): boolean;
|
|
286
300
|
loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[], relations: ReadonlyMap<string, TDbRelation>, foreignKeys: ReadonlyMap<string, TDbForeignKey>, tableResolver?: TTableResolver): Promise<void>;
|
|
@@ -309,6 +323,12 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
309
323
|
data: Array<Record<string, unknown>>;
|
|
310
324
|
count: number;
|
|
311
325
|
}>;
|
|
326
|
+
isGeoSearchable(): boolean;
|
|
327
|
+
geoSearch(point: [number, number], query: DbQuery, indexName?: string): Promise<Record<string, unknown>[]>;
|
|
328
|
+
geoSearchWithCount(point: [number, number], query: DbQuery, indexName?: string): Promise<{
|
|
329
|
+
data: Array<Record<string, unknown>>;
|
|
330
|
+
count: number;
|
|
331
|
+
}>;
|
|
312
332
|
findManyWithCount(query: DbQuery): Promise<{
|
|
313
333
|
data: Array<Record<string, unknown>>;
|
|
314
334
|
count: number;
|
|
@@ -407,4 +427,4 @@ declare const validateMongoIdPlugin: TValidatorPlugin;
|
|
|
407
427
|
//#region src/lib/index.d.ts
|
|
408
428
|
declare function createAdapter(connection: string, _options?: Record<string, unknown>): DbSpace;
|
|
409
429
|
//#endregion
|
|
410
|
-
export { CollectionPatcher, MongoAdapter, TCollectionPatcherContext, type TMongoIndex, type TMongoSearchIndexDefinition, type TPlainIndex, type TSearchIndex, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
|
430
|
+
export { CollectionPatcher, MongoAdapter, MongoPlugin, TCollectionPatcherContext, type TMongoIndex, type TMongoSearchIndexDefinition, type TPlainIndex, type TSearchIndex, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as MongoPlugin } from "./index-Bl_O47fp.mjs";
|
|
1
2
|
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertManyResult, TDbInsertResult, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
|
|
2
3
|
import { AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
|
|
3
4
|
import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
|
|
@@ -148,8 +149,8 @@ declare class CollectionPatcher {
|
|
|
148
149
|
interface TPlainIndex {
|
|
149
150
|
key: string;
|
|
150
151
|
name: string;
|
|
151
|
-
type: "plain" | "unique" | "text";
|
|
152
|
-
fields: Record<string, 1 | "text">;
|
|
152
|
+
type: "plain" | "unique" | "text" | "2dsphere";
|
|
153
|
+
fields: Record<string, 1 | "text" | "2dsphere">;
|
|
153
154
|
weights: Record<string, number>;
|
|
154
155
|
/**
|
|
155
156
|
* For "present-only" unique indexes on optional fields: a MongoDB
|
|
@@ -279,8 +280,21 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
279
280
|
* filterability blocker the way it is for SQL adapters.
|
|
280
281
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
281
282
|
* sort-by-min/max-element on arrays is a footgun for generic UI sort headers.
|
|
283
|
+
*
|
|
284
|
+
* The `@db.encrypted` veto is core-supplied and absolute: ciphertext
|
|
285
|
+
* envelopes cannot be filtered, no matter how permissive Mongo is.
|
|
286
|
+
*/
|
|
287
|
+
canFilterField(fd: TDbFieldMeta): boolean;
|
|
288
|
+
/**
|
|
289
|
+
* Converts `db.geoPoint` tuples to/from GeoJSON Point storage:
|
|
290
|
+
* write `[lng, lat]` → `{ type: 'Point', coordinates: [lng, lat] }`,
|
|
291
|
+
* unwrap back to the tuple on read. Non-tuple values (e.g. `$geoWithin`
|
|
292
|
+
* operator objects flowing through filter translation) pass through.
|
|
282
293
|
*/
|
|
283
|
-
|
|
294
|
+
formatValue(field: TDbFieldMeta): {
|
|
295
|
+
toStorage: (value: unknown) => unknown;
|
|
296
|
+
fromStorage: (value: unknown) => unknown;
|
|
297
|
+
} | undefined;
|
|
284
298
|
getAdapterTableName(_type: unknown): string | undefined;
|
|
285
299
|
supportsNativeRelations(): boolean;
|
|
286
300
|
loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[], relations: ReadonlyMap<string, TDbRelation>, foreignKeys: ReadonlyMap<string, TDbForeignKey>, tableResolver?: TTableResolver): Promise<void>;
|
|
@@ -309,6 +323,12 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
309
323
|
data: Array<Record<string, unknown>>;
|
|
310
324
|
count: number;
|
|
311
325
|
}>;
|
|
326
|
+
isGeoSearchable(): boolean;
|
|
327
|
+
geoSearch(point: [number, number], query: DbQuery, indexName?: string): Promise<Record<string, unknown>[]>;
|
|
328
|
+
geoSearchWithCount(point: [number, number], query: DbQuery, indexName?: string): Promise<{
|
|
329
|
+
data: Array<Record<string, unknown>>;
|
|
330
|
+
count: number;
|
|
331
|
+
}>;
|
|
312
332
|
findManyWithCount(query: DbQuery): Promise<{
|
|
313
333
|
data: Array<Record<string, unknown>>;
|
|
314
334
|
count: number;
|
|
@@ -407,4 +427,4 @@ declare const validateMongoIdPlugin: TValidatorPlugin;
|
|
|
407
427
|
//#region src/lib/index.d.ts
|
|
408
428
|
declare function createAdapter(connection: string, _options?: Record<string, unknown>): DbSpace;
|
|
409
429
|
//#endregion
|
|
410
|
-
export { CollectionPatcher, MongoAdapter, TCollectionPatcherContext, type TMongoIndex, type TMongoSearchIndexDefinition, type TPlainIndex, type TSearchIndex, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
|
430
|
+
export { CollectionPatcher, MongoAdapter, MongoPlugin, TCollectionPatcherContext, type TMongoIndex, type TMongoSearchIndexDefinition, type TPlainIndex, type TSearchIndex, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
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-DceAGI-S.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
|
|
@@ -652,6 +653,83 @@ async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
|
652
653
|
const controls = query.controls || {};
|
|
653
654
|
return runSearchWithCountPipeline(host, buildVectorSearchStage(host, vector, indexName, controls.$limit), query, "vectorSearchWithCount", resolveThreshold(host, controls, indexName));
|
|
654
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Field name `$geoNear` writes the computed distance into. Renamed to the
|
|
658
|
+
* public `$distance` pseudo-field after fetch — Mongo field paths cannot
|
|
659
|
+
* start with `$`, so the public name can't be used as `distanceField`.
|
|
660
|
+
*/
|
|
661
|
+
const DISTANCE_FIELD = "__atscript_distance";
|
|
662
|
+
/** Distance-ranked geo search via a leading `$geoNear` aggregation stage. */
|
|
663
|
+
async function geoSearchImpl(host, point, query, indexName) {
|
|
664
|
+
const controls = query.controls || {};
|
|
665
|
+
const pipeline = [buildGeoNearStage(host, point, query, indexName)];
|
|
666
|
+
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
667
|
+
pipeline.push({ $limit: controls.$limit || 1e3 });
|
|
668
|
+
pushGeoProjection(pipeline, query.controls);
|
|
669
|
+
host._log("aggregate (geoSearch)", pipeline);
|
|
670
|
+
return (await wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray())).map((row) => renameDistance(row));
|
|
671
|
+
}
|
|
672
|
+
/** Geo search with faceted count (rows within the distance window). */
|
|
673
|
+
async function geoSearchWithCountImpl(host, point, query, indexName) {
|
|
674
|
+
const controls = query.controls || {};
|
|
675
|
+
const dataStages = [];
|
|
676
|
+
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
677
|
+
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
678
|
+
pushGeoProjection(dataStages, query.controls);
|
|
679
|
+
const pipeline = [buildGeoNearStage(host, point, query, indexName), { $facet: {
|
|
680
|
+
data: dataStages,
|
|
681
|
+
meta: [{ $count: "count" }]
|
|
682
|
+
} }];
|
|
683
|
+
host._log("aggregate (geoSearchWithCount)", pipeline);
|
|
684
|
+
const result = await wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
|
|
685
|
+
return {
|
|
686
|
+
data: (result[0]?.data || []).map((row) => renameDistance(row)),
|
|
687
|
+
count: result[0]?.meta?.[0]?.count || 0
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/** Builds the leading `$geoNear` stage; the filter rides in its `query` option. */
|
|
691
|
+
function buildGeoNearStage(host, point, query, indexName) {
|
|
692
|
+
const controls = query.controls || {};
|
|
693
|
+
const geoNear = {
|
|
694
|
+
near: {
|
|
695
|
+
type: "Point",
|
|
696
|
+
coordinates: point
|
|
697
|
+
},
|
|
698
|
+
distanceField: DISTANCE_FIELD,
|
|
699
|
+
spherical: true,
|
|
700
|
+
key: resolveGeoKeyPath(host, indexName),
|
|
701
|
+
query: buildMongoFilter(query.filter)
|
|
702
|
+
};
|
|
703
|
+
if (typeof controls.$maxDistance === "number") geoNear.maxDistance = controls.$maxDistance;
|
|
704
|
+
if (typeof controls.$minDistance === "number") geoNear.minDistance = controls.$minDistance;
|
|
705
|
+
return { $geoNear: geoNear };
|
|
706
|
+
}
|
|
707
|
+
/** Resolves the physical field path of the targeted geo index. */
|
|
708
|
+
function resolveGeoKeyPath(host, indexName) {
|
|
709
|
+
const geoIndexes = [...host._table.indexes.values()].filter((index) => index.type === "geo");
|
|
710
|
+
const field = (indexName ? geoIndexes.find((candidate) => candidate.name === indexName) : geoIndexes[0])?.fields[0]?.name;
|
|
711
|
+
if (!field) throw new DbError("GEO_INDEX_MISSING", [{
|
|
712
|
+
path: indexName ?? "",
|
|
713
|
+
message: `No geo index${indexName ? ` "${indexName}"` : ""} on "${host._table.tableName}"`
|
|
714
|
+
}]);
|
|
715
|
+
return field;
|
|
716
|
+
}
|
|
717
|
+
/** Appends a `$project` stage, keeping the computed distance in inclusion mode. */
|
|
718
|
+
function pushGeoProjection(stages, controls) {
|
|
719
|
+
const projection = controls?.$select?.asProjection;
|
|
720
|
+
if (!projection) return;
|
|
721
|
+
const deduped = dedupeProjection(projection);
|
|
722
|
+
if (Object.values(deduped).some((v) => v === 1)) deduped[DISTANCE_FIELD] = 1;
|
|
723
|
+
stages.push({ $project: deduped });
|
|
724
|
+
}
|
|
725
|
+
/** Renames the internal distance field to the public `$distance` pseudo-field. */
|
|
726
|
+
function renameDistance(row) {
|
|
727
|
+
if (DISTANCE_FIELD in row) {
|
|
728
|
+
row.$distance = row[DISTANCE_FIELD];
|
|
729
|
+
delete row[DISTANCE_FIELD];
|
|
730
|
+
}
|
|
731
|
+
return row;
|
|
732
|
+
}
|
|
655
733
|
/** Resolves the effective threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
656
734
|
function resolveThreshold(host, controls, indexName) {
|
|
657
735
|
const queryThreshold = controls.$threshold;
|
|
@@ -1127,6 +1205,9 @@ async function syncIndexesImpl(host) {
|
|
|
1127
1205
|
fields[f.name] = "text";
|
|
1128
1206
|
weights[f.name] = f.weight ?? 1;
|
|
1129
1207
|
}
|
|
1208
|
+
} else if (index.type === "geo") {
|
|
1209
|
+
mongoType = "2dsphere";
|
|
1210
|
+
for (const f of index.fields) fields[f.name] = "2dsphere";
|
|
1130
1211
|
} else {
|
|
1131
1212
|
mongoType = index.type;
|
|
1132
1213
|
for (const f of index.fields) fields[f.name] = 1;
|
|
@@ -1157,7 +1238,8 @@ async function syncIndexesImpl(host) {
|
|
|
1157
1238
|
switch (local.type) {
|
|
1158
1239
|
case "plain":
|
|
1159
1240
|
case "unique":
|
|
1160
|
-
case "text":
|
|
1241
|
+
case "text":
|
|
1242
|
+
case "2dsphere": {
|
|
1161
1243
|
const fieldsMatch = local.type === "text" || objMatch(local.fields, remote.key);
|
|
1162
1244
|
const weightsMatch = objMatch(local.weights || {}, remote.weights || {});
|
|
1163
1245
|
const optionsMatch = local.type === "text" || local.type === "unique" === (remote.unique === true) && partialFilterEqual(local.partialFilterExpression, remote.partialFilterExpression);
|
|
@@ -1198,6 +1280,11 @@ async function syncIndexesImpl(host) {
|
|
|
1198
1280
|
name: key
|
|
1199
1281
|
});
|
|
1200
1282
|
break;
|
|
1283
|
+
case "2dsphere":
|
|
1284
|
+
if (!indexesToCreate.has(key)) continue;
|
|
1285
|
+
host._log("createIndex (2dsphere)", key, value.fields);
|
|
1286
|
+
await host.collection.createIndex(value.fields, { name: key });
|
|
1287
|
+
break;
|
|
1201
1288
|
default:
|
|
1202
1289
|
}
|
|
1203
1290
|
try {
|
|
@@ -1519,9 +1606,31 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1519
1606
|
* filterability blocker the way it is for SQL adapters.
|
|
1520
1607
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
1521
1608
|
* sort-by-min/max-element on arrays is a footgun for generic UI sort headers.
|
|
1609
|
+
*
|
|
1610
|
+
* The `@db.encrypted` veto is core-supplied and absolute: ciphertext
|
|
1611
|
+
* envelopes cannot be filtered, no matter how permissive Mongo is.
|
|
1522
1612
|
*/
|
|
1523
|
-
canFilterField(
|
|
1524
|
-
return
|
|
1613
|
+
canFilterField(fd) {
|
|
1614
|
+
return !fd.encrypted;
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Converts `db.geoPoint` tuples to/from GeoJSON Point storage:
|
|
1618
|
+
* write `[lng, lat]` → `{ type: 'Point', coordinates: [lng, lat] }`,
|
|
1619
|
+
* unwrap back to the tuple on read. Non-tuple values (e.g. `$geoWithin`
|
|
1620
|
+
* operator objects flowing through filter translation) pass through.
|
|
1621
|
+
*/
|
|
1622
|
+
formatValue(field) {
|
|
1623
|
+
if (!field.isGeoPoint) return;
|
|
1624
|
+
return {
|
|
1625
|
+
toStorage: (value) => Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number" ? {
|
|
1626
|
+
type: "Point",
|
|
1627
|
+
coordinates: value
|
|
1628
|
+
} : value,
|
|
1629
|
+
fromStorage: (value) => {
|
|
1630
|
+
const v = value;
|
|
1631
|
+
return v && typeof v === "object" && v.type === "Point" && Array.isArray(v.coordinates) ? v.coordinates : value;
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1525
1634
|
}
|
|
1526
1635
|
getAdapterTableName(_type) {}
|
|
1527
1636
|
supportsNativeRelations() {
|
|
@@ -1720,6 +1829,15 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1720
1829
|
async vectorSearchWithCount(vector, query, indexName) {
|
|
1721
1830
|
return vectorSearchWithCountImpl(this, vector, query, indexName);
|
|
1722
1831
|
}
|
|
1832
|
+
isGeoSearchable() {
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
async geoSearch(point, query, indexName) {
|
|
1836
|
+
return geoSearchImpl(this, point, query, indexName);
|
|
1837
|
+
}
|
|
1838
|
+
async geoSearchWithCount(point, query, indexName) {
|
|
1839
|
+
return geoSearchWithCountImpl(this, point, query, indexName);
|
|
1840
|
+
}
|
|
1723
1841
|
async findManyWithCount(query) {
|
|
1724
1842
|
const filter = buildMongoFilter(query.filter);
|
|
1725
1843
|
const controls = query.controls || {};
|
|
@@ -2160,4 +2278,4 @@ function createAdapter(connection, _options) {
|
|
|
2160
2278
|
return new DbSpace(() => new MongoAdapter(db, client));
|
|
2161
2279
|
}
|
|
2162
2280
|
//#endregion
|
|
2163
|
-
export { CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
|
2281
|
+
export { CollectionPatcher, MongoAdapter, MongoPlugin, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
|
@@ -17,6 +17,11 @@ function parseRegexString(value) {
|
|
|
17
17
|
flags: ""
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Earth radius in meters used by MongoDB's `$centerSphere` radians conversion
|
|
22
|
+
* (Mongo documents dividing by 6378.1 km).
|
|
23
|
+
*/
|
|
24
|
+
const EARTH_RADIUS_M = 6378100;
|
|
20
25
|
const mongoVisitor = {
|
|
21
26
|
comparison(field, op, value) {
|
|
22
27
|
if (op === "$eq") return { [field]: value };
|
|
@@ -27,6 +32,10 @@ const mongoVisitor = {
|
|
|
27
32
|
$options: flags
|
|
28
33
|
} } : { [field]: { $regex: pattern } };
|
|
29
34
|
}
|
|
35
|
+
if (op === "$geoWithin") {
|
|
36
|
+
const { center, radius } = value;
|
|
37
|
+
return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
|
|
38
|
+
}
|
|
30
39
|
return { [field]: { [op]: value } };
|
|
31
40
|
},
|
|
32
41
|
and(children) {
|
|
@@ -17,6 +17,11 @@ function parseRegexString(value) {
|
|
|
17
17
|
flags: ""
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Earth radius in meters used by MongoDB's `$centerSphere` radians conversion
|
|
22
|
+
* (Mongo documents dividing by 6378.1 km).
|
|
23
|
+
*/
|
|
24
|
+
const EARTH_RADIUS_M = 6378100;
|
|
20
25
|
const mongoVisitor = {
|
|
21
26
|
comparison(field, op, value) {
|
|
22
27
|
if (op === "$eq") return { [field]: value };
|
|
@@ -27,6 +32,10 @@ const mongoVisitor = {
|
|
|
27
32
|
$options: flags
|
|
28
33
|
} } : { [field]: { $regex: pattern } };
|
|
29
34
|
}
|
|
35
|
+
if (op === "$geoWithin") {
|
|
36
|
+
const { center, radius } = value;
|
|
37
|
+
return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
|
|
38
|
+
}
|
|
30
39
|
return { [field]: { [op]: value } };
|
|
31
40
|
},
|
|
32
41
|
and(children) {
|