@atscript/moost-db 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/index.cjs CHANGED
@@ -980,6 +980,25 @@ const QUERY_CONTROLS = [
980
980
  ];
981
981
  const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
982
982
  const ONE_CONTROLS = dtoControls(GetOneControlsDto);
983
+ /**
984
+ * Controls accepted by the `/geo` endpoint. No backing DTO — `$center` /
985
+ * `$maxDistance` / `$minDistance` are parsed and validated by the handler.
986
+ */
987
+ const GEO_CONTROLS = [
988
+ "filter",
989
+ "insights",
990
+ "center",
991
+ "maxDistance",
992
+ "minDistance",
993
+ "index",
994
+ "select",
995
+ "skip",
996
+ "limit",
997
+ "page",
998
+ "size",
999
+ "with",
1000
+ "actions"
1001
+ ];
983
1002
  //#endregion
984
1003
  //#region src/as-db-readable.controller.ts
985
1004
  let AsDbReadableController = class AsDbReadableController extends AsReadableController {
@@ -1413,6 +1432,82 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1413
1432
  };
1414
1433
  }
1415
1434
  /**
1435
+ * **GET /geo** — distance-ranked geospatial search (mirrors the search /
1436
+ * vector read endpoints; geo-index spec §7).
1437
+ *
1438
+ * URL controls: `$center=lng,lat` (required), `$maxDistance` / `$minDistance`
1439
+ * (meters), `$index` (geo index name), plus the standard filter / `$select` /
1440
+ * `$with` / pagination syntax. Each row carries a computed `$distance`
1441
+ * (meters). With `$page` / `$size` the response is the `/pages` envelope;
1442
+ * otherwise a plain row array (`$skip` / `$limit` compose).
1443
+ */
1444
+ async geo(url) {
1445
+ const parsed = this.parseQueryString(url);
1446
+ const controls = parsed.controls;
1447
+ this._coerceActionsControl(controls);
1448
+ const point = this._parseGeoCenter(controls.$center);
1449
+ if (point instanceof _moostjs_event_http.HttpError) return point;
1450
+ for (const key of ["$maxDistance", "$minDistance"]) if (controls[key] !== void 0) {
1451
+ const num = Number(controls[key]);
1452
+ if (!Number.isFinite(num) || num < 0) return new _moostjs_event_http.HttpError(400, `${key} must be a non-negative number of meters`);
1453
+ controls[key] = num;
1454
+ }
1455
+ const indexName = typeof controls.$index === "string" ? controls.$index : void 0;
1456
+ if (parsed.insights) {
1457
+ const insightsError = this.validateInsights(parsed.insights);
1458
+ if (insightsError) return new _moostjs_event_http.HttpError(400, insightsError);
1459
+ }
1460
+ const gateError = this.checkGates(parsed.filter, controls, this._gates);
1461
+ if (gateError) return gateError;
1462
+ const [filter, rawSelect] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
1463
+ const select = this.widenPreferredIdProjection(rawSelect);
1464
+ if (select instanceof _moostjs_event_http.HttpError) return select;
1465
+ const paginated = controls.$page !== void 0 || controls.$size !== void 0;
1466
+ const page = Math.max(Number(controls.$page || 1), 1);
1467
+ const size = Math.max(Number(controls.$size || 10), 1);
1468
+ const queryObj = {
1469
+ filter,
1470
+ controls: {
1471
+ ...controls,
1472
+ $center: void 0,
1473
+ $index: void 0,
1474
+ $select: select,
1475
+ ...paginated ? {
1476
+ $skip: (page - 1) * size,
1477
+ $limit: size
1478
+ } : { $limit: controls.$limit || 1e3 }
1479
+ }
1480
+ };
1481
+ if (paginated) {
1482
+ const result = await this._runReadWithActions(queryObj, controls, select, async (q) => indexName ? this.readable.geoSearchWithCount(indexName, point, q) : this.readable.geoSearchWithCount(point, q));
1483
+ return {
1484
+ data: result.data,
1485
+ page,
1486
+ itemsPerPage: size,
1487
+ pages: Math.ceil(result.count / size),
1488
+ count: result.count
1489
+ };
1490
+ }
1491
+ return (await this._runReadWithActions(queryObj, controls, select, async (q) => ({ data: await (indexName ? this.readable.geoSearch(indexName, point, q) : this.readable.geoSearch(point, q)) }))).data;
1492
+ }
1493
+ /** Parses the `$center` control: `"lng,lat"` string (or tuple) → `[number, number]`. */
1494
+ _parseGeoCenter(raw) {
1495
+ let lng;
1496
+ let lat;
1497
+ if (typeof raw === "string") {
1498
+ const parts = raw.split(",");
1499
+ if (parts.length === 2) {
1500
+ lng = Number(parts[0]);
1501
+ lat = Number(parts[1]);
1502
+ }
1503
+ } else if (Array.isArray(raw) && raw.length === 2) {
1504
+ lng = Number(raw[0]);
1505
+ lat = Number(raw[1]);
1506
+ }
1507
+ if (lng === void 0 || lat === void 0 || !Number.isFinite(lng) || !Number.isFinite(lat)) return new _moostjs_event_http.HttpError(400, "$center is required: $center=lng,lat (GeoJSON order)");
1508
+ return [lng, lat];
1509
+ }
1510
+ /**
1416
1511
  * **GET /one/:id** — retrieves a single record by ID or unique property.
1417
1512
  * The id-filter is AND-combined with {@link transformOne} so row-level
1418
1513
  * read overlays gate `/one` symmetrically with `/query` / `/pages`.
@@ -1486,6 +1581,10 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1486
1581
  });
1487
1582
  const filterableMode = this.readable.type.metadata.get("db.table.filterable") === "manual";
1488
1583
  const sortableMode = this.readable.type.metadata.get("db.table.sortable") === "manual";
1584
+ const geoIndexedPhysical = /* @__PURE__ */ new Set();
1585
+ if (this.readable.indexes instanceof Map) {
1586
+ for (const index of this.readable.indexes.values()) if (index.type === "geo") for (const f of index.fields) geoIndexedPhysical.add(f.name);
1587
+ }
1489
1588
  const fields = {};
