@geolonia/geonicdb-sdk 0.13.0 → 0.15.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
@@ -115,6 +115,30 @@ On runtimes without Web Crypto (`subtle.generateKey`, e.g. a non-HTTPS
115
115
  context), the session cannot be sender-constrained: the SDK silently keeps
116
116
  the plain Bearer session and **login still succeeds** (`tokenType === 'Bearer'`).
117
117
 
118
+ #### First-login forced password change (#1532)
119
+
120
+ When an administrator issues a **temporary password** — either at creation
121
+ (`POST /admin/users` with `passwordResetRequired: true`) or afterwards
122
+ (`POST /admin/users/{userId}/reset-password`) — the user must set a new password
123
+ on first login. Logging in with only the temporary password throws
124
+ `PasswordResetRequiredError`; supply `newPassword` to set the new password and
125
+ receive tokens in the same call (no re-login needed):
126
+
127
+ ```typescript
128
+ import { PasswordResetRequiredError, TemporaryPasswordExpiredError } from '@geolonia/geonicdb-sdk';
129
+
130
+ try {
131
+ await db.login('user@example.com', temporaryPassword);
132
+ } catch (err) {
133
+ if (err instanceof PasswordResetRequiredError) {
134
+ // Prompt the user for a new password, then complete login in one call:
135
+ await db.login('user@example.com', temporaryPassword, { newPassword });
136
+ } else if (err instanceof TemporaryPasswordExpiredError) {
137
+ // Ask an administrator to re-issue the temporary password.
138
+ }
139
+ }
140
+ ```
141
+
118
142
  #### Keeping the user signed in across page reloads
119
143
 
120
144
  By default a session is held in memory only — reloading the page loses it
@@ -196,6 +220,11 @@ const entities = await db.getEntities({ type: 'GeoJSON' });
196
220
 
197
221
  ## Entity CRUD
