@geolonia/geonicdb-sdk 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -176,6 +176,57 @@ await db.connect();
176
176
  | `reconnecting` | `{ attempt, delay }` | Auto-reconnect starting |
177
177
  | `error` | `Error` | Error occurred |
178
178
  | `tokenRefresh` | `RefreshedCredentials` | Bearer token was refreshed |
179
+ | `cacheHit` | `CacheEvent` | Request served from in-memory cache (304 received) |
180
+ | `cacheMiss` | `CacheEvent` | Cache miss — fresh response fetched from origin |
181
+ | `cacheInvalidated` | `CacheEvent` | Cache entry dropped (e.g. after a WebSocket entity event) |
182
+
183
+ ## Client-side Cache & Polling
184
+
185
+ The SDK includes an in-memory cache that handles `ETag` / `If-None-Match`
186
+ negotiation transparently. Subsequent reads of an unchanged resource are
187
+ served as `304 Not Modified` — the SDK presents them to your code as a normal
188
+ `200` response with the cached body.
189
+
190
+ ```typescript
191
+ // First call: fresh fetch, ETag stored
192
+ const rooms1 = await db.getEntities({ type: 'Room' });
193
+
194
+ // Second call: server returns 304, SDK returns the cached body
195
+ const rooms2 = await db.getEntities({ type: 'Room' });
196
+
197
+ // Concurrent calls to the same path are deduplicated:
198
+ const [a, b, c] = await Promise.all([
199
+ db.getEntities({ type: 'Room' }),
200
+ db.getEntities({ type: 'Room' }),
201
+ db.getEntities({ type: 'Room' }),
202
+ ]); // → only one HTTP request hits the network
203
+ ```
204
+
205
+ Cache is enabled by default; pass `cache: false` to opt out.
206
+
207
+ ### Polling
208
+
209
+ `db.poll(params, options)` repeats `getEntities()` at an interval and reports
210
+ back whether the data actually changed. Internally it leverages the cache's
211
+ ETag negotiation, so unchanged ticks transfer almost no bytes.
212
+
213
+ ```typescript
214
+ const handle = db.poll({ type: 'Room' }, {
215
+ interval: 5000,
216
+ onData: (rooms) => render(rooms),
217
+ onNoChange: () => {}, // server returned 304
218
+ onError: (err) => console.error(err),
219
+ });
220
+
221
+ // Stop later
222
+ handle.stop();
223
+ ```
224
+
225
+ ### WebSocket-driven cache invalidation
226
+
227
+ When the SDK receives `entityCreated` / `entityUpdated` / `entityDeleted`
228
+ WebSocket events, it drops affected cache entries automatically. The next
229
+ read re-validates with the server.
179
230
 
180
231
  ## API Reference
181
232
 
@@ -194,6 +245,8 @@ await db.connect();
194
245
  | `baseUrl` | `string` | No | API base URL (auto-detected from script `src` if omitted) |
195
246
  | `wsEndpoint` | `string` | No | WebSocket endpoint URL (auto-detected from `baseUrl` if omitted) |
196
247
  | `debug` | `boolean` | No | Enable debug logging to console (default: `false`) |
248
+ | `cache` | `boolean` | No | Enable in-memory cache with ETag/304 + request dedup (default: `true`) |
249
+ | `cacheMaxEntries` | `number` | No | LRU cache capacity when `cache` is enabled (default: `1000`) |
197
250
 
198
251
  ### Authentication
199
252
 
@@ -278,6 +331,35 @@ await db.connect();
278
331
  | `request(method, path, body?)` | `string, string, unknown` | `Promise<unknown>` | Authenticated request with automatic JSON parsing |
279
332
  | `requestRaw(method, path, body?)` | `string, string, unknown` | `Promise<Response>` | Authenticated request returning raw `Response` (for accessing headers) |
280
333
 
