@atscript/db-mongo 0.1.103 → 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.cjs +121 -4
- package/dist/index.d.cts +22 -3
- package/dist/index.d.mts +22 -3
- package/dist/index.mjs +121 -4
- 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/package.json +2 -2
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,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
|
const require_plugin = require("./plugin-Bq6hZMBA.cjs");
|
|
4
4
|
let _atscript_db = require("@atscript/db");
|
|
5
5
|
let mongodb = require("mongodb");
|
|
@@ -654,6 +654,83 @@ async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
|
654
654
|
const controls = query.controls || {};
|
|
655
655
|
return runSearchWithCountPipeline(host, buildVectorSearchStage(host, vector, indexName, controls.$limit), query, "vectorSearchWithCount", resolveThreshold(host, controls, indexName));
|
|
656
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
|
+
}
|
|
657
734
|
/** Resolves the effective threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
658
735
|
function resolveThreshold(host, controls, indexName) {
|
|
659
736
|
const queryThreshold = controls.$threshold;
|
|
@@ -1129,6 +1206,9 @@ async function syncIndexesImpl(host) {
|
|
|
1129
1206
|
fields[f.name] = "text";
|
|
1130
1207
|
weights[f.name] = f.weight ?? 1;
|
|
1131
1208
|
}
|
|
1209
|
+
} else if (index.type === "geo") {
|
|
1210
|
+
mongoType = "2dsphere";
|
|
1211
|
+
for (const f of index.fields) fields[f.name] = "2dsphere";
|
|
1132
1212
|
} else {
|
|
1133
1213
|
mongoType = index.type;
|
|
1134
1214
|
for (const f of index.fields) fields[f.name] = 1;
|
|
@@ -1159,7 +1239,8 @@ async function syncIndexesImpl(host) {
|
|
|
1159
1239
|
switch (local.type) {
|
|
1160
1240
|
case "plain":
|
|
1161
1241
|
case "unique":
|
|
1162
|
-
case "text":
|
|
1242
|
+
case "text":
|
|
1243
|
+
case "2dsphere": {
|
|
1163
1244
|
const fieldsMatch = local.type === "text" || objMatch(local.fields, remote.key);
|
|
1164
1245
|
const weightsMatch = objMatch(local.weights || {}, remote.weights || {});
|
|
1165
1246
|
const optionsMatch = local.type === "text" || local.type === "unique" === (remote.unique === true) && partialFilterEqual(local.partialFilterExpression, remote.partialFilterExpression);
|
|
@@ -1200,6 +1281,11 @@ async function syncIndexesImpl(host) {
|
|
|
1200
1281
|
name: key
|
|
1201
1282
|
});
|
|
1202
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;
|
|
1203
1289
|
default:
|
|
1204
1290
|
}
|
|
1205
1291
|
try {
|
|
@@ -1521,9 +1607,31 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1521
1607
|
* filterability blocker the way it is for SQL adapters.
|
|
1522
1608
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
1523
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.
|
|
1524
1613
|
*/
|
|
1525
|
-
canFilterField(
|
|
1526
|
-
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
|
+
};
|
|
1527
1635
|
}
|
|
1528
1636
|
getAdapterTableName(_type) {}
|
|
1529
1637
|
supportsNativeRelations() {
|
|
@@ -1722,6 +1830,15 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1722
1830
|
async vectorSearchWithCount(vector, query, indexName) {
|
|
1723
1831
|
return vectorSearchWithCountImpl(this, vector, query, indexName);
|
|
1724
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
|
+
}
|
|
1725
1842
|
async findManyWithCount(query) {
|
|
1726
1843
|
const filter = require_mongo_filter.buildMongoFilter(query.filter);
|
|
1727
1844
|
const controls = query.controls || {};
|
package/dist/index.d.cts
CHANGED
|
@@ -149,8 +149,8 @@ declare class CollectionPatcher {
|
|
|
149
149
|
interface TPlainIndex {
|
|
150
150
|
key: string;
|
|
151
151
|
name: string;
|
|
152
|
-
type: "plain" | "unique" | "text";
|
|
153
|
-
fields: Record<string, 1 | "text">;
|
|
152
|
+
type: "plain" | "unique" | "text" | "2dsphere";
|
|
153
|
+
fields: Record<string, 1 | "text" | "2dsphere">;
|
|
154
154
|
weights: Record<string, number>;
|
|
155
155
|
/**
|
|
156
156
|
* For "present-only" unique indexes on optional fields: a MongoDB
|
|
@@ -280,8 +280,21 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
280
280
|
* filterability blocker the way it is for SQL adapters.
|
|
281
281
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
282
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.
|
|
283
293
|
*/
|
|
284
|
-
|
|
294
|
+
formatValue(field: TDbFieldMeta): {
|
|
295
|
+
toStorage: (value: unknown) => unknown;
|
|
296
|
+
fromStorage: (value: unknown) => unknown;
|
|
297
|
+
} | undefined;
|
|
285
298
|
getAdapterTableName(_type: unknown): string | undefined;
|
|
286
299
|
supportsNativeRelations(): boolean;
|
|
287
300
|
loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[], relations: ReadonlyMap<string, TDbRelation>, foreignKeys: ReadonlyMap<string, TDbForeignKey>, tableResolver?: TTableResolver): Promise<void>;
|
|
@@ -310,6 +323,12 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
310
323
|
data: Array<Record<string, unknown>>;
|
|
311
324
|
count: number;
|
|
312
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
|
+
}>;
|
|
313
332
|
findManyWithCount(query: DbQuery): Promise<{
|
|
314
333
|
data: Array<Record<string, unknown>>;
|
|
315
334
|
count: number;
|
package/dist/index.d.mts
CHANGED
|
@@ -149,8 +149,8 @@ declare class CollectionPatcher {
|
|
|
149
149
|
interface TPlainIndex {
|
|
150
150
|
key: string;
|
|
151
151
|
name: string;
|
|
152
|
-
type: "plain" | "unique" | "text";
|
|
153
|
-
fields: Record<string, 1 | "text">;
|
|
152
|
+
type: "plain" | "unique" | "text" | "2dsphere";
|
|
153
|
+
fields: Record<string, 1 | "text" | "2dsphere">;
|
|
154
154
|
weights: Record<string, number>;
|
|
155
155
|
/**
|
|
156
156
|
* For "present-only" unique indexes on optional fields: a MongoDB
|
|
@@ -280,8 +280,21 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
280
280
|
* filterability blocker the way it is for SQL adapters.
|
|
281
281
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
282
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.
|
|
283
293
|
*/
|
|
284
|
-
|
|
294
|
+
formatValue(field: TDbFieldMeta): {
|
|
295
|
+
toStorage: (value: unknown) => unknown;
|
|
296
|
+
fromStorage: (value: unknown) => unknown;
|
|
297
|
+
} | undefined;
|
|
285
298
|
getAdapterTableName(_type: unknown): string | undefined;
|
|
286
299
|
supportsNativeRelations(): boolean;
|
|
287
300
|
loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[], relations: ReadonlyMap<string, TDbRelation>, foreignKeys: ReadonlyMap<string, TDbForeignKey>, tableResolver?: TTableResolver): Promise<void>;
|
|
@@ -310,6 +323,12 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
310
323
|
data: Array<Record<string, unknown>>;
|
|
311
324
|
count: number;
|
|
312
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
|
+
}>;
|
|
313
332
|
findManyWithCount(query: DbQuery): Promise<{
|
|
314
333
|
data: Array<Record<string, unknown>>;
|
|
315
334
|
count: number;
|
package/dist/index.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 { t as MongoPlugin } from "./plugin-KVFAwoGw.mjs";
|
|
3
3
|
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, computeInsights, getDbFieldOp, getKeyProps, resolveDesignType } from "@atscript/db";
|
|
4
4
|
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
@@ -653,6 +653,83 @@ async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
|
653
653
|
const controls = query.controls || {};
|
|
654
654
|
return runSearchWithCountPipeline(host, buildVectorSearchStage(host, vector, indexName, controls.$limit), query, "vectorSearchWithCount", resolveThreshold(host, controls, indexName));
|
|
655
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
|
+
}
|
|
656
733
|
/** Resolves the effective threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
657
734
|
function resolveThreshold(host, controls, indexName) {
|
|
658
735
|
const queryThreshold = controls.$threshold;
|
|
@@ -1128,6 +1205,9 @@ async function syncIndexesImpl(host) {
|
|
|
1128
1205
|
fields[f.name] = "text";
|
|
1129
1206
|
weights[f.name] = f.weight ?? 1;
|
|
1130
1207
|
}
|
|
1208
|
+
} else if (index.type === "geo") {
|
|
1209
|
+
mongoType = "2dsphere";
|
|
1210
|
+
for (const f of index.fields) fields[f.name] = "2dsphere";
|
|
1131
1211
|
} else {
|
|
1132
1212
|
mongoType = index.type;
|
|
1133
1213
|
for (const f of index.fields) fields[f.name] = 1;
|
|
@@ -1158,7 +1238,8 @@ async function syncIndexesImpl(host) {
|
|
|
1158
1238
|
switch (local.type) {
|
|
1159
1239
|
case "plain":
|
|
1160
1240
|
case "unique":
|
|
1161
|
-
case "text":
|
|
1241
|
+
case "text":
|
|
1242
|
+
case "2dsphere": {
|
|
1162
1243
|
const fieldsMatch = local.type === "text" || objMatch(local.fields, remote.key);
|
|
1163
1244
|
const weightsMatch = objMatch(local.weights || {}, remote.weights || {});
|
|
1164
1245
|
const optionsMatch = local.type === "text" || local.type === "unique" === (remote.unique === true) && partialFilterEqual(local.partialFilterExpression, remote.partialFilterExpression);
|
|
@@ -1199,6 +1280,11 @@ async function syncIndexesImpl(host) {
|
|
|
1199
1280
|
name: key
|
|
1200
1281
|
});
|
|
1201
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;
|
|
1202
1288
|
default:
|
|
1203
1289
|
}
|
|
1204
1290
|
try {
|
|
@@ -1520,9 +1606,31 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1520
1606
|
* filterability blocker the way it is for SQL adapters.
|
|
1521
1607
|
* `canSortField` keeps the conservative default (no sort on JSON storage):
|
|
1522
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.
|
|
1523
1612
|
*/
|
|
1524
|
-
canFilterField(
|
|
1525
|
-
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
|
+
};
|
|
1526
1634
|
}
|
|
1527
1635
|
getAdapterTableName(_type) {}
|
|
1528
1636
|
supportsNativeRelations() {
|
|
@@ -1721,6 +1829,15 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1721
1829
|
async vectorSearchWithCount(vector, query, indexName) {
|
|
1722
1830
|
return vectorSearchWithCountImpl(this, vector, query, indexName);
|
|
1723
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
|
+
}
|
|
1724
1841
|
async findManyWithCount(query) {
|
|
1725
1842
|
const filter = buildMongoFilter(query.filter);
|
|
1726
1843
|
const controls = query.controls || {};
|
|
@@ -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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mongo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.104",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@atscript/core": "^0.1.75",
|
|
57
57
|
"@atscript/typescript": "^0.1.75",
|
|
58
58
|
"mongodb": "^6.17.0",
|
|
59
|
-
"@atscript/db": "^0.1.
|
|
59
|
+
"@atscript/db": "^0.1.104"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"postinstall": "asc -f dts",
|