198
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).
227
+
199
228
  ```typescript
200
229
  // Create
201
230
  await db.createEntity({ id: 'urn:ngsi-ld:Room:001', type: 'Room', ... });
@@ -633,12 +662,39 @@ const rooms = await client.getEntities({ type: 'Room', limit: 20 });
633
662
  | Method | Description |
634
663
  |--------|-------------|
635
664
  | `createNgsiV2Client(config)` | `NgsiV2Client` を生成。`config`: `baseUrl`(必須)/ `apiKey` / `service` |
636
- | `getEntities(options?)` | エンティティ一覧(`type` / `q` / `limit` / `offset` / `attrs` / `options`) |
665
+ | `getEntities(options?)` | エンティティ一覧(`type` / `q` / `limit` / `offset` / `attrs` / `options` / `georel` / `geometry` / `coords`) |
637
666
  | `getEntity(id, options?)` | 単一エンティティ取得 |
638
667
  | `createEntity(entity)` | エンティティ作成 |
639
668
  | `updateEntityAttributes(id, attrs)` | 属性の部分更新 |
640
669
  | `deleteEntity(id)` | エンティティ削除 |
641
670
 
671
+ ### Geospatial query (`georel` / `geometry` / `coords`)
672
+
673
+ NGSIv2 の地理空間クエリに対応。`georel`・`geometry`・`coords` は 3 つ揃えて指定する(部分指定は Orion 側で `400`)。
674
+
675
+ | Option | Type | Description |
676
+ |--------|------|-------------|
677
+ | `georel` | `string` | 地理的関係。複合形も可(例: `near;maxDistance:1000`, `coveredBy`) |
678
+ | `geometry` | `'point' \| 'line' \| 'polygon' \| 'box'` | ジオメトリ種別 |
679
+ | `coords` | `string` | WGS84 座標。複数点はセミコロン区切り(例: `35.6,139.7`) |
680
+
681
+ ```ts
682
+ // 指定座標から半径 1km 以内
683
+ const nearby = await client.getEntities({
684
+ type: 'PublicFacility',
685
+ georel: 'near;maxDistance:1000',
686
+ geometry: 'point',
687
+ coords: '35.6,139.7',
688
+ });
689
+
690
+ // ポリゴン内に含まれるエンティティ
691
+ const inArea = await client.getEntities({
692
+ georel: 'coveredBy',
693
+ geometry: 'polygon',
694
+ coords: '0,0;1,0;1,1;0,1;0,0',
695
+ });
696
+ ```
697
+
642
698
  ## License
643
699
 
644
700
  MIT
package/geonicdb.cjs CHANGED
@@ -29,7 +29,9 @@ __export(index_exports, {
29
29
  GeonicDBError: () => GeonicDBError,
30
30
  NetworkError: () => NetworkError,
31
31
  NotFoundError: () => NotFoundError,
32
+ PasswordResetRequiredError: () => PasswordResetRequiredError,
32
33
  RateLimitError: () => RateLimitError,
34
+ TemporaryPasswordExpiredError: () => TemporaryPasswordExpiredError,
33
35
  ValidationError: () => ValidationError,
34
36
  buildPathWithParams: () => buildPathWithParams,
35
37
  default: () => index_default
@@ -80,6 +82,7 @@ var RECONNECT_MAX_ATTEMPTS = 10;
80
82
  var RECONNECT_BASE_MS = 1e3;
81
83
  var RECONNECT_MAX_DELAY_MS = 3e4;
82
84
  var SUB_PROTOCOL = "access_token";
85
+ var NGSI_LD_CORE_CONTEXT = "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld";
83
86
  var SDK_CACHE_MAX_ENTRIES_DEFAULT = 1e3;
84
87
  var SDK_POLL_INTERVAL_MS_DEFAULT = 5e3;
85
88
  var DPOP_IDB_NAME = "geonicdb-sdk";
@@ -321,6 +324,20 @@ var ConflictError = class extends GeonicDBError {
321
324
  this.name = "ConflictError";
322
325
  }
323
326
  };
327
+ var PasswordResetRequiredError = class extends AuthenticationError {
328
+ constructor(message = "Password reset required: set a new password to complete login") {
329
+ super(message);
330
+ this.name = "PasswordResetRequiredError";
331
+ this.statusCode = 409;
332
+ }
333
+ };
334
+ var TemporaryPasswordExpiredError = class extends AuthenticationError {
335
+ constructor(message = "Temporary password expired: ask an administrator to re-issue it") {
336
+ super(message);
337
+ this.name = "TemporaryPasswordExpiredError";
338
+ this.statusCode = 403;
339
+ }
340
+ };
324
341
  var ValidationError = class extends GeonicDBError {
325
342
  constructor(message = "Validation failed") {
326
343
  super(message, 422);
@@ -711,16 +728,24 @@ var _AuthManager = class _AuthManager {
711
728
  "Content-Type": "application/json"
712
729
  };
713
730
  if (this._tenant) headers["NGSILD-Tenant"] = this._tenant;
731
+ const loginBody = { email, password };
732
+ if (options?.newPassword !== void 0) loginBody.newPassword = options.newPassword;
714
733
  const res = await fetch(this._baseUrl + "/auth/login", {
715
734
  method: "POST",
716
735
  headers,
717
- body: JSON.stringify({ email, password })
736
+ body: JSON.stringify(loginBody)
718
737
  });
719
738
  if (!res.ok) {
720
739
  const e = await res.json().catch(() => ({}));
721
- throw new AuthenticationError(
722
- e.detail || e.description || "Login failed: " + res.status
723
- );
740
+ const message = e.detail || e.description || "Login failed: " + res.status;
741
+ const code = e.error || e.type || e.title || "";
742
+ if (res.status === 409 && code.includes("PasswordResetRequired")) {
743
+ throw new PasswordResetRequiredError(message);
744
+ }
745
+ if (res.status === 403 && code.includes("TemporaryPasswordExpired")) {
746
+ throw new TemporaryPasswordExpiredError(message);
747
+ }
748
+ throw new AuthenticationError(message);
724
749
  }
725
750
  const data = await res.json();
726
751
  this._anonymous = false;
@@ -1221,7 +1246,15 @@ var _AuthManager = class _AuthManager {
1221
1246
  }
1222
1247
  const url = this._baseUrl + path;
1223
1248
  const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && token !== null;
1224
- const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
1249
+ const isEntityWrite = path.split("?")[0].startsWith("/ngsi-ld/v1/entities");
1250
+ 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
+ };
1256
+ }
1257
+ const bodyStr = effectiveBody !== void 0 ? JSON.stringify(effectiveBody) : void 0;
1225
1258
  const doRequest = async (reqToken, dpopNonce) => {
1226
1259
  const reqIsDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && reqToken !== null;
1227
1260
  const dpopProof = reqIsDPoP ? await createDPoPProof(