@centia-io/sdk 0.2.13 → 0.2.15

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.
@@ -346,6 +346,9 @@
346
346
  isSignUpOptions(options) {
347
347
  return "parentDb" in options;
348
348
  }
349
+ isGuestFlowOptions(options) {
350
+ return "database" in options && !("username" in options);
351
+ }
349
352
  buildUrl(path) {
350
353
  if (path.startsWith("http://") || path.startsWith("https://")) return path;
351
354
  return `${this.host}${path}`;
@@ -458,12 +461,24 @@
458
461
  database
459
462
  });
460
463
  }
461
- async getRefreshToken(token) {
464
+ async getGuestToken() {
462
465
  var _this5 = this;
463
- var _this$options$tokenUr3;
464
- const path = (_this$options$tokenUr3 = _this5.options.tokenUri) !== null && _this$options$tokenUr3 !== void 0 ? _this$options$tokenUr3 : `${_this5.host}/api/v4/oauth`;
466
+ let database;
467
+ if (_this5.isGuestFlowOptions(_this5.options)) database = _this5.options.database;
468
+ else throw new Error("GuestFlow options required for this operation");
469
+ const path = `${_this5.host}/api/v4/oauth/guest`;
465
470
  return _this5.request(_this5.buildUrl(path), "POST", {
471
+ database,
466
472
  client_id: _this5.options.clientId,
473
+ client_secret: _this5.options.clientSecret
474
+ });
475
+ }
476
+ async getRefreshToken(token) {
477
+ var _this6 = this;
478
+ var _this$options$tokenUr3;
479
+ const path = (_this$options$tokenUr3 = _this6.options.tokenUri) !== null && _this$options$tokenUr3 !== void 0 ? _this$options$tokenUr3 : `${_this6.host}/api/v4/oauth`;
480
+ return _this6.request(_this6.buildUrl(path), "POST", {
481
+ client_id: _this6.options.clientId,
467
482
  grant_type: "refresh_token",
468
483
  refresh_token: token
469
484
  });
@@ -581,6 +596,48 @@
581
596
  }
582
597
  };
583
598
 
599
+ //#endregion
600
+ //#region src/GuestFlow.ts
601
+ /**
602
+ * @author Martin Høgh <mh@mapcentia.com>
603
+ * @copyright 2013-2026 MapCentia ApS
604
+ * @license https://opensource.org/license/mit The MIT License
605
+ *
606
+ */
607
+ /**
608
+ * Guest token flow. Issues access/refresh tokens for the database's default
609
+ * user (the sub-user used for anonymous access) without any user credentials.
610
+ * `clientSecret` is only required when the OAuth client is not public.
611
+ */
612
+ var GuestFlow = class {
613
+ constructor(options) {
614
+ this.options = options;
615
+ this.service = new Gc2Service(options);
616
+ }
617
+ async signIn() {
618
+ var _this = this;
619
+ const { access_token, refresh_token } = await _this.service.getGuestToken();
620
+ setTokens({
621
+ accessToken: access_token,
622
+ refreshToken: refresh_token
623
+ });
624
+ setOptions({
625
+ clientId: _this.options.clientId,
626
+ host: _this.options.host,
627
+ redirectUri: "",
628
+ clientSecret: _this.options.clientSecret
629
+ });
630
+ }
631
+ signOut() {
632
+ this.clear();
633
+ }
634
+ clear() {
635
+ clearTokens();
636
+ clearOptions();
637
+ clearNonce();
638
+ }
639
+ };
640
+
584
641
  //#endregion
585
642
  //#region src/http/errors.ts