334
+ ### Cache & Polling
335
+
336
+ | Method | Parameters | Returns | Description |
337
+ |--------|-----------|---------|-------------|
338
+ | `poll(params, options)` | `GetEntitiesParams \| undefined, PollOptions<T>` | `PollHandle` | ETag-based polling — fires `onData` only when the data actually changes. `options` is **required** (`onData` must be specified to receive data) |
339
+ | `clearCache()` | — | `void` | Drop all cached responses |
340
+
341
+ #### `PollOptions<T>`
342
+
343
+ | Property | Type | Description |
344
+ |----------|------|-------------|
345
+ | `interval` | `number` | Polling interval in milliseconds (default: `5000`) |
346
+ | `onData` | `(data: T) => void` | Called whenever the server returns fresh data (200) |
347
+ | `onNoChange` | `() => void` | Called when the server returns 304 Not Modified |
348
+ | `onError` | `(err: Error) => void` | Called on any fetch/parse failure |
349
+
350
+ #### `PollHandle`
351
+
352
+ | Method | Parameters | Returns | Description |
353
+ |--------|-----------|---------|-------------|
354
+ | `stop()` | — | `void` | Stop the polling timer |
355
+
356
+ #### `CacheEvent`
357
+
358
+ | Property | Type | Description |
359
+ |----------|------|-------------|
360
+ | `key` | `string` | Cache key (`METHOD:path`) |
361
+ | `path` | `string` | Request path |
362
+
281
363
  ### WebSocket
282
364
 
283
365
  | Method | Parameters | Returns | Description |
package/geonicdb.cjs CHANGED
@@ -180,6 +180,8 @@ var RECONNECT_MAX_ATTEMPTS = 10;
180
180
  var RECONNECT_BASE_MS = 1e3;
181
181
  var RECONNECT_MAX_DELAY_MS = 3e4;
182
182
  var SUB_PROTOCOL = "access_token";
183
+ var SDK_CACHE_MAX_ENTRIES_DEFAULT = 1e3;
184
+ var SDK_POLL_INTERVAL_MS_DEFAULT = 5e3;
183
185
 
184
186
  // src/sdk/errors.ts
185
187
  var GeonicDBError = class extends Error {
@@ -256,6 +258,93 @@ function createErrorFromResponse(status, body, fallbackMessage) {
256
258
  }
257
259
  }
258
260
 
261
+ // src/sdk/cache.ts
262
+ var SdkCache = class {
263
+ constructor(maxEntries = SDK_CACHE_MAX_ENTRIES_DEFAULT) {
264
+ __publicField(this, "_store", /* @__PURE__ */ new Map());
265
+ __publicField(this, "_maxEntries");
266
+ if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
267
+ throw new Error(`SdkCache maxEntries must be a positive integer, got ${String(maxEntries)}`);
268
+ }
269
+ this._maxEntries = maxEntries;
270
+ }
271
+ /** Build a stable cache key. Method is upper-cased so casing differences do not produce duplicate entries. */
272
+ static keyFor(method, path) {
273
+ return `${method.toUpperCase()}:${path}`;
274
+ }
275
+ /** Returns the entry and bumps it to most-recent position, or `undefined`. */
276
+ get(key) {
277
+ const entry = this._store.get(key);
278
+ if (!entry) return void 0;
279
+ this._store.delete(key);
280
+ this._store.set(key, entry);
281
+ return entry;
282
+ }
283
+ /** Insert or replace an entry. Evicts the LRU entry when over capacity. */
284
+ set(key, entry) {
285
+ if (this._store.has(key)) {
286
+ this._store.delete(key);
287
+ }
288
+ this._store.set(key, entry);
289
+ while (this._store.size > this._maxEntries) {
290
+ const oldest = this._store.keys().next().value;
291
+ if (oldest === void 0) break;
292
+ this._store.delete(oldest);
293
+ }
294
+ }
295
+ /** Delete a specific entry. Returns whether anything was removed. */
296
+ delete(key) {
297
+ return this._store.delete(key);
298
+ }
299
+ /**
300
+ * Delete every entry whose key matches a predicate. Returns the array of
301
+ * removed keys so callers can emit `cacheInvalidated` events.
302
+ */
303
+ deleteWhere(predicate) {
304
+ const removed = [];
305
+ for (const [key, entry] of this._store) {
306
+ if (predicate(key, entry)) {
307
+ this._store.delete(key);
308
+ removed.push(key);
309
+ }
310
+ }
311
+ return removed;
312
+ }
313
+ /** Drop everything. */
314
+ clear() {
315
+ this._store.clear();
316
+ }
317
+ /** Current entry count. Useful for tests / metrics. */
318
+ size() {
319
+ return this._store.size;
320
+ }
321
+ };
322
+ var HEADERS_TO_PERSIST = [
323
+ "content-type",
324
+ "etag",
325
+ "last-modified",
326
+ "cache-control",
327
+ "vary",
328
+ "link",
329
+ "ngsild-results-count",
330
+ "fiware-total-count",
331
+ "x-total-count",
332
+ "ngsild-next",
333
+ "fiware-next-token"
334
+ ];
335
+ function snapshotHeaders(headers) {
336
+ const out = {};
337
+ for (const name of HEADERS_TO_PERSIST) {
338
+ const value = headers.get(name);
339
+ if (value !== null) out[name] = value;
340
+ }
341
+ return out;
342
+ }
343
+ function isCacheableMethod(method) {
344
+ const upper = method.toUpperCase();
345
+ return upper === "GET" || upper === "HEAD";
346
+ }
347
+
259
348
  // src/sdk/auth.ts
