@geolonia/geonicdb-sdk 0.16.0 → 0.18.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 +123 -12
- package/geonicdb.cjs +72 -25
- package/geonicdb.cjs.map +2 -2
- package/geonicdb.d.ts +145 -13
- package/geonicdb.iife.js +72 -25
- package/geonicdb.iife.js.map +2 -2
- package/geonicdb.mjs +72 -25
- package/geonicdb.mjs.map +2 -2
- package/package.json +1 -1
- package/react/index.d.ts +116 -11
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
GeonicDB JavaScript SDK — NGSI-LD Context Broker client for browser and Node.js.
|
|
4
4
|
|
|
5
|
+
> **Server-side reference**: most responses carry a
|
|
6
|
+
> `Link: </llms.txt>; rel="describedby"` header pointing at
|
|
7
|
+
> `https://<your-broker>/llms.txt` — a single Markdown document covering the API
|
|
8
|
+
> surface, the auth flow, the entity data model, and a **Troubleshooting**
|
|
9
|
+
> section for symptoms whose cause is not what the error text suggests. NGSI-LD
|
|
10
|
+
> paginated list responses are the exception: their `Link` header carries only
|
|
11
|
+
> `rel="next"`/`rel="prev"`, not the discovery link. Read `/llms.txt` directly
|
|
12
|
+
> alongside this README when a request fails for a reason the SDK cannot
|
|
13
|
+
> explain on its own.
|
|
14
|
+
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
7
17
|
```bash
|
|
@@ -209,6 +219,7 @@ const entities = await db.getEntities({ type: 'GeoJSON' });
|
|
|
209
219
|
```
|
|
210
220
|
|
|
211
221
|
- `apiKey` and `anonymous: true` cannot be combined (constructor throws).
|
|
222
|
+
- A **presented but invalid** Bearer / DPoP token returns HTTP 401 from the server (#2794), not an anonymous 403 — so refresh can fire. True anonymous mode sends **no** `Authorization` header.
|
|
212
223
|
- `db.login()` upgrades the SDK out of anonymous mode (Bearer JWT).
|
|
213
224
|
- `db.setCredentials({ token, tokenType: 'Bearer', ... })` upgrades from
|
|
214
225
|
anonymous. **`tokenType: 'DPoP'` is rejected** for anonymous-built instances
|
|
@@ -220,10 +231,25 @@ const entities = await db.getEntities({ type: 'GeoJSON' });
|
|
|
220
231
|
|
|
221
232
|
## Entity CRUD
|
|
222
233
|
|
|
223
|
-
> **Automatic `@context` (#1583)**: the SDK sends requests as
|
|
224
|
-
> which
|
|
225
|
-
> `@context`, the SDK injects the NGSI-LD core
|
|
226
|
-
> `@context` you provide is always preserved (never overwritten).
|
|
234
|
+
> **Automatic `@context` (#1583 / #1599 / #2069)**: the SDK sends requests as
|
|
235
|
+
> `application/ld+json`, which per ETSI GS CIM 009 clause 6.3.5 requires an inline JSON-LD
|
|
236
|
+
> `@context` in the body. When your body omits `@context`, the SDK injects the NGSI-LD core
|
|
237
|
+
> context automatically; a `@context` you provide is always preserved (never overwritten).
|
|
238
|
+
>
|
|
239
|
+
> Injection covers the **whole NGSI-LD API (`/ngsi-ld/v1/**`)** — the same surface where the
|
|
240
|
+
> broker enforces clause 6.3.5 (`/ngsi-ld/v1/jsonldContexts` is excluded, since its body *is*
|
|
241
|
+
> the context document). **Batch array bodies (`/entityOperations/*`) get it per element**,
|
|
242
|
+
> because each element is its own JSON-LD document (clause 5.6.7.3) and an element without a
|
|
243
|
+
> `@context` comes back as a per-entity `BadRequestData` inside a **`207 Multi-Status`**.
|
|
244
|
+
> Arrays of ID strings (`entityOperations/delete`, clause 5.6.10.3) are left alone, as are
|
|
245
|
+
> non-NGSI-LD endpoints (auth / admin / rules), whose `.strict()` schemas reject unknown keys.
|
|
246
|
+
>
|
|
247
|
+
> **Do not attach a `Link` header to writes (#1924)**: ETSI GS CIM 009 clause 6.3.5
|
|
248
|
+
> restricts where `@context` may come from based on `Content-Type` and forbids
|
|
249
|
+
> mixing the two. Since the SDK always sends `application/ld+json`, adding a
|
|
250
|
+
> JSON-LD `Link` header to a `POST` / `PATCH` / `PUT` via the `request()` /
|
|
251
|
+
> `requestRaw()` escape hatch returns `400 BadRequestData`. Put the `@context`
|
|
252
|
+
> in the body for writes; the `Link` header remains the channel for reads.
|
|
227
253
|
|
|
228
254
|
```typescript
|
|
229
255
|
// Create
|
|
@@ -302,6 +328,17 @@ const result = await db.request('GET', '/ngsi-ld/v1/subscriptions');
|
|
|
302
328
|
|
|
303
329
|
## WebSocket Event Streaming
|
|
304
330
|
|
|
331
|
+
> **NGSI-LD changes only (#2284).** A WebSocket connection streams entity changes made through the
|
|
332
|
+
> NGSI-LD API. Writes performed with the NGSIv2 client (`@geolonia/geonicdb-sdk/ngsi-v2`) or any
|
|
333
|
+
> other `/v2/*` caller are **not** delivered — entities are protocol-isolated and the socket is an
|
|
334
|
+
> NGSI-LD reader. Use HTTP subscriptions (`/v2/subscriptions`) for NGSIv2 changes.
|
|
335
|
+
|
|
336
|
+
> **Dedicated deployments (#2867):** REST resolves the database from the custom domain (`Host`), but
|
|
337
|
+
> WebSocket uses a shared execute-api endpoint. The SDK calls `GET /sdk/v1/streaming` on your
|
|
338
|
+
> `baseUrl`, reads the optional `deployment` hostname from the response, and appends
|
|
339
|
+
> `?deployment=<hostname>` on `$connect`. Raw `WebSocket` clients must set the same query parameter
|
|
340
|
+
> manually when connecting to a non-default deployment.
|
|
341
|
+
|
|
305
342
|
```typescript
|
|
306
343
|
db.subscribe({ entityTypes: ['Room'] });
|
|
307
344
|
|
|
@@ -322,8 +359,12 @@ await db.connect();
|
|
|
322
359
|
| `entityCreated` | `EntityEvent` | Entity was created |
|
|
323
360
|
| `entityUpdated` | `EntityEvent` | Entity was updated |
|
|
324
361
|
| `entityDeleted` | `EntityEvent` | Entity was deleted |
|
|
362
|
+
|
|
363
|
+
`EntityEvent.entityType` is `string | string[]` (#2477): a single type is a string; multi-type entities use a string array (same rule as NGSI-LD GET / Table 5.2.4).
|
|
364
|
+
|
|
325
365
|
| `open` | — | WebSocket connection opened |
|
|
326
366
|
| `connected` | — | WebSocket ready |
|
|
367
|
+
| `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
368
|
| `close` | `{ code, reason }` | WebSocket closed |
|
|
328
369
|
| `disconnected` | — | Unexpected disconnect |
|
|
329
370
|
| `reconnecting` | `{ attempt, delay }` | Auto-reconnect starting |
|
|
@@ -477,7 +518,9 @@ handle.stop();
|
|
|
477
518
|
| `limit` | `number` | Max results |
|
|
478
519
|
| `offset` | `number` | Pagination offset |
|
|
479
520
|
| `q` | `string` | NGSI-LD query expression |
|
|
480
|
-
| `
|
|
521
|
+
| `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) |
|
|
522
|
+
| `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) |
|
|
523
|
+
| `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
524
|
|
|
482
525
|
#### `CountEntitiesParams`
|
|
483
526
|
|
|
@@ -485,6 +528,7 @@ handle.stop();
|
|
|
485
528
|
|----------|------|-------------|
|
|
486
529
|
| `type` | `string` | Entity type filter |
|
|
487
530
|
| `q` | `string` | NGSI-LD query expression |
|
|
531
|
+
| `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
532
|
|
|
489
533
|
### Types / Attributes Discovery
|
|
490
534
|
|
|
@@ -502,6 +546,8 @@ handle.stop();
|
|
|
502
546
|
| `getTemporalEntities(params?)` | `GetTemporalEntitiesParams` | `Promise<Record<string, unknown>[]>` | Query temporal entities |
|
|
503
547
|
| `getTemporalEntity(entityId)` | `string` | `Promise<Record<string, unknown>>` | Get temporal representation of a single entity |
|
|
504
548
|
|
|
549
|
+
> 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.
|
|
550
|
+
|
|
505
551
|
#### `GetTemporalEntitiesParams`
|
|
506
552
|
|
|
507
553
|
| Property | Type | Description |
|
|
@@ -511,20 +557,23 @@ handle.stop();
|
|
|
511
557
|
| `idPattern` | `string` | Entity ID pattern (regex) |
|
|
512
558
|
| `attrs` | `string` | Attribute names to retrieve |
|
|
513
559
|
| `q` | `string` | NGSI-LD query expression |
|
|
514
|
-
| `timeproperty` | `string` | Time property (`observedAt`, `createdAt`, `modifiedAt`) |
|
|
560
|
+
| `timeproperty` | `string` | Time property (`observedAt`, `createdAt`, `modifiedAt`, `deletedAt`; default `observedAt`) |
|
|
515
561
|
| `timeAt` | `string` | Start time (ISO 8601) |
|
|
516
562
|
| `endTimeAt` | `string` | End time (ISO 8601) |
|
|
517
563
|
| `timerel` | `string` | Temporal relation (`before`, `after`, `between`) |
|
|
518
564
|
| `limit` | `number` | Max results |
|
|
519
565
|
| `offset` | `number` | Pagination offset |
|
|
520
|
-
| `options` | `string` | NGSI-LD representation options (comma-separated): `
|
|
566
|
+
| `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** |
|
|
567
|
+
| `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 |
|
|
568
|
+
| `aggrMethods` | `string` | Aggregation methods (comma-separated): `totalCount`, `distinctCount`, `sum`, `avg`, `min`, `max`, `stddev`, `sumsq`. Required when `aggregatedValues` is requested via `format` or `options` |
|
|
569
|
+
| `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
570
|
|
|
522
571
|
### Batch Operations
|
|
523
572
|
|
|
524
573
|
| Method | Parameters | Returns | Description |
|
|
525
574
|
|--------|-----------|---------|-------------|
|
|
526
575
|
| `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 |
|
|
576
|
+
| `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
577
|
| `batchUpdate(entities)` | `Record<string, unknown>[]` | `Promise<Record<string, unknown>>` | Update multiple entities |
|
|
529
578
|
| `batchDelete(entityIds)` | `string[]` | `Promise<Record<string, unknown>>` | Delete multiple entities by ID |
|
|
530
579
|
|
|
@@ -580,6 +629,8 @@ handle.stop();
|
|
|
580
629
|
|----------|------|-------------|
|
|
581
630
|
| `entityTypes` | `string[]` | Entity types to subscribe to |
|
|
582
631
|
| `idPattern` | `string` | Entity ID regex pattern |
|
|
632
|
+
| `scopeQ` | `string` | NGSI-LD scope query expression (#1132) |
|
|
633
|
+
| `@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
634
|
|
|
584
635
|
### Event Handling
|
|
585
636
|
|
|
@@ -593,13 +644,23 @@ handle.stop();
|
|
|
593
644
|
All async methods throw typed errors extending `GeonicDBError`. Use `instanceof` to distinguish error types.
|
|
594
645
|
|
|
595
646
|
```typescript
|
|
596
|
-
import {
|
|
647
|
+
import {
|
|
648
|
+
GeonicDBError,
|
|
649
|
+
AuthenticationError,
|
|
650
|
+
PasswordResetRequiredError,
|
|
651
|
+
TemporaryPasswordExpiredError,
|
|
652
|
+
NotFoundError,
|
|
653
|
+
} from '@geolonia/geonicdb-sdk';
|
|
597
654
|
|
|
598
655
|
try {
|
|
599
656
|
await db.getEntity('urn:ngsi-ld:Room:404');
|
|
600
657
|
} catch (err) {
|
|
601
|
-
if (err instanceof
|
|
602
|
-
//
|
|
658
|
+
if (err instanceof PasswordResetRequiredError) {
|
|
659
|
+
// 409 — collect a new password and call login(..., { newPassword })
|
|
660
|
+
} else if (err instanceof TemporaryPasswordExpiredError) {
|
|
661
|
+
// 403 — temporary password expired; ask an administrator to re-issue it
|
|
662
|
+
} else if (err instanceof AuthenticationError) {
|
|
663
|
+
// Other auth failures — re-login needed (typically 401; for HTTP-derived errors see err.statusCode)
|
|
603
664
|
} else if (err instanceof NotFoundError) {
|
|
604
665
|
// 404 — entity not found
|
|
605
666
|
} else if (err instanceof GeonicDBError) {
|
|
@@ -613,7 +674,9 @@ try {
|
|
|
613
674
|
| Class | Status | Description |
|
|
614
675
|
|-------|--------|-------------|
|
|
615
676
|
| `GeonicDBError` | — | Base class (`statusCode` property) |
|
|
616
|
-
| `AuthenticationError` | 401 | Invalid credentials
|
|
677
|
+
| `AuthenticationError` | Real HTTP status (default 401) | Invalid credentials, expired token, or an HTTP failure on an auth endpoint. HTTP-derived errors carry the server status |
|
|
678
|
+
| `PasswordResetRequiredError` | 409 | First login with a temporary password — set a new password (`AuthenticationError` subclass) |
|
|
679
|
+
| `TemporaryPasswordExpiredError` | 403 | Temporary password expired (`AuthenticationError` subclass) |
|
|
617
680
|
| `AuthorizationError` | 403 | Insufficient permissions |
|
|
618
681
|
| `NotFoundError` | 404 | Resource not found |
|
|
619
682
|
| `ConflictError` | 409 | Entity already exists |
|
|
@@ -621,6 +684,54 @@ try {
|
|
|
621
684
|
| `RateLimitError` | 429 | Rate limited (`retryAfter` property) |
|
|
622
685
|
| `NetworkError` | — | Fetch failure, DNS error, timeout |
|
|
623
686
|
|
|
687
|
+
### Which request failed (`request`)
|
|
688
|
+
|
|
689
|
+
`login()` makes up to two requests, the second one conditionally: `POST /auth/login`,
|
|
690
|
+
followed by `POST /auth/dpop-bind` (RFC 9449 session sender-constraining) **only when
|
|
691
|
+
that login succeeded and a DPoP key pair exists**. Where Web Crypto (`crypto.subtle`)
|
|
692
|
+
is unavailable no key pair is generated, so the bind is skipped and login succeeds with
|
|
693
|
+
a plain Bearer token.
|
|
694
|
+
|
|
695
|
+
Both requests surface the server's `detail` / `description` verbatim as an
|
|
696
|
+
`AuthenticationError` (or one of its subclasses, `PasswordResetRequiredError` /
|
|
697
|
+
`TemporaryPasswordExpiredError`). When the server answers both with the same body, the
|
|
698
|
+
error alone cannot tell a rejected credential from a rejected bind.
|
|
699
|
+
|
|
700
|
+
Errors from those two requests carry a `request` property (`{ method, path }`), and
|
|
701
|
+
their message is prefixed with `[METHOD path]`.
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
try {
|
|
705
|
+
await db.login(email, password);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
if (err instanceof AuthenticationError) {
|
|
708
|
+
if (err.request?.path === '/auth/dpop-bind') {
|
|
709
|
+
// The credentials were accepted; the DPoP bind is what failed.
|
|
710
|
+
}
|
|
711
|
+
console.error(err.message); // "[POST /auth/dpop-bind] Access denied: no applicable policy"
|
|
712
|
+
} else {
|
|
713
|
+
console.error(err);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
| Property | Type | Description |
|
|
719
|
+
|----------|------|-------------|
|
|
720
|
+
| `request` | `RequestContext \| undefined` | `{ method, path }`. `path` is the endpoint path without the base URL — if the base URL itself carries a path prefix, the request path on the wire includes that prefix too. |
|
|
721
|
+
|
|
722
|
+
Only `POST /auth/login` (`AuthenticationError`, `PasswordResetRequiredError`,
|
|
723
|
+
`TemporaryPasswordExpiredError`) and `POST /auth/dpop-bind` set it. Other auth failures
|
|
724
|
+
already name their step in the message (`Token refresh failed: <status>` and friends),
|
|
725
|
+
so they leave `request` as `undefined` — but `statusCode` still reflects the real HTTP
|
|
726
|
+
status. When the bind never runs — no Web Crypto, or a
|
|
727
|
+
failed `/auth/login` — there is no `/auth/dpop-bind` error to attribute in the first
|
|
728
|
+
place.
|
|
729
|
+
|
|
730
|
+
> `PasswordResetRequiredError` / `TemporaryPasswordExpiredError` are control-flow
|
|
731
|
+
> errors whose `message` an app may render as a prompt. With the prefix that text reads
|
|
732
|
+
> `[POST /auth/login] Password reset required: …`, so branch on `err.request` and supply
|
|
733
|
+
> your own wording rather than displaying the message verbatim.
|
|
734
|
+
|
|
624
735
|
## React Hooks (`@geolonia/geonicdb-sdk/react`)
|
|
625
736
|
|
|
626
737
|
Optional React integration, shipped as a separate subpath so the core SDK stays
|
package/geonicdb.cjs
CHANGED
|
@@ -296,13 +296,20 @@ var GeonicDBError = class extends Error {
|
|
|
296
296
|
super(message);
|
|
297
297
|
/** HTTP status code (if applicable) */
|
|
298
298
|
__publicField(this, "statusCode");
|
|
299
|
+
/** The request that failed, when the SDK knows it. */
|
|
300
|
+
__publicField(this, "request");
|
|
299
301
|
this.name = "GeonicDBError";
|
|
300
302
|
this.statusCode = statusCode;
|
|
301
303
|
}
|
|
302
304
|
};
|
|
305
|
+
function withRequest(error, request) {
|
|
306
|
+
error.request = request;
|
|
307
|
+
error.message = `[${request.method} ${request.path}] ${error.message}`;
|
|
308
|
+
return error;
|
|
309
|
+
}
|
|
303
310
|
var AuthenticationError = class extends GeonicDBError {
|
|
304
|
-
constructor(message = "Authentication failed") {
|
|
305
|
-
super(message,
|
|
311
|
+
constructor(message = "Authentication failed", statusCode = 401) {
|
|
312
|
+
super(message, statusCode);
|
|
306
313
|
this.name = "AuthenticationError";
|
|
307
314
|
}
|
|
308
315
|
};
|
|
@@ -326,16 +333,14 @@ var ConflictError = class extends GeonicDBError {
|
|
|
326
333
|
};
|
|
327
334
|
var PasswordResetRequiredError = class extends AuthenticationError {
|
|
328
335
|
constructor(message = "Password reset required: set a new password to complete login") {
|
|
329
|
-
super(message);
|
|
336
|
+
super(message, 409);
|
|
330
337
|
this.name = "PasswordResetRequiredError";
|
|
331
|
-
this.statusCode = 409;
|
|
332
338
|
}
|
|
333
339
|
};
|
|
334
340
|
var TemporaryPasswordExpiredError = class extends AuthenticationError {
|
|
335
341
|
constructor(message = "Temporary password expired: ask an administrator to re-issue it") {
|
|
336
|
-
super(message);
|
|
342
|
+
super(message, 403);
|
|
337
343
|
this.name = "TemporaryPasswordExpiredError";
|
|
338
|
-
this.statusCode = 403;
|
|
339
344
|
}
|
|
340
345
|
};
|
|
341
346
|
var ValidationError = class extends GeonicDBError {
|
|
@@ -740,12 +745,21 @@ var _AuthManager = class _AuthManager {
|
|
|
740
745
|
const message = e.detail || e.description || "Login failed: " + res.status;
|
|
741
746
|
const code = e.error || e.type || e.title || "";
|
|
742
747
|
if (res.status === 409 && code.includes("PasswordResetRequired")) {
|
|
743
|
-
throw new PasswordResetRequiredError(message)
|
|
748
|
+
throw withRequest(new PasswordResetRequiredError(message), {
|
|
749
|
+
method: "POST",
|
|
750
|
+
path: "/auth/login"
|
|
751
|
+
});
|
|
744
752
|
}
|
|
745
753
|
if (res.status === 403 && code.includes("TemporaryPasswordExpired")) {
|
|
746
|
-
throw new TemporaryPasswordExpiredError(message)
|
|
754
|
+
throw withRequest(new TemporaryPasswordExpiredError(message), {
|
|
755
|
+
method: "POST",
|
|
756
|
+
path: "/auth/login"
|
|
757
|
+
});
|
|
747
758
|
}
|
|
748
|
-
throw new AuthenticationError(message)
|
|
759
|
+
throw withRequest(new AuthenticationError(message, res.status), {
|
|
760
|
+
method: "POST",
|
|
761
|
+
path: "/auth/login"
|
|
762
|
+
});
|
|
749
763
|
}
|
|
750
764
|
const data = await res.json();
|
|
751
765
|
this._anonymous = false;
|
|
@@ -848,8 +862,12 @@ var _AuthManager = class _AuthManager {
|
|
|
848
862
|
}
|
|
849
863
|
if (!res.ok) {
|
|
850
864
|
const e = await res.json().catch(() => ({}));
|
|
851
|
-
throw
|
|
852
|
-
|
|
865
|
+
throw withRequest(
|
|
866
|
+
new AuthenticationError(
|
|
867
|
+
e.detail || e.description || "DPoP bind failed: " + res.status,
|
|
868
|
+
res.status
|
|
869
|
+
),
|
|
870
|
+
{ method: "POST", path: "/auth/dpop-bind" }
|
|
853
871
|
);
|
|
854
872
|
}
|
|
855
873
|
return await res.json();
|
|
@@ -1021,7 +1039,7 @@ var _AuthManager = class _AuthManager {
|
|
|
1021
1039
|
this._refreshToken = null;
|
|
1022
1040
|
this._token = null;
|
|
1023
1041
|
this._tokenPromise = null;
|
|
1024
|
-
throw new AuthenticationError("Token refresh failed: " + res.status);
|
|
1042
|
+
throw new AuthenticationError("Token refresh failed: " + res.status, res.status);
|
|
1025
1043
|
}
|
|
1026
1044
|
const data = await res.json();
|
|
1027
1045
|
this._token = data.accessToken;
|
|
@@ -1089,7 +1107,7 @@ var _AuthManager = class _AuthManager {
|
|
|
1089
1107
|
body: JSON.stringify({ api_key: this._apiKey })
|
|
1090
1108
|
});
|
|
1091
1109
|
if (!nonceRes.ok)
|
|
1092
|
-
throw new AuthenticationError("Nonce request failed: " + nonceRes.status);
|
|
1110
|
+
throw new AuthenticationError("Nonce request failed: " + nonceRes.status, nonceRes.status);
|
|
1093
1111
|
const nonceData = await nonceRes.json();
|
|
1094
1112
|
if (nonceData.dpop_nonce) this._dpopNonce = nonceData.dpop_nonce;
|
|
1095
1113
|
const proof = await solvePoW(nonceData.challenge, nonceData.difficulty);
|
|
@@ -1102,7 +1120,7 @@ var _AuthManager = class _AuthManager {
|
|
|
1102
1120
|
});
|
|
1103
1121
|
const res = await this._doTokenExchange(tokenUrl, tokenBody, this._dpopNonce);
|
|
1104
1122
|
if (!res.ok)
|
|
1105
|
-
throw new AuthenticationError("Token request failed: " + res.status);
|
|
1123
|
+
throw new AuthenticationError("Token request failed: " + res.status, res.status);
|
|
1106
1124
|
const newNonce = res.headers.get("DPoP-Nonce");
|
|
1107
1125
|
if (newNonce) this._dpopNonce = newNonce;
|
|
1108
1126
|
const data = await res.json();
|
|
@@ -1144,8 +1162,8 @@ var _AuthManager = class _AuthManager {
|
|
|
1144
1162
|
return this._doTokenExchange(tokenUrl, tokenBody, serverNonce, retryCount + 1);
|
|
1145
1163
|
}
|
|
1146
1164
|
}
|
|
1147
|
-
throw new
|
|
1148
|
-
"Token request failed: " + (errBody.error_description || errBody.error),
|
|
1165
|
+
throw new AuthenticationError(
|
|
1166
|
+
"Token request failed: " + (errBody.error_description || errBody.error || "400"),
|
|
1149
1167
|
400
|
|
1150
1168
|
);
|
|
1151
1169
|
}
|
|
@@ -1246,13 +1264,12 @@ var _AuthManager = class _AuthManager {
|
|
|
1246
1264
|
}
|
|
1247
1265
|
const url = this._baseUrl + path;
|
|
1248
1266
|
const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && token !== null;
|
|
1249
|
-
const
|
|
1267
|
+
const pathname = path.split("?")[0];
|
|
1268
|
+
const isNgsiLdWrite = pathname.startsWith("/ngsi-ld/v1/") && pathname !== "/ngsi-ld/v1/jsonldContexts";
|
|
1269
|
+
const withCoreContext = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && !("@context" in value) ? { ...value, "@context": NGSI_LD_CORE_CONTEXT } : value;
|
|
1250
1270
|
let effectiveBody = body;
|
|
1251
|
-
if (
|
|
1252
|
-
effectiveBody =
|
|
1253
|
-
...effectiveBody,
|
|
1254
|
-
"@context": NGSI_LD_CORE_CONTEXT
|
|
1255
|
-
};
|
|
1271
|
+
if (isNgsiLdWrite && effectiveBody !== void 0) {
|
|
1272
|
+
effectiveBody = Array.isArray(effectiveBody) ? effectiveBody.map(withCoreContext) : withCoreContext(effectiveBody);
|
|
1256
1273
|
}
|
|
1257
1274
|
const bodyStr = effectiveBody !== void 0 ? JSON.stringify(effectiveBody) : void 0;
|
|
1258
1275
|
const doRequest = async (reqToken, dpopNonce) => {
|
|
@@ -1326,6 +1343,7 @@ var WebSocketManager = class {
|
|
|
1326
1343
|
__publicField(this, "_emit");
|
|
1327
1344
|
__publicField(this, "_ws", null);
|
|
1328
1345
|
__publicField(this, "_wsEndpoint", null);
|
|
1346
|
+
__publicField(this, "_deploymentHostname", null);
|
|
1329
1347
|
__publicField(this, "_wsIntentionalClose", false);
|
|
1330
1348
|
__publicField(this, "_reconnectAttempts", 0);
|
|
1331
1349
|
__publicField(this, "_tokenRefreshTimer", null);
|
|
@@ -1398,6 +1416,7 @@ var WebSocketManager = class {
|
|
|
1398
1416
|
if (options.entityTypes) msg.entityTypes = options.entityTypes;
|
|
1399
1417
|
if (options.idPattern) msg.idPattern = options.idPattern;
|
|
1400
1418
|
if (options.scopeQ) msg.scopeQ = options.scopeQ;
|
|
1419
|
+
if (options["@context"]) msg["@context"] = options["@context"];
|
|
1401
1420
|
}
|
|
1402
1421
|
this._subscription = msg;
|
|
1403
1422
|
if (this._pendingSubscription) return;
|
|
@@ -1414,6 +1433,9 @@ var WebSocketManager = class {
|
|
|
1414
1433
|
const info = await res.json();
|
|
1415
1434
|
if (info.enabled && info.endpoint) {
|
|
1416
1435
|
this._wsEndpoint = info.endpoint;
|
|
1436
|
+
if (info.deployment) {
|
|
1437
|
+
this._deploymentHostname = info.deployment;
|
|
1438
|
+
}
|
|
1417
1439
|
return this._wsEndpoint;
|
|
1418
1440
|
}
|
|
1419
1441
|
}
|
|
@@ -1425,7 +1447,11 @@ var WebSocketManager = class {
|
|
|
1425
1447
|
async _openWebSocket(token) {
|
|
1426
1448
|
const endpoint = await this._discoverWsEndpoint();
|
|
1427
1449
|
return new Promise((resolve, reject) => {
|
|
1428
|
-
const
|
|
1450
|
+
const params = new URLSearchParams({ tenant: this._tenant });
|
|
1451
|
+
if (this._deploymentHostname) {
|
|
1452
|
+
params.set("deployment", this._deploymentHostname);
|
|
1453
|
+
}
|
|
1454
|
+
const wsUrl = endpoint + (endpoint.indexOf("?") === -1 ? "?" : "&") + params.toString();
|
|
1429
1455
|
this._log("connecting", wsUrl);
|
|
1430
1456
|
const ws = new WebSocket(wsUrl, [SUB_PROTOCOL, token]);
|
|
1431
1457
|
this._ws = ws;
|
|
@@ -1465,6 +1491,10 @@ var WebSocketManager = class {
|
|
|
1465
1491
|
return;
|
|
1466
1492
|
}
|
|
1467
1493
|
if (msg.type === "pong") return;
|
|
1494
|
+
if (msg.type === "subscribed") {
|
|
1495
|
+
this._emit("subscribed", void 0);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1468
1498
|
if (msg.type === "dpop_verified") {
|
|
1469
1499
|
this._pendingSubscription = false;
|
|
1470
1500
|
if (this._subscription) {
|
|
@@ -1720,6 +1750,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
1720
1750
|
if (!tenant) tenant = script?.getAttribute?.("data-tenant") || "";
|
|
1721
1751
|
if (!baseUrl) baseUrl = script?.getAttribute?.("data-base-url") || "";
|
|
1722
1752
|
}
|
|
1753
|
+
baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1723
1754
|
this._auth = new AuthManager(baseUrl, apiKey, tenant, opts.debug, opts.anonymous);
|
|
1724
1755
|
this._auth.onTokenRefresh = (creds) => {
|
|
1725
1756
|
this.onTokenRefresh?.(creds);
|
|
@@ -1931,7 +1962,9 @@ var GeonicDB = class extends EventEmitter {
|
|
|
1931
1962
|
limit: 0,
|
|
1932
1963
|
type: params?.type,
|
|
1933
1964
|
q: params?.q,
|
|
1934
|
-
scopeQ: params?.scopeQ
|
|
1965
|
+
scopeQ: params?.scopeQ,
|
|
1966
|
+
// #2278: clause 5.7.2.4 の "too wide query" 免除条件 (e) local scope。
|
|
1967
|
+
local: params?.local
|
|
1935
1968
|
});
|
|
1936
1969
|
const res = await this._auth.request("GET", path);
|
|
1937
1970
|
if (!res.ok) {
|
|
@@ -2028,7 +2061,21 @@ var GeonicDB = class extends EventEmitter {
|
|
|
2028
2061
|
"Batch create failed"
|
|
2029
2062
|
);
|
|
2030
2063
|
}
|
|
2031
|
-
/**
|
|
2064
|
+
/**
|
|
2065
|
+
* Upsert multiple entities in a single request.
|
|
2066
|
+
*
|
|
2067
|
+
* #1742: the server follows ETSI GS CIM 009 clause 5.6.8.5 / Table 6.15.3.1-1:
|
|
2068
|
+
* - **201** with a `string[]` of the **created** entity IDs, when at least one entity
|
|
2069
|
+
* was newly created and none failed (merely updated entities are not listed);
|
|
2070
|
+
* - **204** with no body, when every entity already existed and was updated
|
|
2071
|
+
* (`_jsonPost` maps this to `{}`);
|
|
2072
|
+
* - **207** with a `BatchOperationResult` (`{ created, updated, errors }`), on partial
|
|
2073
|
+
* or total failure.
|
|
2074
|
+
*
|
|
2075
|
+
* The return type reflects all three shapes. Narrow with `Array.isArray(result)` to
|
|
2076
|
+
* detect the "something was created" case.
|
|
2077
|
+
* https://cim.etsi.org/NGSI-LD/official/clause-6.html
|
|
2078
|
+
*/
|
|
2032
2079
|
async batchUpsert(entities) {
|
|
2033
2080
|
return this._jsonPost(
|
|
2034
2081
|
"/ngsi-ld/v1/entityOperations/upsert",
|