586
643
  /**
@@ -655,6 +712,52 @@
655
712
  getHeader: (name) => response.headers.get(name)
656
713
  };
657
714
  }
715
+ /**
716
+ * Execute a request and return the raw Response without consuming the body.
717
+ * For binary endpoints (file downloads): follows redirects (a presigned
718
+ * storage URL — the fetch spec strips Authorization on cross-origin
719
+ * redirects), sends no Content-Type, and supports extra request headers
720
+ * such as Range. Throws CentiaApiError on a status not in expectedStatus.
721
+ */
722
+ async requestRaw(opts) {
723
+ var _this3 = this;
724
+ var _opts$accept, _opts$expectedStatus2;
725
+ const url = _this3.buildUrl(opts.path, opts.query);
726
+ const headers = { "Accept": (_opts$accept = opts.accept) !== null && _opts$accept !== void 0 ? _opts$accept : "*/*" };
727
+ if (_this3.auth.getAccessToken) {
728
+ const token = await _this3.auth.getAccessToken();
729
+ if (token) headers["Authorization"] = `Bearer ${token}`;
730
+ }
731
+ if (_this3.auth.getHeaders) Object.assign(headers, await _this3.auth.getHeaders());
732
+ if (_this3.userAgent && typeof navigator === "undefined") headers["User-Agent"] = _this3.userAgent;
733
+ Object.assign(headers, opts.headers);
734
+ const response = await _this3.fetchFn(url, {
735
+ method: opts.method,
736
+ headers,
737
+ redirect: "follow"
738
+ });
739
+ if (!((_opts$expectedStatus2 = opts.expectedStatus) !== null && _opts$expectedStatus2 !== void 0 ? _opts$expectedStatus2 : [200]).includes(response.status)) {
740
+ var _ref, _parsed$message, _response$headers$get;
741
+ let bodyText = "";
742
+ try {
743
+ bodyText = await response.text();
744
+ } catch (_unused) {}
745
+ let parsed = null;
746
+ if (bodyText) try {
747
+ parsed = JSON.parse(bodyText);
748
+ } catch (_unused2) {}
749
+ throw new CentiaApiError({
750
+ message: ((_ref = (_parsed$message = parsed === null || parsed === void 0 ? void 0 : parsed.message) !== null && _parsed$message !== void 0 ? _parsed$message : parsed === null || parsed === void 0 ? void 0 : parsed.error) !== null && _ref !== void 0 ? _ref : bodyText) || `Unexpected status ${response.status}`,
751
+ status: response.status,
752
+ code: parsed === null || parsed === void 0 ? void 0 : parsed.code,
753
+ details: parsed,
754
+ requestId: (_response$headers$get = response.headers.get("x-request-id")) !== null && _response$headers$get !== void 0 ? _response$headers$get : void 0,
755
+ method: opts.method,
756
+ url
757
+ });
758
+ }
759
+ return response;
760
+ }
658
761
  buildUrl(path, query) {
659
762
  const cleanPath = path.replace(/^\/+/, "");
660
763
  let url = `${this.baseUrl}/${cleanPath}`;
@@ -665,19 +768,19 @@
665
768
  return url;
666
769
  }