1490
1589
  for (const fd of this.readable.fieldDescriptors) {
1491
1590
  if (fd.ignored) continue;
@@ -1499,10 +1598,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1499
1598
  sortable: adapterCanSort && (sortableMode ? annotatedSortable : !!fd.isIndexed),
1500
1599
  filterable: adapterCanFilter && (filterableMode ? annotatedFilterable : true)
1501
1600
  };
1601
+ if (fd.encrypted) fields[fd.path].encrypted = true;
1602
+ if (geoIndexedPhysical.has(fd.physicalName)) fields[fd.path].geo = true;
1502
1603
  }
1503
1604
  return {
1504
1605
  searchable: this.readable.isSearchable(),
1505
1606
  vectorSearchable: this.readable.isVectorSearchable(),
1607
+ geoSearchable: this._isGeoSearchable(),
1506
1608
  searchIndexes: this.readable.getSearchIndexes(),
1507
1609
  primaryKeys: [...this.readable.primaryKeys],
1508
1610
  preferredId: [...this.readable.preferredId],
@@ -1519,9 +1621,17 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1519
1621
  ...super.buildCrud(),
1520
1622
  query: [...QUERY_CONTROLS],
1521
1623
  pages: [...PAGES_CONTROLS],
1522
- one: [...ONE_CONTROLS]
1624
+ one: [...ONE_CONTROLS],
1625
+ ...this._isGeoSearchable() ? { geo: [...GEO_CONTROLS] } : {}
1523
1626
  };
1524
1627
  }
1628
+ /** Adapter supports geo search AND the table declares at least one geo index. */
1629
+ _isGeoSearchable() {
1630
+ if (typeof this.readable.isGeoSearchable !== "function" || !this.readable.isGeoSearchable()) return false;
1631
+ if (!(this.readable.indexes instanceof Map)) return false;
1632
+ for (const index of this.readable.indexes.values()) if (index.type === "geo") return true;
1633
+ return false;
1634
+ }
1525
1635
  };
