@geolonia/geonicdb-sdk 0.15.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
@@ -257,9 +272,11 @@ await db.deleteEntity('urn:ngsi-ld:Room:001');
257
272
  ## Types / Attributes Discovery
258
273
 
259
274
  ```typescript
260
- const types = await db.getTypes();
275
+ // getTypes()/getAttributes() は EntityTypeList/AttributeList オブジェクトを返す。
276
+ // 型名・属性名の一覧は typeList / attributeList から取得する。
277
+ const { typeList } = await db.getTypes(); // string[]
261
278
  const roomType = await db.getType('Room');
262
- const attributes = await db.getAttributes();
279
+ const { attributeList } = await db.getAttributes(); // string[]
263
280
  const tempAttr = await db.getAttribute('temperature');
264
281
  ```
265
282
 
@@ -300,6 +317,11 @@ const result = await db.request('GET', '/ngsi-ld/v1/subscriptions');
300
317
 
301
318
  ## WebSocket Event Streaming
302
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
+
303
325
  ```typescript
304
326
  db.subscribe({ entityTypes: ['Room'] });
305
327
 
@@ -322,6 +344,7 @@ await db.connect();
322
344
  | `entityDeleted` | `EntityEvent` | Entity was deleted |
323
345
  | `open` | — | WebSocket connection opened |
324
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 |
325
348
  | `close` | `{ code, reason }` | WebSocket closed |
326
349
  | `disconnected` | — | Unexpected disconnect |
327
350
  | `reconnecting` | `{ attempt, delay }` | Auto-reconnect starting |
@@ -475,7 +498,9 @@ handle.stop();
475
498
  | `limit` | `number` | Max results |
476
499
  | `offset` | `number` | Pagination offset |
477
500
  | `q` | `string` | NGSI-LD query expression |
478
- | `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) |
479
504
 
480
505
  #### `CountEntitiesParams`
481
506
 
@@ -483,14 +508,15 @@ handle.stop();
483
508
  |----------|------|-------------|
484
509
  | `type` | `string` | Entity type filter |
485
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) |
486
512
 
487
513
  ### Types / Attributes Discovery
488
514
 
489
515
  | Method | Parameters | Returns | Description |
490
516
  |--------|-----------|---------|-------------|
491
- | `getTypes()` | — | `Promise<Record<string, unknown>[]>` | List all entity types |
517
+ | `getTypes()` | — | `Promise<EntityTypeList>` | List all entity types (`{ id, type, typeList }`) |
492
518
  | `getType(typeName)` | `string` | `Promise<Record<string, unknown>>` | Get details for a specific entity type |
493
- | `getAttributes()` | — | `Promise<Record<string, unknown>[]>` | List all attributes |
519
+ | `getAttributes()` | — | `Promise<AttributeList>` | List all attributes (`{ id, type, attributeList }`) |
494
520
  | `getAttribute(attrName)` | `string` | `Promise<Record<string, unknown>>` | Get details for a specific attribute |
495
521
 
496
522
  ### Temporal API
@@ -500,6 +526,8 @@ handle.stop();
500
526
  | `getTemporalEntities(params?)` | `GetTemporalEntitiesParams` | `Promise<Record<string, unknown>[]>` | Query temporal entities |
501
527
  | `getTemporalEntity(entityId)` | `string` | `Promise<Record<string, unknown>>` | Get temporal representation of a single entity |
502
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
+
503
531
  #### `GetTemporalEntitiesParams`
504
532
 
505
533
  | Property | Type | Description |
@@ -509,20 +537,23 @@ handle.stop();
509
537
  | `idPattern` | `string` | Entity ID pattern (regex) |
510
538
  | `attrs` | `string` | Attribute names to retrieve |
511
539
  | `q` | `string` | NGSI-LD query expression |
512
- | `timeproperty` | `string` | Time property (`observedAt`, `createdAt`, `modifiedAt`) |
540
+ | `timeproperty` | `string` | Time property (`observedAt`, `createdAt`, `modifiedAt`, `deletedAt`; default `observedAt`) |
513
541
  | `timeAt` | `string` | Start time (ISO 8601) |
