@geolonia/geonicdb-sdk 0.16.0 → 0.17.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
@@ -220,10 +220,25 @@ const entities = await db.getEntities({ type: 'GeoJSON' });
220
220
 
221
221
  ## Entity CRUD
222
222
 
223
- > **Automatic `@context` (#1583)**: the SDK sends requests as `application/ld+json`,
224
- > which requires an inline JSON-LD `@context` in the body. When your body omits
225
- > `@context`, the SDK injects the NGSI-LD core context automatically; a
226
- > `@context` you provide is always preserved (never overwritten).
223
+ > **Automatic `@context` (#1583 / #1599 / #2069)**: the SDK sends requests as
224
+ > `application/ld+json`, which per ETSI GS CIM 009 clause 6.3.5 requires an inline JSON-LD
225
+ > `@context` in the body. When your body omits `@context`, the SDK injects the NGSI-LD core
226
+ > context automatically; a `@context` you provide is always preserved (never overwritten).
227
+ >
228
+ > Injection covers the **whole NGSI-LD API (`/ngsi-ld/v1/**`)** — the same surface where the
229
+ > broker enforces clause 6.3.5 (`/ngsi-ld/v1/jsonldContexts` is excluded, since its body *is*
230
+ > the context document). **Batch array bodies (`/entityOperations/*`) get it per element**,
231
+ > because each element is its own JSON-LD document (clause 5.6.7.3) and an element without a
232
+ > `@context` comes back as a per-entity `BadRequestData` inside a **`207 Multi-Status`**.
233
+ > Arrays of ID strings (`entityOperations/delete`, clause 5.6.10.3) are left alone, as are
234
+ > non-NGSI-LD endpoints (auth / admin / rules), whose `.strict()` schemas reject unknown keys.
235
+ >
236
+ > **Do not attach a `Link` header to writes (#1924)**: ETSI GS CIM 009 clause 6.3.5
237
+ > restricts where `@context` may come from based on `Content-Type` and forbids
238
+ > mixing the two. Since the SDK always sends `application/ld+json`, adding a
239
+ > JSON-LD `Link` header to a `POST` / `PATCH` / `PUT` via the `request()` /
240
+ > `requestRaw()` escape hatch returns `400 BadRequestData`. Put the `@context`
241
+ > in the body for writes; the `Link` header remains the channel for reads.
227
242
 
228
243
  ```typescript
229
244
  // Create
@@ -302,6 +317,11 @@ const result = await db.request('GET', '/ngsi-ld/v1/subscriptions');
302
317
 
303
318
  ## WebSocket Event Streaming
304
319
 
320
+ > **NGSI-LD changes only (#2284).** A WebSocket connection streams entity changes made through the
321
+ > NGSI-LD API. Writes performed with the NGSIv2 client (`@geolonia/geonicdb-sdk/ngsi-v2`) or any
322
+ > other `/v2/*` caller are **not** delivered — entities are protocol-isolated and the socket is an
323
+ > NGSI-LD reader. Use HTTP subscriptions (`/v2/subscriptions`) for NGSIv2 changes.
324
+
305
325
  ```typescript
306
326
  db.subscribe({ entityTypes: ['Room'] });
307
327
 
@@ -324,6 +344,7 @@ await db.connect();
324
344
  | `entityDeleted` | `EntityEvent` | Entity was deleted |
325
345
  | `open` | — | WebSocket connection opened |
326
346
  | `connected` | — | WebSocket ready |
347
+ | `subscribed` | — | `subscribe()` filter is now active on the server. Applying a subscription is asynchronous (the server resolves the supplied `@context` to expand `entityTypes` for matching), so events published before this fires are not guaranteed to be delivered |
327
348
  | `close` | `{ code, reason }` | WebSocket closed |
328
349
  | `disconnected` | — | Unexpected disconnect |
329
350
  | `reconnecting` | `{ attempt, delay }` | Auto-reconnect starting |
@@ -477,7 +498,9 @@ handle.stop();
477
498
  | `limit` | `number` | Max results |
478
499
  | `offset` | `number` | Pagination offset |
479
500
  | `q` | `string` | NGSI-LD query expression |
480
- | `options` | `string` | NGSI-LD representation options (comma-separated): `sysAttrs` (include `createdAt`/`modifiedAt`), `keyValues`, `concise`, `normalized` |
501
+ | `datasetId` | `string` | Multi-attribute instance filter (comma-separated; default instance is `@none`). A single match is returned as one Attribute element (ETSI GS CIM 009 clause 4.5.5.1) |
502
+ | `options` | `string` | NGSI-LD representation options (comma-separated): `sysAttrs` (include `createdAt`/`modifiedAt`), `keyValues`, `concise`, `normalized`. Under `keyValues`, multi-attributes (instances with a `datasetId`) are returned as a `dataset` map **only when more than one instance matches**, the default instance keyed by `@none`. A single matching instance stays a bare value (ETSI GS CIM 009 clause 4.5.4 / 4.5.5.1) |
503
+ | `local` | `boolean` | **#2278.** Limit execution to local scope (ETSI GS CIM 009 clause 5.5.13). `GET /ngsi-ld/v1/entities` requires at least one of `type` / `attrs` (with a non-system attribute) / `q` (with a non-system attribute) / a geoquery / local scope (clause 5.7.2.4). **To fetch everything without a filter, pass `local: true`** — otherwise the broker returns `400 BadRequestData` (too wide query) |
481
504
 
482
505
  #### `CountEntitiesParams`
483
506
 
@@ -485,6 +508,7 @@ handle.stop();
485
508
  |----------|------|-------------|
486
509
  | `type` | `string` | Entity type filter |
487
510
  | `q` | `string` | NGSI-LD query expression |
511
+ | `local` | `boolean` | **#2278.** Limit execution to local scope (ETSI GS CIM 009 clause 5.5.13). `GET /ngsi-ld/v1/entities` requires at least one of `type` / `attrs` (with a non-system attribute) / `q` (with a non-system attribute) / a geoquery / local scope (clause 5.7.2.4). **To fetch everything without a filter, pass `local: true`** — otherwise the broker returns `400 BadRequestData` (too wide query) |
488
512
 
489
513
  ### Types / Attributes Discovery
490
514
 
@@ -502,6 +526,8 @@ handle.stop();
502
526
  | `getTemporalEntities(params?)` | `GetTemporalEntitiesParams` | `Promise<Record<string, unknown>[]>` | Query temporal entities |
503
527
  | `getTemporalEntity(entityId)` | `string` | `Promise<Record<string, unknown>>` | Get temporal representation of a single entity |
504
528
 
529
+ > Temporal reads answer with **`206 Partial Content`** and a `Content-Range` header when `lastN` is present or the default instance cap actually truncated the history (#2343, ETSI GS CIM 009 clause 6.3.10). Untruncated reads are `200`. Empty results and aggregated representations (`aggrMethods`) remain `200` even when `lastN` is specified, because `Content-Range` cannot be derived. Both methods are unaffected — the SDK checks `res.ok`, which accepts any 2xx. Use `requestRaw()` when you need to read the `Content-Range` value itself.
530
+
505
531
  #### `GetTemporalEntitiesParams`
506
532
 
507
533
  | Property | Type | Description |
@@ -511,20 +537,23 @@ handle.stop();
511
537
  | `idPattern` | `string` | Entity ID pattern (regex) |
512
538
  | `attrs` | `string` | Attribute names to retrieve |
513
539
  | `q` | `string` | NGSI-LD query expression |
514
- | `timeproperty` | `string` | Time property (`observedAt`, `createdAt`, `modifiedAt`) |
540
+ | `timeproperty` | `string` | Time property (`observedAt`, `createdAt`, `modifiedAt`, `deletedAt`; default `observedAt`) |
515
541
  | `timeAt` | `string` | Start time (ISO 8601) |
516
542
  | `endTimeAt` | `string` | End time (ISO 8601) |
517
543
  | `timerel` | `string` | Temporal relation (`before`, `after`, `between`) |
518
544
  | `limit` | `number` | Max results |
519
545
  | `offset` | `number` | Pagination offset |
520
- | `options` | `string` | NGSI-LD representation options (comma-separated): `sysAttrs`, `temporalValues`, `aggregatedValues` |
546
+ | `options` | `string` | NGSI-LD temporal representation options (comma-separated): `temporalValues`, `simplified` (alias), `aggregatedValues` (ETSI clause 4.5.19 shape — one member per aggregation method, each period an array of `[value, start, end]`; `aggrMethods` is required when `aggregatedValues` is specified via `options` in this SDK, and the server applies the same requirement to `format=aggregatedValues`), `sysAttrs` (ETSI 6.3.11 — adds `createdAt` / `modifiedAt` / `expiresAt` to each instance of the normalized representation where known; accepted and ignored for the simplified and aggregated representations). `options` is deprecated in favour of `format` (ETSI 6.3.12); when both are present, **`format` takes precedence** |
547
+ | `format` | `'temporalValues' \| 'aggregatedValues' \| 'simplified'` | NGSI-LD temporal representation format (ETSI 6.3.12): `temporalValues` or `aggregatedValues` (`simplified` is accepted as an alias of `temporalValues`). Unknown values are rejected by the server with `400 InvalidRequest`. **Takes precedence over `options`** when both are present |
548
+ | `aggrMethods` | `string` | Aggregation methods (comma-separated): `totalCount`, `distinctCount`, `sum`, `avg`, `min`, `max`, `stddev`, `sumsq`. Required when `aggregatedValues` is requested via `format` or `options` |
549
+ | `aggrPeriodDuration` | `string` | Aggregation period as an ISO 8601 duration (e.g. `PT1H`). Optional (ETSI Table 5.2.21-1 cardinality 0..1) — when omitted, the whole time range of the query is aggregated as a single period (`PT0S`) |
521
550
 
522
551
  ### Batch Operations
523
552
 
524
553
  | Method | Parameters | Returns | Description |
525
554
  |--------|-----------|---------|-------------|
526
555
  | `batchCreate(entities)` | `Record<string, unknown>[]` | `Promise<Record<string, unknown>>` | Create multiple entities |
527
- | `batchUpsert(entities)` | `Record<string, unknown>[]` | `Promise<Record<string, unknown>>` | Upsert multiple entities |
556
+ | `batchUpsert(entities)` | `Record<string, unknown>[]` | `Promise<string[] \| Record<string, unknown>>` | Upsert multiple entities. Per ETSI clause 5.6.8.5 the server returns **201** with a `string[]` of the **created** entity IDs when anything was created, **204** (mapped to `{}`) when every entity already existed, or **207** with `{ created, updated, errors }` on partial failure (#1742) |
528
557
  | `batchUpdate(entities)` | `Record<string, unknown>[]` | `Promise<Record<string, unknown>>` | Update multiple entities |
529
558
  | `batchDelete(entityIds)` | `string[]` | `Promise<Record<string, unknown>>` | Delete multiple entities by ID |
530
559
 
@@ -580,6 +609,8 @@ handle.stop();
580
609
  |----------|------|-------------|
581
610
  | `entityTypes` | `string[]` | Entity types to subscribe to |
582
611
  | `idPattern` | `string` | Entity ID regex pattern |
612
+ | `scopeQ` | `string` | NGSI-LD scope query expression (#1132) |
613
+ | `@context` | `string \| string[]` | **#2026.** JSON-LD `@context` used to render delivered events — entity type and attribute names are compacted against it (ETSI GS CIM 009 clause 5.5.7) and echoed back in each event's `@context`. URL string(s) only. **#2055.** It also governs `entityTypes` matching — the `@context` is resolved once at `subscribe` time to expand `entityTypes` into the canonical stored form, so custom terms change which entities match. `idPattern` alone matches the stored form verbatim |
583
614
 
584
615
  ### Event Handling
585
616
 
package/geonicdb.cjs CHANGED
@@ -1246,13 +1246,12 @@ var _AuthManager = class _AuthManager {
1246
1246
  }
1247
1247
  const url = this._baseUrl + path;
1248
1248
  const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && token !== null;
1249
- const isEntityWrite = path.split("?")[0].startsWith("/ngsi-ld/v1/entities");
1249
+ const pathname = path.split("?")[0];
1250
+ const isNgsiLdWrite = pathname.startsWith("/ngsi-ld/v1/") && pathname !== "/ngsi-ld/v1/jsonldContexts";
1251
+ const withCoreContext = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && !("@context" in value) ? { ...value, "@context": NGSI_LD_CORE_CONTEXT } : value;
1250
1252
  let effectiveBody = body;
1251
- if (isEntityWrite && effectiveBody !== null && typeof effectiveBody === "object" && !Array.isArray(effectiveBody) && !("@context" in effectiveBody)) {
1252
- effectiveBody = {
1253
- ...effectiveBody,
1254
- "@context": NGSI_LD_CORE_CONTEXT
1255
- };
1253
+ if (isNgsiLdWrite && effectiveBody !== void 0) {
1254
+ effectiveBody = Array.isArray(effectiveBody) ? effectiveBody.map(withCoreContext) : withCoreContext(effectiveBody);
1256
1255
  }
1257
1256
  const bodyStr = effectiveBody !== void 0 ? JSON.stringify(effectiveBody) : void 0;
1258
1257
  const doRequest = async (reqToken, dpopNonce) => {
@@ -1398,6 +1397,7 @@ var WebSocketManager = class {
1398
1397
  if (options.entityTypes) msg.entityTypes = options.entityTypes;
1399
1398
  if (options.idPattern) msg.idPattern = options.idPattern;
1400
1399
  if (options.scopeQ) msg.scopeQ = options.scopeQ;
1400
+ if (options["@context"]) msg["@context"] = options["@context"];
1401
1401
  }
1402
1402
  this._subscription = msg;
1403
1403
  if (this._pendingSubscription) return;
@@ -1465,6 +1465,10 @@ var WebSocketManager = class {
1465
1465
  return;
1466
1466
  }
1467
1467
  if (msg.type === "pong") return;
1468
+ if (msg.type === "subscribed") {
1469
+ this._emit("subscribed", void 0);
1470
+ return;
1471
+ }
1468
1472
  if (msg.type === "dpop_verified") {
1469
1473
  this._pendingSubscription = false;
1470
1474
  if (this._subscription) {
@@ -1931,7 +1935,9 @@ var GeonicDB = class extends EventEmitter {
1931
1935
  limit: 0,
1932
1936
  type: params?.type,
1933
1937
  q: params?.q,
1934
- scopeQ: params?.scopeQ
1938
+ scopeQ: params?.scopeQ,
1939
+ // #2278: clause 5.7.2.4 の "too wide query" 免除条件 (e) local scope。
1940
+ local: params?.local
1935
1941
  });
1936
1942
  const res = await this._auth.request("GET", path);
1937
1943
  if (!res.ok) {
@@ -2028,7 +2034,21 @@ var GeonicDB = class extends EventEmitter {
2028
2034
  "Batch create failed"
2029
2035
  );
2030
2036
  }
2031
- /** Upsert multiple entities in a single request. */
2037
+ /**
2038
+ * Upsert multiple entities in a single request.
2039
+ *
2040
+ * #1742: the server follows ETSI GS CIM 009 clause 5.6.8.5 / Table 6.15.3.1-1:
2041
+ * - **201** with a `string[]` of the **created** entity IDs, when at least one entity
2042
+ * was newly created and none failed (merely updated entities are not listed);
2043
+ * - **204** with no body, when every entity already existed and was updated
2044
+ * (`_jsonPost` maps this to `{}`);
2045
+ * - **207** with a `BatchOperationResult` (`{ created, updated, errors }`), on partial
2046
+ * or total failure.
2047
+ *
2048
+ * The return type reflects all three shapes. Narrow with `Array.isArray(result)` to
2049
+ * detect the "something was created" case.
2050
+ * https://cim.etsi.org/NGSI-LD/official/clause-6.html
2051
+ */
2032
2052
  async batchUpsert(entities) {
2033
2053
  return this._jsonPost(
2034
2054
  "/ngsi-ld/v1/entityOperations/upsert",