1526
1636
  __decorate([
1527
1637
  (0, _moostjs_event_http.Get)("query"),
@@ -1537,6 +1647,13 @@ __decorate([
1537
1647
  __decorateMetadata("design:paramtypes", [String]),
1538
1648
  __decorateMetadata("design:returntype", Promise)
1539
1649
  ], AsDbReadableController.prototype, "pages", null);
1650
+ __decorate([
1651
+ (0, _moostjs_event_http.Get)("geo"),
1652
+ __decorateParam(0, (0, _moostjs_event_http.Url)()),
1653
+ __decorateMetadata("design:type", Function),
1654
+ __decorateMetadata("design:paramtypes", [String]),
1655
+ __decorateMetadata("design:returntype", Promise)
1656
+ ], AsDbReadableController.prototype, "geo", null);
1540
1657
  __decorate([
1541
1658
  (0, _moostjs_event_http.Get)("one/:id"),
1542
1659
  __decorateParam(0, (0, moost.Param)("id")),
package/dist/index.d.cts CHANGED
@@ -263,6 +263,25 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
263
263
  pages: number;
264
264
  count: number;
265
265
  } | HttpError>;
266
+ /**
267
+ * **GET /geo** — distance-ranked geospatial search (mirrors the search /
268
+ * vector read endpoints; geo-index spec §7).
269
+ *
270
+ * URL controls: `$center=lng,lat` (required), `$maxDistance` / `$minDistance`
271
+ * (meters), `$index` (geo index name), plus the standard filter / `$select` /
272
+ * `$with` / pagination syntax. Each row carries a computed `$distance`
273
+ * (meters). With `$page` / `$size` the response is the `/pages` envelope;
274
+ * otherwise a plain row array (`$skip` / `$limit` compose).
275
+ */
276
+ geo(url: string): Promise<DataType[] | {
277
+ data: DataType[];
278
+ page: number;
279
+ itemsPerPage: number;
280
+ pages: number;
281
+ count: number;
282
+ } | HttpError>;
283
+ /** Parses the `$center` control: `"lng,lat"` string (or tuple) → `[number, number]`. */
284
+ private _parseGeoCenter;
266
285
  /**
267
286
  * **GET /one/:id** — retrieves a single record by ID or unique property.
268
287
  * The id-filter is AND-combined with {@link transformOne} so row-level
@@ -285,6 +304,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
285
304
  */
286
305
  protected buildMetaResponse(): TMetaResponse;
287
306
  protected buildCrud(): TCrudPermissions$1;
307
+ /** Adapter supports geo search AND the table declares at least one geo index. */
308
+ private _isGeoSearchable;
288
309
  }
289
310
  //#endregion
290
311
  //#region src/as-db.controller.d.ts
package/dist/index.d.mts CHANGED
@@ -263,6 +263,25 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
263
263
  pages: number;
264
264
  count: number;
265
265
  } | HttpError>;
266
+ /**
267
+ * **GET /geo** — distance-ranked geospatial search (mirrors the search /
268
+ * vector read endpoints; geo-index spec §7).
269
+ *
270
+ * URL controls: `$center=lng,lat` (required), `$maxDistance` / `$minDistance`
271
+ * (meters), `$index` (geo index name), plus the standard filter / `$select` /
272
+ * `$with` / pagination syntax. Each row carries a computed `$distance`
273
+ * (meters). With `$page` / `$size` the response is the `/pages` envelope;
274
+ * otherwise a plain row array (`$skip` / `$limit` compose).
275
+ */
276
+ geo(url: string): Promise<DataType[] | {
277
+ data: DataType[];
278
+ page: number;
279
+ itemsPerPage: number;
280
+ pages: number;
281
+ count: number;
282
+ } | HttpError>;
283
+ /** Parses the `$center` control: `"lng,lat"` string (or tuple) → `[number, number]`. */
284
+ private _parseGeoCenter;
266
285
  /**
267
286
  * **GET /one/:id** — retrieves a single record by ID or unique property.
268
287
  * The id-filter is AND-combined with {@link transformOne} so row-level
@@ -285,6 +304,8 @@ declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscrip
285
304
  */
286
305
  protected buildMetaResponse(): TMetaResponse;
287
306
  protected buildCrud(): TCrudPermissions$1;
307
+ /** Adapter supports geo search AND the table declares at least one geo index. */
308
+ private _isGeoSearchable;
288
309
  }
289
310
  //#endregion
290
311
  //#region src/as-db.controller.d.ts
package/dist/index.mjs CHANGED
@@ -979,6 +979,25 @@ const QUERY_CONTROLS = [
979
979
  ];
980
980
  const PAGES_CONTROLS = ["filter", ...dtoControls(PagesControlsDto)];
981
981
  const ONE_CONTROLS = dtoControls(GetOneControlsDto);