667
770
  async buildHeaders(opts) {
668
- var _this3 = this;
669
- var _opts$accept;
670
- const headers = { "Accept": (_opts$accept = opts.accept) !== null && _opts$accept !== void 0 ? _opts$accept : "application/json" };
671
- if (_this3.auth.getAccessToken) {
672
- const token = await _this3.auth.getAccessToken();
771
+ var _this4 = this;
772
+ var _opts$accept2;
773
+ const headers = { "Accept": (_opts$accept2 = opts.accept) !== null && _opts$accept2 !== void 0 ? _opts$accept2 : "application/json" };
774
+ if (_this4.auth.getAccessToken) {
775
+ const token = await _this4.auth.getAccessToken();
673
776
  if (token) headers["Authorization"] = `Bearer ${token}`;
674
777
  }
675
- if (_this3.auth.getHeaders) {
676
- const authHeaders = await _this3.auth.getHeaders();
778
+ if (_this4.auth.getHeaders) {
779
+ const authHeaders = await _this4.auth.getHeaders();
677
780
  Object.assign(headers, authHeaders);
678
781
  }
679
- if (_this3.userAgent && typeof navigator === "undefined") headers["User-Agent"] = _this3.userAgent;
680
- const ct = _this3.resolveContentType(opts.contentType);
782
+ if (_this4.userAgent && typeof navigator === "undefined") headers["User-Agent"] = _this4.userAgent;
783
+ const ct = _this4.resolveContentType(opts.contentType);
681
784
  if (ct) headers["Content-Type"] = ct;
682
785
  return headers;
683
786
  }
@@ -686,24 +789,24 @@
686
789
  return contentType !== null && contentType !== void 0 ? contentType : "application/json";
687
790
  }
688
791
  async handleResponse(response, opts, url) {
689
- var _opts$expectedStatus2, _parsed;
690
- const expectedStatus = (_opts$expectedStatus2 = opts.expectedStatus) !== null && _opts$expectedStatus2 !== void 0 ? _opts$expectedStatus2 : 200;
792
+ var _opts$expectedStatus3, _parsed;
793
+ const expectedStatus = (_opts$expectedStatus3 = opts.expectedStatus) !== null && _opts$expectedStatus3 !== void 0 ? _opts$expectedStatus3 : 200;
691
794
  let bodyText = "";
692
795
  try {
693
796
  bodyText = await response.text();
694
- } catch (_unused) {}
797
+ } catch (_unused3) {}
695
798
  let parsed = null;
696
799
  if (bodyText) try {
697
800
  parsed = JSON.parse(bodyText);
698
- } catch (_unused2) {}
801
+ } catch (_unused4) {}
699
802
  if (response.status !== expectedStatus) {
700
- var _ref, _parsed$message, _response$headers$get;
803
+ var _ref2, _parsed$message2, _response$headers$get2;
701
804
  throw new CentiaApiError({
702
- message: ((_ref = (_parsed$message = parsed === null || parsed === void 0 ? void 0 : parsed.message) !== null && _parsed$message !== void 0 ? _parsed$message : parsed === null || parsed === void 0 ? void 0 : parsed.error) !== null && _ref !== void 0 ? _ref : bodyText) || `Unexpected status ${response.status}`,
805
+ message: ((_ref2 = (_parsed$message2 = parsed === null || parsed === void 0 ? void 0 : parsed.message) !== null && _parsed$message2 !== void 0 ? _parsed$message2 : parsed === null || parsed === void 0 ? void 0 : parsed.error) !== null && _ref2 !== void 0 ? _ref2 : bodyText) || `Unexpected status ${response.status}`,
703
806
  status: response.status,
704
807
  code: parsed === null || parsed === void 0 ? void 0 : parsed.code,
705
808
  details: parsed,
706
- requestId: (_response$headers$get = response.headers.get("x-request-id")) !== null && _response$headers$get !== void 0 ? _response$headers$get : void 0,
809
+ requestId: (_response$headers$get2 = response.headers.get("x-request-id")) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : void 0,
707
810
  method: opts.method,
708
811
  url
709
812
  });
@@ -3334,6 +3437,150 @@
3334
3437
  }
3335
3438
  };
3336
3439
 
