@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_mongo_filter = require("./mongo-filter-1EpqdD-T.cjs");
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-DBYaF9aH.mjs";
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-1EpqdD-T.cjs");
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(_fd) {
1526
- return true;
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
- canFilterField(_fd: TDbFieldMeta): boolean;
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
- canFilterField(_fd: TDbFieldMeta): boolean;
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-DBYaF9aH.mjs";
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(_fd) {
1525
- return true;
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.103",
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.103"
59
+ "@atscript/db": "^0.1.104"
60
60
  },
61
61
  "scripts": {
62
62
  "postinstall": "asc -f dts",