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