982
+ /**
983
+ * Controls accepted by the `/geo` endpoint. No backing DTO — `$center` /
984
+ * `$maxDistance` / `$minDistance` are parsed and validated by the handler.
985
+ */
986
+ const GEO_CONTROLS = [
987
+ "filter",
988
+ "insights",
989
+ "center",
990
+ "maxDistance",
991
+ "minDistance",
992
+ "index",
993
+ "select",
994
+ "skip",
995
+ "limit",
996
+ "page",
997
+ "size",
998
+ "with",
999
+ "actions"
1000
+ ];
982
1001
  //#endregion
983
1002
  //#region src/as-db-readable.controller.ts
984
1003
  let AsDbReadableController = class AsDbReadableController extends AsReadableController {
@@ -1412,6 +1431,82 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1412
1431
  };
1413
1432
  }
1414
1433
  /**
1434
+ * **GET /geo** — distance-ranked geospatial search (mirrors the search /
1435
+ * vector read endpoints; geo-index spec §7).
1436
+ *
1437
+ * URL controls: `$center=lng,lat` (required), `$maxDistance` / `$minDistance`
1438
+ * (meters), `$index` (geo index name), plus the standard filter / `$select` /
1439
+ * `$with` / pagination syntax. Each row carries a computed `$distance`
1440
+ * (meters). With `$page` / `$size` the response is the `/pages` envelope;
1441
+ * otherwise a plain row array (`$skip` / `$limit` compose).
1442
+ */
1443
+ async geo(url) {
1444
+ const parsed = this.parseQueryString(url);
1445
+ const controls = parsed.controls;
1446
+ this._coerceActionsControl(controls);
1447
+ const point = this._parseGeoCenter(controls.$center);
1448
+ if (point instanceof HttpError) return point;
1449
+ for (const key of ["$maxDistance", "$minDistance"]) if (controls[key] !== void 0) {
1450
+ const num = Number(controls[key]);
1451
+ if (!Number.isFinite(num) || num < 0) return new HttpError(400, `${key} must be a non-negative number of meters`);
1452
+ controls[key] = num;
1453
+ }
1454
+ const indexName = typeof controls.$index === "string" ? controls.$index : void 0;
1455
+ if (parsed.insights) {
1456
+ const insightsError = this.validateInsights(parsed.insights);
1457
+ if (insightsError) return new HttpError(400, insightsError);
1458
+ }
1459
+ const gateError = this.checkGates(parsed.filter, controls, this._gates);
1460
+ if (gateError) return gateError;
1461
+ const [filter, rawSelect] = await Promise.all([this.transformFilter(parsed.filter), this.transformProjection(controls.$select)]);
1462
+ const select = this.widenPreferredIdProjection(rawSelect);
1463
+ if (select instanceof HttpError) return select;
1464
+ const paginated = controls.$page !== void 0 || controls.$size !== void 0;
1465
+ const page = Math.max(Number(controls.$page || 1), 1);
1466
+ const size = Math.max(Number(controls.$size || 10), 1);
1467
+ const queryObj = {
1468
+ filter,
1469
+ controls: {
1470
+ ...controls,
1471
+ $center: void 0,
1472
+ $index: void 0,
1473
+ $select: select,
1474
+ ...paginated ? {
1475
+ $skip: (page - 1) * size,
1476
+ $limit: size
1477
+ } : { $limit: controls.$limit || 1e3 }
1478
+ }
1479
+ };
1480
+ if (paginated) {
1481
+ const result = await this._runReadWithActions(queryObj, controls, select, async (q) => indexName ? this.readable.geoSearchWithCount(indexName, point, q) : this.readable.geoSearchWithCount(point, q));
1482
+ return {
1483
+ data: result.data,
1484
+ page,
1485
+ itemsPerPage: size,
1486
+ pages: Math.ceil(result.count / size),
1487
+ count: result.count
1488
+ };
1489
+ }
1490
+ return (await this._runReadWithActions(queryObj, controls, select, async (q) => ({ data: await (indexName ? this.readable.geoSearch(indexName, point, q) : this.readable.geoSearch(point, q)) }))).data;
1491
+ }
1492
+ /** Parses the `$center` control: `"lng,lat"` string (or tuple) → `[number, number]`. */
1493
+ _parseGeoCenter(raw) {
1494
+ let lng;
1495
+ let lat;
1496
+ if (typeof raw === "string") {
1497
+ const parts = raw.split(",");
1498
+ if (parts.length === 2) {
1499
+ lng = Number(parts[0]);
1500
+ lat = Number(parts[1]);
1501
+ }
1502
+ } else if (Array.isArray(raw) && raw.length === 2) {
1503
+ lng = Number(raw[0]);
1504
+ lat = Number(raw[1]);
1505
+ }
1506
+ if (lng === void 0 || lat === void 0 || !Number.isFinite(lng) || !Number.isFinite(lat)) return new HttpError(400, "$center is required: $center=lng,lat (GeoJSON order)");
1507
+ return [lng, lat];
1508
+ }
1509
+ /**
1415
1510
  * **GET /one/:id** — retrieves a single record by ID or unique property.
1416
1511
  * The id-filter is AND-combined with {@link transformOne} so row-level
1417
1512
  * read overlays gate `/one` symmetrically with `/query` / `/pages`.
@@ -1485,6 +1580,10 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1485
1580
  });
1486
1581
  const filterableMode = this.readable.type.metadata.get("db.table.filterable") === "manual";
1487
1582
  const sortableMode = this.readable.type.metadata.get("db.table.sortable") === "manual";
1583
+ const geoIndexedPhysical = /* @__PURE__ */ new Set();
1584
+ if (this.readable.indexes instanceof Map) {
1585
+ for (const index of this.readable.indexes.values()) if (index.type === "geo") for (const f of index.fields) geoIndexedPhysical.add(f.name);
1586
+ }
1488
1587
  const fields = {};