260
349
  var _AuthManager = class _AuthManager {
261
350
  constructor(baseUrl, apiKey, tenant, debug = false) {
@@ -273,6 +362,16 @@ var _AuthManager = class _AuthManager {
273
362
  __publicField(this, "_tokenPromise", null);
274
363
  /** Callback to emit tokenRefresh events. Set by GeonicDB class. */
275
364
  __publicField(this, "onTokenRefresh", null);
365
+ /**
366
+ * SDK-level cache (#991 Phase A). When set, cacheable GET requests are
367
+ * served via ETag/304 with automatic If-None-Match negotiation, and
368
+ * concurrent requests to the same path are deduplicated.
369
+ */
370
+ __publicField(this, "_cache", null);
371
+ /** Emitter for cacheHit / cacheMiss / cacheInvalidated events. */
372
+ __publicField(this, "_emitCacheEvent", null);
373
+ /** In-flight request map keyed by `${METHOD}:${path}` for request deduplication. */
374
+ __publicField(this, "_inFlight", /* @__PURE__ */ new Map());
276
375
  this._baseUrl = baseUrl;
277
376
  this._apiKey = apiKey;
278
377
  this._tenant = tenant;
@@ -288,6 +387,22 @@ var _AuthManager = class _AuthManager {
288
387
  _log(...args) {
289
388
  if (this._debug) console.log("[GeonicDB]", ...args);
290
389
  }
390
+ /** Wire an SdkCache instance (called by GeonicDB constructor when caching is enabled). */
391
+ setCache(cache) {
392
+ this._cache = cache;
393
+ }
394
+ /** Provide the cache event emitter (forwarded to the GeonicDB EventEmitter). */
395
+ setCacheEventEmitter(emitter) {
396
+ this._emitCacheEvent = emitter;
397
+ }
398
+ /** Expose the cache for invalidation (e.g. WebSocket entity events). */
399
+ getCache() {
400
+ return this._cache;
401
+ }
402
+ /** Emit a cache event if a listener is wired. */
403
+ emitCacheEvent(name, payload) {
404
+ this._emitCacheEvent?.(name, payload);
405
+ }
291
406
  /** Login with email and password (Bearer JWT). */
292
407
  async login(email, password) {
293
408
  this._log("login", email);
@@ -311,6 +426,7 @@ var _AuthManager = class _AuthManager {
311
426
  this._tokenExpiry = Date.now() + (data.expiresIn - 60) * 1e3;
312
427
  this._tokenType = "Bearer";
313
428
  this._refreshToken = data.refreshToken;
429
+ this._invalidateAuthScopedState();
314
430
  return data;
315
431
  }
316
432
  /**
@@ -325,6 +441,7 @@ var _AuthManager = class _AuthManager {
325
441
  this._tokenExpiry = opts.expiresIn != null ? Date.now() + (opts.expiresIn - 60) * 1e3 : Date.now() + DEFAULT_TOKEN_TTL_SEC * 1e3;
326
442
  this._refreshToken = opts.refreshToken || null;
327
443
  this._tokenPromise = null;
444
+ this._invalidateAuthScopedState();
328
445
  }
329
446
  /** Clear all credentials. */
330
447
  logout() {
@@ -332,6 +449,17 @@ var _AuthManager = class _AuthManager {
332
449
  this._tokenExpiry = 0;
333
450
  this._refreshToken = null;
334
451
  this._tokenPromise = null;
452
+ this._invalidateAuthScopedState();
453
+ }
454
+ /**
455
+ * Drop everything that may have been associated with the previous auth
456
+ * context. Called on `login()` (after token assignment), `setCredentials()`,
457
+ * and `logout()`. Without this, a cached body or in-flight Response from
458
+ * user A could be returned to user B after a credentials swap.
459
+ */
460
+ _invalidateAuthScopedState() {
461
+ this._cache?.clear();
462
+ this._inFlight.clear();
335
463
  }
336
464
  /** Ensure a valid token is available, refreshing or acquiring as needed. */
337
465
  async ensureToken() {
@@ -451,15 +579,89 @@ var _AuthManager = class _AuthManager {
451
579
  }
452
580
  /**
453
581
  * Make an authenticated HTTP request with automatic token refresh and DPoP.
582
+ *
583
+ * When the SDK cache is enabled and the request is cacheable
584
+ * (GET / HEAD without a body), this method:
585
+ * - Sends `If-None-Match` / `If-Modified-Since` derived from a previously
586
+ * cached entry, if any.
587
+ * - Returns the cached body wrapped in a synthesized `200` Response when
588
+ * the server replies `304 Not Modified`.
589
+ * - Persists fresh `200` responses (with their ETag/Last-Modified) into
590
+ * the cache.
591
+ * - Deduplicates concurrent in-flight requests to the same path.
454
592
  */
455
593
  async request(method, path, body) {
456
594
  this._log(method, path);
595
+ if (!this._cache || !isCacheableMethod(method) || body !== void 0) {
596
+ const token = await this.ensureToken();
597
+ const res = await this._doAuthenticatedRequest(method, path, body, token);
598
+ this._log(method, path, "\u2192", res.status);
599
+ return res;
600
+ }
601
+ const key = SdkCache.keyFor(method, path);
602
+ const inFlight = this._inFlight.get(key);
603
+ if (inFlight) {
604
+ return inFlight.then((res) => res.clone());
605
+ }
606
+ const promise = this._cachedRequest(method, path, key);
607
+ this._inFlight.set(key, promise);
608
+ try {
609
+ return await promise;
610
+ } finally {
611
+ this._inFlight.delete(key);
612
+ }
613
+ }
614
+ async _cachedRequest(method, path, key) {
615
+ const cache = this._cache;
616
+ if (!cache) {
617
+ const token2 = await this.ensureToken();
618
+ return this._doAuthenticatedRequest(method, path, void 0, token2);
619
+ }
620
+ const cached = cache.get(key);
621
+ const conditional = {};
622
+ if (cached?.etag) conditional["If-None-Match"] = cached.etag;
623
+ if (cached?.lastModified) conditional["If-Modified-Since"] = cached.lastModified;
457
624
  const token = await this.ensureToken();
458
- const res = await this._doAuthenticatedRequest(method, path, body, token);
625
+ const res = await this._doAuthenticatedRequest(method, path, void 0, token, 0, conditional);
459
626
  this._log(method, path, "\u2192", res.status);
627
+ if (res.status === 304 && cached) {
628
+ const refreshedHeaders = { ...cached.headers, ...snapshotHeaders(res.headers) };
629
+ const refreshedEtag = res.headers.get("etag") ?? cached.etag;
630
+ const refreshedLastModified = res.headers.get("last-modified") ?? cached.lastModified;
631
+ cache.set(key, {
632
+ ...cached,
633
+ etag: refreshedEtag,
634
+ lastModified: refreshedLastModified,
635
+ headers: refreshedHeaders,
636
+ cachedAt: Date.now()
637
+ });
638
+ this.emitCacheEvent("cacheHit", { key, path });
639
+ const body = typeof cached.data === "string" ? cached.data : JSON.stringify(cached.data);
640
+ return new Response(body, { status: 200, headers: refreshedHeaders });
641
+ }
642
+ if (res.status === 200) {
643
+ const etag = res.headers.get("etag") ?? void 0;
644
+ const lastModified = res.headers.get("last-modified") ?? void 0;
645
+ if (etag || lastModified) {
646
+ const text = await res.clone().text();
647
+ let data = text;
648
+ try {
649
+ data = JSON.parse(text);
650
+ } catch {
651
+ }
652
+ cache.set(key, {
653
+ etag,
654
+ lastModified,
655
+ data,
656
+ headers: snapshotHeaders(res.headers),
657
+ cachedAt: Date.now()
658
+ });
659
+ this.emitCacheEvent("cacheMiss", { key, path });
660
+ }
661
+ }
460
662
  return res;
461
663
  }
462
- async _doAuthenticatedRequest(method, path, body, token, retryCount = 0) {
664
+ async _doAuthenticatedRequest(method, path, body, token, retryCount = 0, extraHeaders = {}) {
463
665
  const url = this._baseUrl + path;
464
666
  const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair;
465
667
  const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
@@ -475,7 +677,8 @@ var _AuthManager = class _AuthManager {
475
677
  const headers = {
476
678
  Authorization: (reqIsDPoP ? "DPoP " : "Bearer ") + reqToken,
477
679
  "Content-Type": "application/ld+json",
478
- Accept: "application/ld+json"
680
+ Accept: "application/ld+json",
681
+ ...extraHeaders
479
682
  };
480
683
  if (dpopProof) headers["DPoP"] = dpopProof;
481
684
  if (this._tenant) headers["Fiware-Service"] = this._tenant;
@@ -511,7 +714,7 @@ var _AuthManager = class _AuthManager {
511
714
  if (retryNonce) this._dpopNonce = retryNonce;
512
715
  const delay = parseInt(res.headers.get("Retry-After") || "1", 10) * 1e3;
513
716
  await new Promise((resolve) => setTimeout(resolve, delay));
514
- return this._doAuthenticatedRequest(method, path, body, currentToken, retryCount + 1);
717
+ return this._doAuthenticatedRequest(method, path, body, currentToken, retryCount + 1, extraHeaders);
515
718
  }
516
719
  return res;
517
720
  }
@@ -787,14 +990,50 @@ var GeonicDB = class extends EventEmitter {
787
990
  this.onTokenRefresh?.(creds);
788
991
  this.emit("tokenRefresh", creds);
789
992
  };
993
+ if (opts.cache !== false) {
994
+ const cache = new SdkCache(opts.cacheMaxEntries ?? SDK_CACHE_MAX_ENTRIES_DEFAULT);
995
+ this._auth.setCache(cache);
996
+ this._auth.setCacheEventEmitter((name, payload) => this.emit(name, payload));
997
+ }
790
998
  this._ws = new WebSocketManager(
791
999
  this._auth,
792
1000
  baseUrl,
793
1001
  tenant,
794
- (event, data) => this.emit(event, data),
1002
+ (event, data) => {
1003
+ this.emit(event, data);
1004
+ if (event === "entityCreated" || event === "entityUpdated" || event === "entityDeleted") {
1005
+ this._invalidateCacheForEntityEvent(data);
1006
+ }
1007
+ },
795
1008
  opts.wsEndpoint
796
1009
  );
797
1010
  }
1011
+ /**
1012
+ * Drop cache entries that may have been affected by an entity change.
1013
+ *
1014
+ * The SDK does not know which lists exactly contain the entity, so the
1015
+ * conservative strategy is to drop all NGSI-LD / NGSIv2 entity-related
1016
+ * paths. ETag re-validation on the next read brings the data back in one
1017
+ * round trip with `If-None-Match`, so this remains efficient even when
1018
+ * over-invalidating.
1019
+ */
1020
+ _invalidateCacheForEntityEvent(event) {
1021
+ const cache = this._auth.getCache();
1022
+ if (!cache) return;
1023
+ const entityId = event?.entityId;
1024
+ const removed = cache.deleteWhere((key) => {
1025
+ if (key.includes("/ngsi-ld/v1/entities") || key.includes("/v2/entities")) return true;
1026
+ if (entityId && key.includes(entityId)) return true;
1027
+ return false;
1028
+ });
1029
+ for (const key of removed) {
1030
+ this._auth.emitCacheEvent("cacheInvalidated", { key, path: key.split(":").slice(1).join(":") });
1031
+ }
1032
+ }
1033
+ /** Drop every cached response. Useful in tests and on manual auth changes. */
1034
+ clearCache() {
1035
+ this._auth.getCache()?.clear();
1036
+ }
798
1037
  // --- Authentication ---
799
1038
  /** Login with email and password (Bearer JWT). */
800
1039
  async login(email, password) {
@@ -837,25 +1076,82 @@ var GeonicDB = class extends EventEmitter {
837
1076
  }
838
1077
  /** Query entities with optional filters. */
839
1078
  async getEntities(params) {
840
- let qs = "";
841
- if (params) {
842
- const parts = [];
843
- if (params.type) parts.push("type=" + encodeURIComponent(params.type));
844
- if (params.limit != null) parts.push("limit=" + params.limit);
845
- if (params.offset != null) parts.push("offset=" + params.offset);
846
- if (params.q) parts.push("q=" + encodeURIComponent(params.q));
847
- if (parts.length) qs = "?" + parts.join("&");
848
- }
849
- const res = await this._auth.request(
850
- "GET",
851
- "/ngsi-ld/v1/entities" + qs
852
- );
1079
+ const res = await this._auth.request("GET", buildEntitiesPath(params));
853
1080
  if (!res.ok) {
854
1081
  const e = await res.json().catch(() => ({}));
855
1082
  throw createErrorFromResponse(res.status, e, "Query failed");
856
1083
  }
857
1084
  return await res.json();
858
1085
  }
1086
+ /**
1087
+ * Poll for entity list changes using ETag-based revalidation (#991 Phase A).
1088
+ *
1089
+ * The handle's `stop()` ends the loop. Each tick performs a normal
1090
+ * `getEntities()` call; the SDK's cache layer issues `If-None-Match`
1091
+ * automatically. The poll detects "no change" by comparing the previous
1092
+ * ETag with the response's ETag (the cache exposes the cached ETag on 304
1093
+ * replays as well, so the comparison stays valid in both 200 and 304
1094
+ * paths).
1095
+ *
1096
+ * @example
1097
+ * ```typescript
1098
+ * const handle = db.poll({ type: 'Room' }, {
1099
+ * interval: 5000,
1100
+ * onData: (rooms) => render(rooms),
1101
+ * onNoChange: () => {},
1102
+ * });
1103
+ * // ...later
1104
+ * handle.stop();
1105
+ * ```
1106
+ */
1107
+ poll(params, options) {
1108
+ const interval = options.interval ?? SDK_POLL_INTERVAL_MS_DEFAULT;
1109
+ if (!Number.isFinite(interval) || interval <= 0) {
1110
+ throw new Error(`poll interval must be a positive number (got ${String(interval)})`);
1111
+ }
1112
+ let stopped = false;
1113
+ let timer = null;
1114
+ let prevEtag;
1115
+ let prevPath;
1116
+ const buildPath = () => buildEntitiesPath(params);
1117
+ const tick = async () => {
1118
+ if (stopped) return;
1119
+ try {
1120
+ const path = prevPath ?? (prevPath = buildPath());
1121
+ const res = await this._auth.request("GET", path);
1122
+ if (stopped) return;
1123
+ const etag = res.headers.get("etag") ?? void 0;
1124
+ if (prevEtag !== void 0 && etag !== void 0 && prevEtag === etag) {
1125
+ options.onNoChange?.();
1126
+ } else {
1127
+ if (!res.ok) {
1128
+ throw createErrorFromResponse(res.status, {}, "Poll request failed");
1129
+ }
1130
+ const data = await res.json();
1131
+ prevEtag = etag;
1132
+ options.onData?.(data);
1133
+ }
1134
+ } catch (err) {
1135
+ options.onError?.(err);
1136
+ } finally {
1137
+ if (!stopped) {
1138
+ timer = setTimeout(() => {
1139
+ void tick();
1140
+ }, interval);
1141
+ }
1142
+ }
1143
+ };
1144
+ void tick();
1145
+ return {
1146
+ stop() {
1147
+ stopped = true;
1148
+ if (timer !== null) {
1149
+ clearTimeout(timer);
1150
+ timer = null;
1151
+ }
1152
+ }
1153
+ };
1154
+ }
859
1155
  /** Count entities matching the given filters. */
860
1156
  async count(params) {
861
1157
  const parts = ["count=true", "limit=0"];
@@ -1054,4 +1350,13 @@ var index_default = GeonicDB;
1054
1350
  if (typeof window !== "undefined") {
1055
1351
  window.GeonicDB = GeonicDB;
1056
1352
  }
1353
+ function buildEntitiesPath(params) {
1354
+ if (!params) return "/ngsi-ld/v1/entities";
1355
+ const parts = [];
1356
+ if (params.type) parts.push("type=" + encodeURIComponent(params.type));
1357
+ if (params.limit != null) parts.push("limit=" + params.limit);
1358
+ if (params.offset != null) parts.push("offset=" + params.offset);
1359
+ if (params.q) parts.push("q=" + encodeURIComponent(params.q));
1360
+ return parts.length > 0 ? "/ngsi-ld/v1/entities?" + parts.join("&") : "/ngsi-ld/v1/entities";
1361
+ }
1057
1362
  //# sourceMappingURL=geonicdb.cjs.map