3440
+ //#endregion
3441
+ //#region src/snapshots/Snapshots.ts
3442
+ /**
3443
+ * Client for the snapshot API: GeoParquet exports of a relation.
3444
+ *
3445
+ * The job API (`/api/v4/snapshots`) queues and inspects exports and is
3446
+ * super-user only; exports run asynchronously — poll with `getSnapshot` or
3447
+ * use `waitForSnapshot`. The read API
3448
+ * (`/api/v4/schemas/{schema}/relations/{relation}/snapshots`) serves the
3449
+ * published snapshots to any user with read access to the relation, with
3450
+ * HEAD + byte-range support so Parquet readers can fetch footers and row
3451
+ * groups selectively.
3452
+ *
3453
+ * ```ts
3454
+ * const snapshots = new Snapshots(createCentiaClient({ baseUrl, auth }));
3455
+ * const { id } = await snapshots.postSnapshot({ schema: 'geodanmark', relation: 'bygning' });
3456
+ * const done = await snapshots.waitForSnapshot(id);
3457
+ * ```
3458
+ */
3459
+ var Snapshots = class {
3460
+ constructor(client) {
3461
+ this.client = client;
3462
+ }
3463
+ relationPath(schema, relation, rest = "") {
3464
+ return `api/v4/schemas/${encodeURIComponent(schema)}/relations/${encodeURIComponent(relation)}/snapshots${rest}`;
3465
+ }
3466
+ /**
3467
+ * Queue a Parquet snapshot of a table or view. Returns 202 with the job id.
3468
+ * Throws `CentiaApiError`: 403 (super-user only), 404 (relation not found),
3469
+ * 409 (a snapshot of the relation is already pending or running),
3470
+ * 501 (snapshot storage not configured).
3471
+ */
3472
+ async postSnapshot(body) {
3473
+ return this.client.request({
3474
+ path: "api/v4/snapshots",
3475
+ method: "POST",
3476
+ body,
3477
+ expectedStatus: 202
3478
+ });
3479
+ }
3480
+ /** Get a snapshot job by id. Super-user only. */
3481
+ async getSnapshot(id) {
3482
+ return this.client.request({
3483
+ path: `api/v4/snapshots/${encodeURIComponent(id)}`,
3484
+ method: "GET"
3485
+ });
3486
+ }
3487
+ /** List the newest snapshot jobs (50), optionally filtered by schema/relation. Super-user only. */
3488
+ async getSnapshots(options) {
3489
+ var _this3 = this;
3490
+ const query = {};
3491
+ if ((options === null || options === void 0 ? void 0 : options.schema) !== void 0) query.schema = options.schema;
3492
+ if ((options === null || options === void 0 ? void 0 : options.relation) !== void 0) query.relation = options.relation;
3493
+ return _this3.client.request({
3494
+ path: "api/v4/snapshots",
3495
+ method: "GET",
3496
+ query: Object.keys(query).length > 0 ? query : void 0
3497
+ });
3498
+ }
3499
+ /**
3500
+ * Poll a snapshot job until it reaches a terminal status (succeeded, failed
3501
+ * or superseded) and return it — check `status`/`error` on the result.
3502
+ * Throws an `Error` when `timeoutMs` elapses first.
3503
+ */
3504
+ async waitForSnapshot(id, options) {
3505
+ var _this4 = this;
3506
+ var _options$intervalMs, _options$timeoutMs;
3507
+ const intervalMs = (_options$intervalMs = options === null || options === void 0 ? void 0 : options.intervalMs) !== null && _options$intervalMs !== void 0 ? _options$intervalMs : 2e3;
3508
+ const timeoutMs = (_options$timeoutMs = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _options$timeoutMs !== void 0 ? _options$timeoutMs : 3e5;
3509
+ const deadline = Date.now() + timeoutMs;
3510
+ for (;;) {
3511
+ const job = await _this4.getSnapshot(id);
3512
+ if (job.status === "succeeded" || job.status === "failed" || job.status === "superseded") return job;
3513
+ if (Date.now() + intervalMs > deadline) throw new Error(`Timed out after ${timeoutMs} ms waiting for snapshot ${id} (status: ${job.status})`);
3514
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
3515
+ }
3516
+ }
3517
+ /** List the published snapshots of a relation, newest first. */
3518
+ async getRelationSnapshots(schema, relation) {
3519
+ var _this5 = this;
3520
+ return _this5.client.request({
3521
+ path: _this5.relationPath(schema, relation),
3522
+ method: "GET"
3523
+ });
3524
+ }
3525
+ /** Get one published snapshot (metadata) by date (`YYYY-MM-DD`). */
3526
+ async getRelationSnapshot(schema, relation, date) {
3527
+ var _this6 = this;
3528
+ return _this6.client.request({
3529
+ path: _this6.relationPath(schema, relation, `/${encodeURIComponent(date)}`),
3530
+ method: "GET"
3531
+ });
3532
+ }
3533
+ /**
3534
+ * Absolute URL of the snapshot's Parquet file, without fetching it — for
3535
+ * DuckDB/GDAL or a plain fetch. Authorization travels in the request
3536
+ * header, so protected relations need the caller to attach the token.
3537
+ */
3538
+ getRelationSnapshotDataUrl(schema, relation, date) {
3539
+ return `${this.client.baseUrl}/${this.relationPath(schema, relation, `/${encodeURIComponent(date)}/data`)}`;
3540
+ }
3541
+ /**
3542
+ * Fetch the snapshot's Parquet file and return the raw `Response` (stream
3543
+ * or buffer it yourself). Follows the 302 redirect the server answers in
3544
+ * redirect mode. Pass `range` for a single byte range (206). Throws
3545
+ * `CentiaApiError`: 403, 404, 409 (several files; use
3546
+ * getRelationSnapshotFile), 416, 502.
3547
+ */
3548
+ async getRelationSnapshotData(schema, relation, date, options) {
3549
+ var _this7 = this;
3550
+ return _this7.rawGet(_this7.relationPath(schema, relation, `/${encodeURIComponent(date)}/data`), "GET", options);
3551
+ }
3552
+ /** HEAD request for the snapshot's Parquet file — Content-Length, Accept-Ranges and ETag without the body. */
3553
+ async headRelationSnapshotData(schema, relation, date) {
3554
+ var _this8 = this;
3555
+ return _this8.rawGet(_this8.relationPath(schema, relation, `/${encodeURIComponent(date)}/data`), "HEAD");
3556
+ }
3557
+ /**
3558
+ * Fetch one file of the snapshot by catalog name (`data-<id>.parquet`,
3559
+ * `metadata-<id>.json`). Same redirect/range semantics as
3560
+ * getRelationSnapshotData.
3561
+ */
3562
+ async getRelationSnapshotFile(schema, relation, date, file, options) {
3563
+ var _this9 = this;
3564
+ return _this9.rawGet(_this9.relationPath(schema, relation, `/${encodeURIComponent(date)}/files/${encodeURIComponent(file)}`), "GET", options);
3565
+ }
3566
+ /** HEAD request for one file of the snapshot. */
3567
+ async headRelationSnapshotFile(schema, relation, date, file) {
3568
+ var _this10 = this;
3569
+ return _this10.rawGet(_this10.relationPath(schema, relation, `/${encodeURIComponent(date)}/files/${encodeURIComponent(file)}`), "HEAD");
3570
+ }
3571
+ async rawGet(path, method, options) {
3572
+ var _this11 = this;
3573
+ const headers = {};
3574
+ if (options === null || options === void 0 ? void 0 : options.range) headers["Range"] = `bytes=${options.range[0]}-${options.range[1]}`;
3575
+ return _this11.client.requestRaw({
3576
+ path,
3577
+ method,
3578
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
3579
+ expectedStatus: [200, 206]
3580
+ });
3581
+ }
3582
+ };
3583
+
3337
3584
  //#endregion
3338
3585
  //#region src/auth/errors.ts
3339
3586
  /**
@@ -3461,6 +3708,7 @@ exports.Claims = Claims;
3461
3708
  exports.CodeFlow = CodeFlow;
3462
3709
  exports.Features = Features;
3463
3710
  exports.Gql = Gql;
3711
+ exports.GuestFlow = GuestFlow;
3464
3712
  exports.Keyvalue = Keyvalue;
3465
3713
  exports.Mapcache = Mapcache;
3466
3714
  exports.Meta = Meta;
@@ -3472,6 +3720,7 @@ exports.PasswordFlow = PasswordFlow;
3472
3720
  exports.Rpc = Rpc;
3473
3721
  exports.SessionExpiredError = SessionExpiredError;
3474
3722
  exports.SignUp = SignUp;
3723
+ exports.Snapshots = Snapshots;
3475
3724
  exports.Sql = Sql;
3476
3725
  exports.SqlNoToken = SqlNoToken;
3477
3726
  exports.Stats = Stats;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centia-io/sdk",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Centia-io TypeScript SDK",
5
5
  "author": "Martin Høgh",
6
6
  "license": "MIT",