@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/geonicdb.mjs CHANGED
@@ -260,13 +260,20 @@ var GeonicDBError = class extends Error {
260
260
  super(message);
261
261
  /** HTTP status code (if applicable) */
262
262
  __publicField(this, "statusCode");
263
+ /** The request that failed, when the SDK knows it. */
264
+ __publicField(this, "request");
263
265
  this.name = "GeonicDBError";
264
266
  this.statusCode = statusCode;
265
267
  }
266
268
  };
269
+ function withRequest(error, request) {
270
+ error.request = request;
271
+ error.message = `[${request.method} ${request.path}] ${error.message}`;
272
+ return error;
273
+ }
267
274
  var AuthenticationError = class extends GeonicDBError {
268
- constructor(message = "Authentication failed") {
269
- super(message, 401);
275
+ constructor(message = "Authentication failed", statusCode = 401) {
276
+ super(message, statusCode);
270
277
  this.name = "AuthenticationError";
271
278
  }
272
279
  };
@@ -290,16 +297,14 @@ var ConflictError = class extends GeonicDBError {
290
297
  };
291
298
  var PasswordResetRequiredError = class extends AuthenticationError {
292
299
  constructor(message = "Password reset required: set a new password to complete login") {
293
- super(message);
300
+ super(message, 409);
294
301
  this.name = "PasswordResetRequiredError";
295
- this.statusCode = 409;
296
302
  }
297
303
  };
298
304
  var TemporaryPasswordExpiredError = class extends AuthenticationError {
299
305
  constructor(message = "Temporary password expired: ask an administrator to re-issue it") {
300
- super(message);
306
+ super(message, 403);
301
307
  this.name = "TemporaryPasswordExpiredError";
302
- this.statusCode = 403;
303
308
  }
304
309
  };
305
310
  var ValidationError = class extends GeonicDBError {
@@ -704,12 +709,21 @@ var _AuthManager = class _AuthManager {
704
709
  const message = e.detail || e.description || "Login failed: " + res.status;
705
710
  const code = e.error || e.type || e.title || "";
706
711
  if (res.status === 409 && code.includes("PasswordResetRequired")) {
707
- throw new PasswordResetRequiredError(message);
712
+ throw withRequest(new PasswordResetRequiredError(message), {
713
+ method: "POST",
714
+ path: "/auth/login"
715
+ });
708
716
  }
709
717
  if (res.status === 403 && code.includes("TemporaryPasswordExpired")) {
710
- throw new TemporaryPasswordExpiredError(message);
718
+ throw withRequest(new TemporaryPasswordExpiredError(message), {
719
+ method: "POST",
720
+ path: "/auth/login"
721
+ });
711
722
  }
712
- throw new AuthenticationError(message);
723
+ throw withRequest(new AuthenticationError(message, res.status), {
724
+ method: "POST",
725
+ path: "/auth/login"
726
+ });
713
727
  }
714
728
  const data = await res.json();
715
729
  this._anonymous = false;
@@ -812,8 +826,12 @@ var _AuthManager = class _AuthManager {
812
826
  }
813
827
  if (!res.ok) {
814
828
  const e = await res.json().catch(() => ({}));
815
- throw new AuthenticationError(
816
- e.detail || e.description || "DPoP bind failed: " + res.status
829
+ throw withRequest(
830
+ new AuthenticationError(
831
+ e.detail || e.description || "DPoP bind failed: " + res.status,
832
+ res.status
833
+ ),
834
+ { method: "POST", path: "/auth/dpop-bind" }
817
835
  );
818
836
  }
819
837
  return await res.json();
@@ -985,7 +1003,7 @@ var _AuthManager = class _AuthManager {
985
1003
  this._refreshToken = null;
986
1004
  this._token = null;
987
1005
  this._tokenPromise = null;
988
- throw new AuthenticationError("Token refresh failed: " + res.status);
1006
+ throw new AuthenticationError("Token refresh failed: " + res.status, res.status);
989
1007
  }
990
1008
  const data = await res.json();
991
1009
  this._token = data.accessToken;
@@ -1053,7 +1071,7 @@ var _AuthManager = class _AuthManager {
1053
1071
  body: JSON.stringify({ api_key: this._apiKey })
1054
1072
  });
1055
1073
  if (!nonceRes.ok)
1056
- throw new AuthenticationError("Nonce request failed: " + nonceRes.status);
1074
+ throw new AuthenticationError("Nonce request failed: " + nonceRes.status, nonceRes.status);
1057
1075
  const nonceData = await nonceRes.json();
1058
1076
  if (nonceData.dpop_nonce) this._dpopNonce = nonceData.dpop_nonce;
1059
1077
  const proof = await solvePoW(nonceData.challenge, nonceData.difficulty);
@@ -1066,7 +1084,7 @@ var _AuthManager = class _AuthManager {
1066
1084
  });
1067
1085
  const res = await this._doTokenExchange(tokenUrl, tokenBody, this._dpopNonce);
1068
1086
  if (!res.ok)
1069
- throw new AuthenticationError("Token request failed: " + res.status);
1087
+ throw new AuthenticationError("Token request failed: " + res.status, res.status);
1070
1088
  const newNonce = res.headers.get("DPoP-Nonce");
1071
1089
  if (newNonce) this._dpopNonce = newNonce;
1072
1090
  const data = await res.json();
@@ -1108,8 +1126,8 @@ var _AuthManager = class _AuthManager {
1108
1126
  return this._doTokenExchange(tokenUrl, tokenBody, serverNonce, retryCount + 1);
1109
1127
  }
1110
1128
  }