1489
1588
  for (const fd of this.readable.fieldDescriptors) {
1490
1589
  if (fd.ignored) continue;
@@ -1498,10 +1597,13 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1498
1597
  sortable: adapterCanSort && (sortableMode ? annotatedSortable : !!fd.isIndexed),
1499
1598
  filterable: adapterCanFilter && (filterableMode ? annotatedFilterable : true)
1500
1599
  };
1600
+ if (fd.encrypted) fields[fd.path].encrypted = true;
1601
+ if (geoIndexedPhysical.has(fd.physicalName)) fields[fd.path].geo = true;
1501
1602
  }
1502
1603
  return {
1503
1604
  searchable: this.readable.isSearchable(),
1504
1605
  vectorSearchable: this.readable.isVectorSearchable(),
1606
+ geoSearchable: this._isGeoSearchable(),
1505
1607
  searchIndexes: this.readable.getSearchIndexes(),
1506
1608
  primaryKeys: [...this.readable.primaryKeys],
1507
1609
  preferredId: [...this.readable.preferredId],
@@ -1518,9 +1620,17 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1518
1620
  ...super.buildCrud(),
1519
1621
  query: [...QUERY_CONTROLS],
1520
1622
  pages: [...PAGES_CONTROLS],
1521
- one: [...ONE_CONTROLS]
1623
+ one: [...ONE_CONTROLS],
1624
+ ...this._isGeoSearchable() ? { geo: [...GEO_CONTROLS] } : {}
1522
1625
  };
1523
1626
  }
1627
+ /** Adapter supports geo search AND the table declares at least one geo index. */
1628
+ _isGeoSearchable() {
1629
+ if (typeof this.readable.isGeoSearchable !== "function" || !this.readable.isGeoSearchable()) return false;
1630
+ if (!(this.readable.indexes instanceof Map)) return false;
1631
+ for (const index of this.readable.indexes.values()) if (index.type === "geo") return true;
1632
+ return false;
1633
+ }
1524
1634
  };
1525
1635
  __decorate([
1526
1636
  Get("query"),
@@ -1536,6 +1646,13 @@ __decorate([
1536
1646
  __decorateMetadata("design:paramtypes", [String]),
1537
1647
  __decorateMetadata("design:returntype", Promise)
1538
1648
  ], AsDbReadableController.prototype, "pages", null);
1649
+ __decorate([
1650
+ Get("geo"),
1651
+ __decorateParam(0, Url()),
1652
+ __decorateMetadata("design:type", Function),
1653
+ __decorateMetadata("design:paramtypes", [String]),
1654
+ __decorateMetadata("design:returntype", Promise)
1655
+ ], AsDbReadableController.prototype, "geo", null);
1539
1656
  __decorate([
1540
1657
  Get("one/:id"),
1541
1658
  __decorateParam(0, Param("id")),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.103",
3
+ "version": "0.1.104",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -58,7 +58,7 @@
58
58
  "@wooksjs/event-core": "^0.7.19",
59
59
  "@wooksjs/http-body": "^0.7.19",
60
60
  "moost": "^0.6.26",
61
- "@atscript/db": "^0.1.103"
61
+ "@atscript/db": "^0.1.104"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",