@geolonia/geonicdb-sdk 0.2.1 → 0.3.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 +36 -1
- package/geonicdb.cjs +125 -36
- package/geonicdb.cjs.map +3 -3
- package/geonicdb.iife.js +125 -36
- package/geonicdb.iife.js.map +3 -3
- package/geonicdb.mjs +125 -36
- package/geonicdb.mjs.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,7 +153,8 @@ const result = await db.request('GET', '/ngsi-ld/v1/subscriptions');
|
|
|
153
153
|
db.subscribe({ entityTypes: ['Room'] });
|
|
154
154
|
|
|
155
155
|
db.on('entityUpdated', (event) => {
|
|
156
|
-
|
|
156
|
+
// event.entity contains the complete NGSI-LD entity ({ id, type, ...attributes })
|
|
157
|
+
console.log(`${event.entityId} updated:`, event.entity);
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
db.on('error', (err) => console.error(err));
|
|
@@ -192,6 +193,7 @@ await db.connect();
|
|
|
192
193
|
| `tenant` | `string` | No | Tenant name (or use `data-tenant` attribute on script tag) |
|
|
193
194
|
| `baseUrl` | `string` | No | API base URL (auto-detected from script `src` if omitted) |
|
|
194
195
|
| `wsEndpoint` | `string` | No | WebSocket endpoint URL (auto-detected from `baseUrl` if omitted) |
|
|
196
|
+
| `debug` | `boolean` | No | Enable debug logging to console (default: `false`) |
|
|
195
197
|
|
|
196
198
|
### Authentication
|
|
197
199
|
|
|
@@ -300,6 +302,39 @@ await db.connect();
|
|
|
300
302
|
| `on(event, listener)` | `string, Function` | `this` | Register an event listener |
|
|
301
303
|
| `off(event, listener)` | `string, Function` | `this` | Remove an event listener |
|
|
302
304
|
|
|
305
|
+
## Error Handling
|
|
306
|
+
|
|
307
|
+
All async methods throw typed errors extending `GeonicDBError`. Use `instanceof` to distinguish error types.
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { GeonicDBError, AuthenticationError, NotFoundError } from '@geolonia/geonicdb-sdk';
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await db.getEntity('urn:ngsi-ld:Room:404');
|
|
314
|
+
} catch (err) {
|
|
315
|
+
if (err instanceof AuthenticationError) {
|
|
316
|
+
// 401 — re-login needed
|
|
317
|
+
} else if (err instanceof NotFoundError) {
|
|
318
|
+
// 404 — entity not found
|
|
319
|
+
} else if (err instanceof GeonicDBError) {
|
|
320
|
+
console.error(err.message, err.statusCode);
|
|
321
|
+
} else {
|
|
322
|
+
console.error(err);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
| Class | Status | Description |
|
|
328
|
+
|-------|--------|-------------|
|
|
329
|
+
| `GeonicDBError` | — | Base class (`statusCode` property) |
|
|
330
|
+
| `AuthenticationError` | 401 | Invalid credentials or expired token |
|
|
331
|
+
| `AuthorizationError` | 403 | Insufficient permissions |
|
|
332
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
333
|
+
| `ConflictError` | 409 | Entity already exists |
|
|
334
|
+
| `ValidationError` | 422 | Bad request payload |
|
|
335
|
+
| `RateLimitError` | 429 | Rate limited (`retryAfter` property) |
|
|
336
|
+
| `NetworkError` | — | Fetch failure, DNS error, timeout |
|
|
337
|
+
|
|
303
338
|
## License
|
|
304
339
|
|
|
305
340
|
MIT
|
package/geonicdb.cjs
CHANGED
|
@@ -22,7 +22,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
22
22
|
// src/sdk/index.ts
|
|
23
23
|
var index_exports = {};
|
|
24
24
|
__export(index_exports, {
|
|
25
|
+
AuthenticationError: () => AuthenticationError,
|
|
26
|
+
AuthorizationError: () => AuthorizationError,
|
|
27
|
+
ConflictError: () => ConflictError,
|
|
25
28
|
GeonicDB: () => GeonicDB,
|
|
29
|
+
GeonicDBError: () => GeonicDBError,
|
|
30
|
+
NetworkError: () => NetworkError,
|
|
31
|
+
NotFoundError: () => NotFoundError,
|
|
32
|
+
RateLimitError: () => RateLimitError,
|
|
33
|
+
ValidationError: () => ValidationError,
|
|
26
34
|
default: () => index_default
|
|
27
35
|
});
|
|
28
36
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -173,12 +181,88 @@ var RECONNECT_BASE_MS = 1e3;
|
|
|
173
181
|
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
174
182
|
var SUB_PROTOCOL = "access_token";
|
|
175
183
|
|
|
184
|
+
// src/sdk/errors.ts
|
|
185
|
+
var GeonicDBError = class extends Error {
|
|
186
|
+
constructor(message, statusCode = 0) {
|
|
187
|
+
super(message);
|
|
188
|
+
/** HTTP status code (if applicable) */
|
|
189
|
+
__publicField(this, "statusCode");
|
|
190
|
+
this.name = "GeonicDBError";
|
|
191
|
+
this.statusCode = statusCode;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var AuthenticationError = class extends GeonicDBError {
|
|
195
|
+
constructor(message = "Authentication failed") {
|
|
196
|
+
super(message, 401);
|
|
197
|
+
this.name = "AuthenticationError";
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var AuthorizationError = class extends GeonicDBError {
|
|
201
|
+
constructor(message = "Access denied") {
|
|
202
|
+
super(message, 403);
|
|
203
|
+
this.name = "AuthorizationError";
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var NotFoundError = class extends GeonicDBError {
|
|
207
|
+
constructor(message = "Not found") {
|
|
208
|
+
super(message, 404);
|
|
209
|
+
this.name = "NotFoundError";
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var ConflictError = class extends GeonicDBError {
|
|
213
|
+
constructor(message = "Conflict") {
|
|
214
|
+
super(message, 409);
|
|
215
|
+
this.name = "ConflictError";
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var ValidationError = class extends GeonicDBError {
|
|
219
|
+
constructor(message = "Validation failed") {
|
|
220
|
+
super(message, 422);
|
|
221
|
+
this.name = "ValidationError";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var RateLimitError = class extends GeonicDBError {
|
|
225
|
+
constructor(message = "Rate limit exceeded", retryAfter = 1) {
|
|
226
|
+
super(message, 429);
|
|
227
|
+
/** Seconds to wait before retrying (from Retry-After header) */
|
|
228
|
+
__publicField(this, "retryAfter");
|
|
229
|
+
this.name = "RateLimitError";
|
|
230
|
+
this.retryAfter = retryAfter;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var NetworkError = class extends GeonicDBError {
|
|
234
|
+
constructor(message = "Network error") {
|
|
235
|
+
super(message, 0);
|
|
236
|
+
this.name = "NetworkError";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
function createErrorFromResponse(status, body, fallbackMessage) {
|
|
240
|
+
const message = body.detail || body.description || fallbackMessage;
|
|
241
|
+
switch (status) {
|
|
242
|
+
case 401:
|
|
243
|
+
return new AuthenticationError(message);
|
|
244
|
+
case 403:
|
|
245
|
+
return new AuthorizationError(message);
|
|
246
|
+
case 404:
|
|
247
|
+
return new NotFoundError(message);
|
|
248
|
+
case 409:
|
|
249
|
+
return new ConflictError(message);
|
|
250
|
+
case 422:
|
|
251
|
+
return new ValidationError(message);
|
|
252
|
+
case 429:
|
|
253
|
+
return new RateLimitError(message);
|
|
254
|
+
default:
|
|
255
|
+
return new GeonicDBError(message, status);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
176
259
|
// src/sdk/auth.ts
|
|
177
260
|
var _AuthManager = class _AuthManager {
|
|
178
|
-
constructor(baseUrl, apiKey, tenant) {
|
|
261
|
+
constructor(baseUrl, apiKey, tenant, debug = false) {
|
|
179
262
|
__publicField(this, "_baseUrl");
|
|
180
263
|
__publicField(this, "_apiKey");
|
|
181
264
|
__publicField(this, "_tenant");
|
|
265
|
+
__publicField(this, "_debug");
|
|
182
266
|
__publicField(this, "_token", null);
|
|
183
267
|
__publicField(this, "_tokenExpiry", 0);
|
|
184
268
|
__publicField(this, "_tokenType", "Bearer");
|
|
@@ -192,6 +276,7 @@ var _AuthManager = class _AuthManager {
|
|
|
192
276
|
this._baseUrl = baseUrl;
|
|
193
277
|
this._apiKey = apiKey;
|
|
194
278
|
this._tenant = tenant;
|
|
279
|
+
this._debug = debug;
|
|
195
280
|
if (dpopSupported) {
|
|
196
281
|
this._dpopReady = generateDPoPKeyPair().then((kp) => {
|
|
197
282
|
this._dpopKeyPair = kp;
|
|
@@ -200,8 +285,12 @@ var _AuthManager = class _AuthManager {
|
|
|
200
285
|
});
|
|
201
286
|
}
|
|
202
287
|
}
|
|
288
|
+
_log(...args) {
|
|
289
|
+
if (this._debug) console.log("[GeonicDB]", ...args);
|
|
290
|
+
}
|
|
203
291
|
/** Login with email and password (Bearer JWT). */
|
|
204
292
|
async login(email, password) {
|
|
293
|
+
this._log("login", email);
|
|
205
294
|
const headers = {
|
|
206
295
|
"Content-Type": "application/json"
|
|
207
296
|
};
|
|
@@ -213,7 +302,7 @@ var _AuthManager = class _AuthManager {
|
|
|
213
302
|
});
|
|
214
303
|
if (!res.ok) {
|
|
215
304
|
const e = await res.json().catch(() => ({}));
|
|
216
|
-
throw new
|
|
305
|
+
throw new AuthenticationError(
|
|
217
306
|
e.detail || e.description || "Login failed: " + res.status
|
|
218
307
|
);
|
|
219
308
|
}
|
|
@@ -260,6 +349,7 @@ var _AuthManager = class _AuthManager {
|
|
|
260
349
|
return this._tokenPromise;
|
|
261
350
|
}
|
|
262
351
|
async _refreshBearerToken() {
|
|
352
|
+
this._log("refreshing Bearer token");
|
|
263
353
|
try {
|
|
264
354
|
const res = await fetch(this._baseUrl + "/auth/refresh", {
|
|
265
355
|
method: "POST",
|
|
@@ -270,7 +360,7 @@ var _AuthManager = class _AuthManager {
|
|
|
270
360
|
this._refreshToken = null;
|
|
271
361
|
this._token = null;
|
|
272
362
|
this._tokenPromise = null;
|
|
273
|
-
throw new
|
|
363
|
+
throw new AuthenticationError("Token refresh failed: " + res.status);
|
|
274
364
|
}
|
|
275
365
|
const data = await res.json();
|
|
276
366
|
this._token = data.accessToken;
|
|
@@ -299,7 +389,7 @@ var _AuthManager = class _AuthManager {
|
|
|
299
389
|
body: JSON.stringify({ api_key: this._apiKey })
|
|
300
390
|
});
|
|
301
391
|
if (!nonceRes.ok)
|
|
302
|
-
throw new
|
|
392
|
+
throw new AuthenticationError("Nonce request failed: " + nonceRes.status);
|
|
303
393
|
const nonceData = await nonceRes.json();
|
|
304
394
|
if (nonceData.dpop_nonce) this._dpopNonce = nonceData.dpop_nonce;
|
|
305
395
|
const proof = await solvePoW(nonceData.challenge, nonceData.difficulty);
|
|
@@ -312,7 +402,7 @@ var _AuthManager = class _AuthManager {
|
|
|
312
402
|
});
|
|
313
403
|
const res = await this._doTokenExchange(tokenUrl, tokenBody, this._dpopNonce);
|
|
314
404
|
if (!res.ok)
|
|
315
|
-
throw new
|
|
405
|
+
throw new AuthenticationError("Token request failed: " + res.status);
|
|
316
406
|
const newNonce = res.headers.get("DPoP-Nonce");
|
|
317
407
|
if (newNonce) this._dpopNonce = newNonce;
|
|
318
408
|
const data = await res.json();
|
|
@@ -352,8 +442,9 @@ var _AuthManager = class _AuthManager {
|
|
|
352
442
|
return this._doTokenExchange(tokenUrl, tokenBody, serverNonce, retryCount + 1);
|
|
353
443
|
}
|
|
354
444
|
}
|
|
355
|
-
throw new
|
|
356
|
-
"Token request failed: " + (errBody.error_description || errBody.error)
|
|
445
|
+
throw new GeonicDBError(
|
|
446
|
+
"Token request failed: " + (errBody.error_description || errBody.error),
|
|
447
|
+
400
|
|
357
448
|
);
|
|
358
449
|
}
|
|
359
450
|
return res;
|
|
@@ -362,8 +453,11 @@ var _AuthManager = class _AuthManager {
|
|
|
362
453
|
* Make an authenticated HTTP request with automatic token refresh and DPoP.
|
|
363
454
|
*/
|
|
364
455
|
async request(method, path, body) {
|
|
456
|
+
this._log(method, path);
|
|
365
457
|
const token = await this.ensureToken();
|
|
366
|
-
|
|
458
|
+
const res = await this._doAuthenticatedRequest(method, path, body, token);
|
|
459
|
+
this._log(method, path, "\u2192", res.status);
|
|
460
|
+
return res;
|
|
367
461
|
}
|
|
368
462
|
async _doAuthenticatedRequest(method, path, body, token, retryCount = 0) {
|
|
369
463
|
const url = this._baseUrl + path;
|
|
@@ -448,6 +542,9 @@ var WebSocketManager = class {
|
|
|
448
542
|
this._emit = emit;
|
|
449
543
|
this._wsEndpointOverride = wsEndpointOverride || null;
|
|
450
544
|
}
|
|
545
|
+
_log(...args) {
|
|
546
|
+
if (this._auth._debug) console.log("[GeonicDB:WS]", ...args);
|
|
547
|
+
}
|
|
451
548
|
/** Establish WebSocket connection (authentication is automatic). */
|
|
452
549
|
async connect() {
|
|
453
550
|
if (this._reconnectTimer) {
|
|
@@ -525,10 +622,12 @@ var WebSocketManager = class {
|
|
|
525
622
|
const endpoint = await this._discoverWsEndpoint();
|
|
526
623
|
return new Promise((resolve, reject) => {
|
|
527
624
|
const wsUrl = endpoint + (endpoint.indexOf("?") === -1 ? "?" : "&") + "tenant=" + encodeURIComponent(this._tenant);
|
|
625
|
+
this._log("connecting", wsUrl);
|
|
528
626
|
const ws = new WebSocket(wsUrl, [SUB_PROTOCOL, token]);
|
|
529
627
|
this._ws = ws;
|
|
530
628
|
ws.onopen = () => {
|
|
531
629
|
if (this._ws !== ws) return;
|
|
630
|
+
this._log("connected");
|
|
532
631
|
this._reconnectAttempts = 0;
|
|
533
632
|
const isDPoP = this._auth._tokenType === "DPoP" && !!this._auth._dpopKeyPair;
|
|
534
633
|
const bindPromise = isDPoP ? createDPoPProof(this._auth._dpopKeyPair, "GET", wsUrl, null).then(
|
|
@@ -573,6 +672,14 @@ var WebSocketManager = class {
|
|
|
573
672
|
this._emit("error", new Error(msg.message));
|
|
574
673
|
return;
|
|
575
674
|
}
|
|
675
|
+
this._log("event", msg.type, msg.entityId || "");
|
|
676
|
+
if (msg.entityId && msg.data && typeof msg.data === "object") {
|
|
677
|
+
msg.entity = {
|
|
678
|
+
id: msg.entityId,
|
|
679
|
+
type: msg.entityType,
|
|
680
|
+
...msg.data
|
|
681
|
+
};
|
|
682
|
+
}
|
|
576
683
|
this._emit(msg.type, msg);
|
|
577
684
|
this._emit("message", msg);
|
|
578
685
|
};
|
|
@@ -675,7 +782,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
675
782
|
if (!tenant) tenant = script?.getAttribute?.("data-tenant") || "";
|
|
676
783
|
if (!baseUrl) baseUrl = script?.getAttribute?.("data-base-url") || "";
|
|
677
784
|
}
|
|
678
|
-
this._auth = new AuthManager(baseUrl, apiKey, tenant);
|
|
785
|
+
this._auth = new AuthManager(baseUrl, apiKey, tenant, opts.debug);
|
|
679
786
|
this._auth.onTokenRefresh = (creds) => {
|
|
680
787
|
this.onTokenRefresh?.(creds);
|
|
681
788
|
this.emit("tokenRefresh", creds);
|
|
@@ -712,9 +819,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
712
819
|
const res = await this._auth.request("POST", "/ngsi-ld/v1/entities", entity);
|
|
713
820
|
if (!res.ok) {
|
|
714
821
|
const e = await res.json().catch(() => ({}));
|
|
715
|
-
throw
|
|
716
|
-
e.detail || e.description || "Create failed"
|
|
717
|
-
);
|
|
822
|
+
throw createErrorFromResponse(res.status, e, "Create failed");
|
|
718
823
|
}
|
|
719
824
|
return { created: true };
|
|
720
825
|
}
|
|
@@ -726,9 +831,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
726
831
|
);
|
|
727
832
|
if (!res.ok) {
|
|
728
833
|
const e = await res.json().catch(() => ({}));
|
|
729
|
-
throw
|
|
730
|
-
e.detail || e.description || "Not found"
|
|
731
|
-
);
|
|
834
|
+
throw createErrorFromResponse(res.status, e, "Not found");
|
|
732
835
|
}
|
|
733
836
|
return await res.json();
|
|
734
837
|
}
|
|
@@ -749,9 +852,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
749
852
|
);
|
|
750
853
|
if (!res.ok) {
|
|
751
854
|
const e = await res.json().catch(() => ({}));
|
|
752
|
-
throw
|
|
753
|
-
e.detail || e.description || "Query failed"
|
|
754
|
-
);
|
|
855
|
+
throw createErrorFromResponse(res.status, e, "Query failed");
|
|
755
856
|
}
|
|
756
857
|
return await res.json();
|
|
757
858
|
}
|
|
@@ -767,9 +868,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
767
868
|
);
|
|
768
869
|
if (!res.ok) {
|
|
769
870
|
const e = await res.json().catch(() => ({}));
|
|
770
|
-
throw
|
|
771
|
-
e.detail || e.description || "Count failed"
|
|
772
|
-
);
|
|
871
|
+
throw createErrorFromResponse(res.status, e, "Count failed");
|
|
773
872
|
}
|
|
774
873
|
const countHeader = res.headers.get("NGSILD-Results-Count");
|
|
775
874
|
return countHeader ? parseInt(countHeader, 10) : 0;
|
|
@@ -783,9 +882,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
783
882
|
);
|
|
784
883
|
if (!res.ok) {
|
|
785
884
|
const e = await res.json().catch(() => ({}));
|
|
786
|
-
throw
|
|
787
|
-
e.detail || e.description || "Update failed"
|
|
788
|
-
);
|
|
885
|
+
throw createErrorFromResponse(res.status, e, "Update failed");
|
|
789
886
|
}
|
|
790
887
|
return { updated: true };
|
|
791
888
|
}
|
|
@@ -797,9 +894,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
797
894
|
);
|
|
798
895
|
if (!res.ok) {
|
|
799
896
|
const e = await res.json().catch(() => ({}));
|
|
800
|
-
throw
|
|
801
|
-
e.detail || e.description || "Delete failed"
|
|
802
|
-
);
|
|
897
|
+
throw createErrorFromResponse(res.status, e, "Delete failed");
|
|
803
898
|
}
|
|
804
899
|
return { deleted: true };
|
|
805
900
|
}
|
|
@@ -896,9 +991,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
896
991
|
const res = await this._auth.request("GET", path);
|
|
897
992
|
if (!res.ok) {
|
|
898
993
|
const e = await res.json().catch(() => ({}));
|
|
899
|
-
throw
|
|
900
|
-
e.detail || e.description || fallbackError
|
|
901
|
-
);
|
|
994
|
+
throw createErrorFromResponse(res.status, e, fallbackError);
|
|
902
995
|
}
|
|
903
996
|
return await res.json();
|
|
904
997
|
}
|
|
@@ -907,9 +1000,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
907
1000
|
const res = await this._auth.request("POST", path, body);
|
|
908
1001
|
if (!res.ok) {
|
|
909
1002
|
const e = await res.json().catch(() => ({}));
|
|
910
|
-
throw
|
|
911
|
-
e.detail || e.description || fallbackError
|
|
912
|
-
);
|
|
1003
|
+
throw createErrorFromResponse(res.status, e, fallbackError);
|
|
913
1004
|
}
|
|
914
1005
|
if (res.status === 204) return {};
|
|
915
1006
|
return await res.json();
|
|
@@ -923,9 +1014,7 @@ var GeonicDB = class extends EventEmitter {
|
|
|
923
1014
|
const res = await this._auth.request(method, path, body);
|
|
924
1015
|
if (!res.ok) {
|
|
925
1016
|
const e = await res.json().catch(() => ({}));
|
|
926
|
-
throw
|
|
927
|
-
e.detail || e.description || "Request failed: " + res.status
|
|
928
|
-
);
|
|
1017
|
+
throw createErrorFromResponse(res.status, e, "Request failed: " + res.status);
|
|
929
1018
|
}
|
|
930
1019
|
const ct = res.headers.get("Content-Type") || "";
|
|
931
1020
|
if (res.status === 204 || !ct) return null;
|