1111
- throw new GeonicDBError(
1112
- "Token request failed: " + (errBody.error_description || errBody.error),
1129
+ throw new AuthenticationError(
1130
+ "Token request failed: " + (errBody.error_description || errBody.error || "400"),
1113
1131
  400
1114
1132
  );
1115
1133
  }
@@ -1210,13 +1228,12 @@ var _AuthManager = class _AuthManager {
1210
1228
  }
1211
1229
  const url = this._baseUrl + path;
1212
1230
  const isDPoP = this._tokenType === "DPoP" && !!this._dpopKeyPair && token !== null;
1213
- const isEntityWrite = path.split("?")[0].startsWith("/ngsi-ld/v1/entities");
1231
+ const pathname = path.split("?")[0];
1232
+ const isNgsiLdWrite = pathname.startsWith("/ngsi-ld/v1/") && pathname !== "/ngsi-ld/v1/jsonldContexts";
1233
+ const withCoreContext = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && !("@context" in value) ? { ...value, "@context": NGSI_LD_CORE_CONTEXT } : value;
1214
1234
  let effectiveBody = body;
1215
- if (isEntityWrite && effectiveBody !== null && typeof effectiveBody === "object" && !Array.isArray(effectiveBody) && !("@context" in effectiveBody)) {
1216
- effectiveBody = {
1217
- ...effectiveBody,
1218
- "@context": NGSI_LD_CORE_CONTEXT
1219
- };
1235
+ if (isNgsiLdWrite && effectiveBody !== void 0) {
1236
+ effectiveBody = Array.isArray(effectiveBody) ? effectiveBody.map(withCoreContext) : withCoreContext(effectiveBody);
1220
1237
  }
1221
1238
  const bodyStr = effectiveBody !== void 0 ? JSON.stringify(effectiveBody) : void 0;
1222
1239
  const doRequest = async (reqToken, dpopNonce) => {
@@ -1290,6 +1307,7 @@ var WebSocketManager = class {
1290
1307
  __publicField(this, "_emit");
1291
1308
  __publicField(this, "_ws", null);
1292
1309
  __publicField(this, "_wsEndpoint", null);
1310
+ __publicField(this, "_deploymentHostname", null);
1293
1311
  __publicField(this, "_wsIntentionalClose", false);
1294
1312
  __publicField(this, "_reconnectAttempts", 0);
1295
1313
  __publicField(this, "_tokenRefreshTimer", null);
@@ -1362,6 +1380,7 @@ var WebSocketManager = class {
1362
1380
  if (options.entityTypes) msg.entityTypes = options.entityTypes;
1363
1381
  if (options.idPattern) msg.idPattern = options.idPattern;
1364
1382
  if (options.scopeQ) msg.scopeQ = options.scopeQ;
1383
+ if (options["@context"]) msg["@context"] = options["@context"];
1365
1384
  }
1366
1385
  this._subscription = msg;
1367
1386
  if (this._pendingSubscription) return;
@@ -1378,6 +1397,9 @@ var WebSocketManager = class {
1378
1397
  const info = await res.json();
1379
1398
  if (info.enabled && info.endpoint) {
1380
1399
  this._wsEndpoint = info.endpoint;
1400
+ if (info.deployment) {
1401
+ this._deploymentHostname = info.deployment;
1402
+ }
1381
1403
  return this._wsEndpoint;
1382
1404
  }
1383
1405
  }
@@ -1389,7 +1411,11 @@ var WebSocketManager = class {
1389
1411
  async _openWebSocket(token) {
1390
1412
  const endpoint = await this._discoverWsEndpoint();
1391
1413
  return new Promise((resolve, reject) => {
1392
- const wsUrl = endpoint + (endpoint.indexOf("?") === -1 ? "?" : "&") + "tenant=" + encodeURIComponent(this._tenant);
1414
+ const params = new URLSearchParams({ tenant: this._tenant });
1415
+ if (this._deploymentHostname) {
1416
+ params.set("deployment", this._deploymentHostname);
1417
+ }
1418
+ const wsUrl = endpoint + (endpoint.indexOf("?") === -1 ? "?" : "&") + params.toString();
1393
1419
  this._log("connecting", wsUrl);
1394
1420
  const ws = new WebSocket(wsUrl, [SUB_PROTOCOL, token]);
1395
1421
  this._ws = ws;
@@ -1429,6 +1455,10 @@ var WebSocketManager = class {
1429
1455
  return;
1430
1456
  }
1431
1457
  if (msg.type === "pong") return;
1458
+ if (msg.type === "subscribed") {
1459
+ this._emit("subscribed", void 0);
1460
+ return;
1461
+ }
1432
1462
  if (msg.type === "dpop_verified") {
1433
1463
  this._pendingSubscription = false;
1434
1464
  if (this._subscription) {
@@ -1684,6 +1714,7 @@ var GeonicDB = class extends EventEmitter {
1684
1714
  if (!tenant) tenant = script?.getAttribute?.("data-tenant") || "";
1685
1715
  if (!baseUrl) baseUrl = script?.getAttribute?.("data-base-url") || "";
1686
1716
  }
1717
+ baseUrl = baseUrl.replace(/\/+$/, "");
1687
1718
  this._auth = new AuthManager(baseUrl, apiKey, tenant, opts.debug, opts.anonymous);
1688
1719
  this._auth.onTokenRefresh = (creds) => {
1689
1720
  this.onTokenRefresh?.(creds);
@@ -1895,7 +1926,9 @@ var GeonicDB = class extends EventEmitter {
1895
1926
  limit: 0,
1896
1927
  type: params?.type,
1897
1928
  q: params?.q,
1898
- scopeQ: params?.scopeQ
1929
+ scopeQ: params?.scopeQ,
1930
+ // #2278: clause 5.7.2.4 の "too wide query" 免除条件 (e) local scope。
1931
+ local: params?.local
1899
1932
  });
1900
1933
  const res = await this._auth.request("GET", path);
1901
1934
  if (!res.ok) {
@@ -1992,7 +2025,21 @@ var GeonicDB = class extends EventEmitter {
1992
2025
  "Batch create failed"
1993
2026
  );
1994
2027
  }
1995
- /** Upsert multiple entities in a single request. */
2028
+ /**
2029
+ * Upsert multiple entities in a single request.
2030
+ *
2031
+ * #1742: the server follows ETSI GS CIM 009 clause 5.6.8.5 / Table 6.15.3.1-1:
2032
+ * - **201** with a `string[]` of the **created** entity IDs, when at least one entity
2033
+ * was newly created and none failed (merely updated entities are not listed);
2034
+ * - **204** with no body, when every entity already existed and was updated
2035
+ * (`_jsonPost` maps this to `{}`);
2036
+ * - **207** with a `BatchOperationResult` (`{ created, updated, errors }`), on partial
2037
+ * or total failure.
2038
+ *
2039
+ * The return type reflects all three shapes. Narrow with `Array.isArray(result)` to
2040
+ * detect the "something was created" case.
2041
+ * https://cim.etsi.org/NGSI-LD/official/clause-6.html
2042
+ */
1996
2043
  async batchUpsert(entities) {
1997
2044
  return this._jsonPost(
1998
2045
  "/ngsi-ld/v1/entityOperations/upsert",