514
542
  | `endTimeAt` | `string` | End time (ISO 8601) |
515
543
  | `timerel` | `string` | Temporal relation (`before`, `after`, `between`) |
516
544
  | `limit` | `number` | Max results |
517
545
  | `offset` | `number` | Pagination offset |
518
- | `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`) |
519
550
 
520
551
  ### Batch Operations
521
552
 
522
553
  | Method | Parameters | Returns | Description |
523
554
  |--------|-----------|---------|-------------|
524
555
  | `batchCreate(entities)` | `Record<string, unknown>[]` | `Promise<Record<string, unknown>>` | Create multiple entities |
525
- | `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) |
526
557
  | `batchUpdate(entities)` | `Record<string, unknown>[]` | `Promise<Record<string, unknown>>` | Update multiple entities |
527
558
  | `batchDelete(entityIds)` | `string[]` | `Promise<Record<string, unknown>>` | Delete multiple entities by ID |
528
559
 
@@ -578,6 +609,8 @@ handle.stop();
578
609
  |----------|------|-------------|
579
610
  | `entityTypes` | `string[]` | Entity types to subscribe to |
580
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 |
581
614
 
582
615
  ### Event Handling
583
616
 
package/geonicdb.cjs CHANGED
@@ -82,7 +82,7 @@ var RECONNECT_MAX_ATTEMPTS = 10;
82
82
  var RECONNECT_BASE_MS = 1e3;
83
83
  var RECONNECT_MAX_DELAY_MS = 3e4;
84
84
  var SUB_PROTOCOL = "access_token";
85
- var NGSI_LD_CORE_CONTEXT = "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld";
85
+ var NGSI_LD_CORE_CONTEXT = "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.9.jsonld";
86
86
  var SDK_CACHE_MAX_ENTRIES_DEFAULT = 1e3;
87
87
  var SDK_POLL_INTERVAL_MS_DEFAULT = 5e3;
88
88
  var DPOP_IDB_NAME = "geonicdb-sdk";
@@ -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) {
@@ -1967,7 +1973,13 @@ var GeonicDB = class extends EventEmitter {
1967
1973
  return { deleted: true };
1968
1974
  }
1969
1975
  // --- Types / Attributes Discovery ---
1970
- /** List all entity types. */
1976
+ /**
1977
+ * List all entity types.
1978
+ *
1979
+ * `details` 未指定の `GET /types` は ETSI OpenAPI v1.8.1 準拠の
1980
+ * `EntityTypeList` オブジェクト(`{ id, type: 'EntityTypeList', typeList }`)を返す。
1981
+ * 型名の一覧は `typeList` から取得する。
1982
+ */
1971
1983
  async getTypes() {
1972
1984
  return this._jsonGet("/ngsi-ld/v1/types", "Types query failed");
1973
1985
  }
@@ -1978,7 +1990,13 @@ var GeonicDB = class extends EventEmitter {
1978
1990
  "Type not found"
1979
1991
  );
1980
1992
  }
1981
- /** List all attributes. */
1993
+ /**
1994
+ * List all attributes.
1995
+ *
1996
+ * `details` 未指定の `GET /attributes` は ETSI OpenAPI v1.8.1 準拠の
1997
+ * `AttributeList` オブジェクト(`{ id, type: 'AttributeList', attributeList }`)を返す。
1998
+ * 属性名の一覧は `attributeList` から取得する。
1999
+ */
1982
2000
  async getAttributes() {
1983
2001
  return this._jsonGet("/ngsi-ld/v1/attributes", "Attributes query failed");
1984
2002
  }
@@ -2016,7 +2034,21 @@ var GeonicDB = class extends EventEmitter {
2016
2034
  "Batch create failed"
2017
2035
  );
2018
2036
  }
2019
- /** 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
+ */
2020
2052
  async batchUpsert(entities) {
2021
2053
  return this._jsonPost(
2022
2054
  "/ngsi-ld/v1/entityOperations/upsert",