@narrative.io/data-collaboration-sdk-ts 3.3.0 → 3.4.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
@@ -311,6 +311,80 @@ try {
311
311
  }
312
312
  ```
313
313
 
314
+ ### Centralized Error Handling
315
+
316
+ Instead of repeating `try/catch` normalization at every call site, you can register a
317
+ single `errorTransformer` on the client. It runs once per failed request — for both
318
+ non-2xx responses (as an `HttpError`) and native fetch/network rejections — after the
319
+ `HttpError` (with its parsed `body`) has been built. Whatever it returns is thrown in
320
+ its place, so you can map transport failures onto your own application error types.
321
+
322
+ The SDK only supplies the mechanism; your application decides what a failure *means*.
323
+ The SDK does not log out, retry, redirect, show a toast, or treat any status as fatal
324
+ on your behalf. A `500`, for example, is not automatically application-fatal — that
325
+ policy is yours to define. This keeps the SDK framework-agnostic: no Vue, Nuxt, Pinia,
326
+ or other UI dependency is involved.
327
+
328
+ ```typescript
329
+ import {
330
+ NarrativeApi,
331
+ HttpError,
332
+ type HttpRequestContext,
333
+ } from '@narrative.io/data-collaboration-sdk-ts';
334
+
335
+ // Your application's own error types — the SDK does not define these.
336
+ class AuthenticationRequiredError extends Error {}
337
+ class ApiServerError extends Error {}
338
+
339
+ const narrative = new NarrativeApi({
340
+ apiKey: process.env.NARRATIVE_API_KEY!,
341
+ errorTransformer(error: unknown, context: HttpRequestContext) {
342
+ // Only HTTP responses carry a status; network rejections do not.
343
+ if (error instanceof HttpError) {
344
+ const status = error.response.status;
345
+
346
+ if (status === 401) {
347
+ return new AuthenticationRequiredError(
348
+ `Authentication required for ${context.method} ${context.url}`,
349
+ );
350
+ }
351
+
352
+ if (status >= 500) {
353
+ return new ApiServerError(`Server error (${status})`);
354
+ }
355
+ }
356
+
357
+ // Return `undefined` (or the original `error`) to rethrow it unchanged.
358
+ return undefined;
359
+ },
360
+ });
361
+ ```
362
+
363
+ Semantics:
364
+
365
+ - The transformer runs **exactly once** per failed request, always after the
366
+ `HttpError` and its parsed `body` are available.
367
+ - **Returning the original `error`** preserves its object identity.
368
+ - **Returning a replacement** causes that value to be thrown instead.
369
+ - **Returning `undefined`** means "rethrow the original error unchanged" — identical
370
+ to having no transformer.
371
+ - If the **transformer itself throws or rejects**, that error propagates and the
372
+ transformer is not invoked a second time.
373
+ - `context` intentionally exposes only `method` and `url`. Request bodies and headers
374
+ (including `Authorization`) are never passed to the transformer.
375
+
376
+ ### Custom `fetch` Implementation
377
+
378
+ By default the SDK uses `globalThis.fetch`. You can supply your own implementation —
379
+ useful for tests, instrumentation, or non-browser runtimes:
380
+
381
+ ```typescript
382
+ const narrative = new NarrativeApi({
383
+ apiKey: process.env.NARRATIVE_API_KEY!,
384
+ fetch: myCustomFetch, // defaults to globalThis.fetch
385
+ });
386
+ ```
387
+
314
388
  ## API Reference
315
389
 
316
390
  For detailed API documentation, please visit the [Narrative.io API Documentation](https://api.narrative.dev).
package/build/base-api.js CHANGED
@@ -23,6 +23,8 @@ export class BaseApi {
23
23
  baseUrl: this.getBaseUrl(),
24
24
  headers: this.constructHeaders(),
25
25
  responseAs: config.responseAs ?? "json",
26
+ errorTransformer: config.errorTransformer,
27
+ fetch: config.fetch,
26
28
  });
27
29
  if (this.verbose) {
28
30
  console.info(`Using environment: ${this.environment}`);
@@ -72,7 +72,7 @@ export interface ForecastDimensions {
72
72
  distinct_counts: string[];
73
73
  group_by?: string;
74
74
  }
75
- export type JobState = "pending" | "running" | "completed" | "pending_cancellation" | "cancelled" | "failed";
75
+ export type JobState = "pending" | "scheduled" | "running" | "completed" | "pending_cancellation" | "cancelled" | "failed";
76
76
  export interface ForecastResult {
77
77
  cost: number;
78
78
  datasets: DatasetForecastResponse[];
@@ -11,9 +11,40 @@
11
11
  * resolves to `null`
12
12
  * - throws on any non-2xx status, attaching the {@link Response} and the parsed
13
13
  * body to the error
14
+ *
15
+ * An optional {@link HttpErrorTransformer} lets consumers observe or replace the
16
+ * error before it propagates. It runs for both non-2xx responses and network
17
+ * rejections, always after the {@link HttpError} (with its parsed body) has been
18
+ * built. When absent, the original error is thrown unchanged.
14
19
  */
15
20
  /** How a response body should be returned from a request. */
16
21
  export type ResponseAs = "json" | "text" | "response";
22
+ /** HTTP verbs the SDK issues. */
23
+ export type HttpRequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
24
+ /**
25
+ * Minimal, side-effect-free description of the request that produced an error.
26
+ *
27
+ * Intentionally excludes the request body and headers (including any
28
+ * `Authorization` header) so an {@link HttpErrorTransformer} cannot leak
29
+ * credentials or payloads.
30
+ */
31
+ export interface HttpRequestContext {
32
+ /** The HTTP verb used for the request. */
33
+ method: HttpRequestMethod;
34
+ /** The fully-resolved request URL (base URL joined with the endpoint). */
35
+ url: string;
36
+ }
37
+ /**
38
+ * Hook invoked when a request fails, either with an {@link HttpError} (non-2xx)
39
+ * or a native fetch/network rejection.
40
+ *
41
+ * Return the value to throw in place of the original error. Returning the
42
+ * original `error` preserves its identity. Returning `undefined` means "rethrow
43
+ * the original error unchanged" — the same as having no transformer. If the
44
+ * transformer itself throws (or rejects), that error propagates; the transformer
45
+ * is never invoked a second time for the same request.
46
+ */
47
+ export type HttpErrorTransformer = (error: unknown, context: HttpRequestContext) => unknown | Promise<unknown>;
17
48
  /** Options for constructing an {@link HttpClient}. */
18
49
  export interface HttpClientOptions {
19
50
  /** Absolute base URL that every request is resolved against. */
@@ -22,6 +53,16 @@ export interface HttpClientOptions {
22
53
  headers?: Record<string, string>;
23
54
  /** How to interpret response bodies. Defaults to `"json"`. */
24
55
  responseAs?: ResponseAs;
56
+ /**
57
+ * Optional hook to observe or transform request failures. See
58
+ * {@link HttpErrorTransformer}.
59
+ */
60
+ errorTransformer?: HttpErrorTransformer;
61
+ /**
62
+ * Optional `fetch` implementation. Defaults to `globalThis.fetch`, resolved
63
+ * at call time so the ambient global is honored when this is omitted.
64
+ */
65
+ fetch?: typeof globalThis.fetch;
25
66
  }
26
67
  /**
27
68
  * Error thrown for any non-2xx response. Carries the originating
@@ -36,10 +77,25 @@ export declare class HttpClient {
36
77
  private readonly baseUrl;
37
78
  private readonly headers;
38
79
  private readonly responseAs;
80
+ private readonly errorTransformer?;
81
+ private readonly fetchImpl?;
39
82
  constructor(options: HttpClientOptions);
40
83
  get<T>(url: string): Promise<T>;
41
84
  post<T>(url: string, data?: unknown): Promise<T>;
42
85
  put<T>(url: string, data?: unknown): Promise<T>;
43
86
  delete<T>(url: string): Promise<T>;
44
87
  private request;
88
+ /**
89
+ * Performs the request and parses the response. Throws {@link HttpError} on a
90
+ * non-2xx status; native fetch/network failures reject as they always have.
91
+ * All failures are funneled through {@link transformError} by {@link request}.
92
+ */
93
+ private send;
94
+ /**
95
+ * Runs the configured {@link HttpErrorTransformer}, if any, and returns the
96
+ * error to throw. Without a transformer, or when the transformer returns
97
+ * `undefined`, the original error is returned unchanged. A transformer that
98
+ * throws/rejects propagates its own error (and is not retried).
99
+ */
100
+ private transformError;
45
101
  }
@@ -11,6 +11,11 @@
11
11
  * resolves to `null`
12
12
  * - throws on any non-2xx status, attaching the {@link Response} and the parsed
13
13
  * body to the error
14
+ *
15
+ * An optional {@link HttpErrorTransformer} lets consumers observe or replace the
16
+ * error before it propagates. It runs for both non-2xx responses and network
17
+ * rejections, always after the {@link HttpError} (with its parsed body) has been
18
+ * built. When absent, the original error is thrown unchanged.
14
19
  */
15
20
  /**
16
21
  * Error thrown for any non-2xx response. Carries the originating
@@ -39,9 +44,13 @@ export class HttpClient {
39
44
  baseUrl;
40
45
  headers;
41
46
  responseAs;
47
+ errorTransformer;
48
+ fetchImpl;
42
49
  constructor(options) {
43
50
  this.baseUrl = options.baseUrl;
44
51
  this.responseAs = options.responseAs ?? "json";
52
+ this.errorTransformer = options.errorTransformer;
53
+ this.fetchImpl = options.fetch;
45
54
  this.headers = {
46
55
  Accept: "application/json",
47
56
  "Content-Type": "application/json",
@@ -61,12 +70,33 @@ export class HttpClient {
61
70
  return this.request("DELETE", url);
62
71
  }
63
72
  async request(method, url, data) {
73
+ const requestUrl = joinUrl(this.baseUrl, url);
74
+ try {
75
+ return await this.send(method, requestUrl, data);
76
+ }
77
+ catch (error) {
78
+ throw await this.transformError(error, {
79
+ method: method,
80
+ url: requestUrl,
81
+ });
82
+ }
83
+ }
84
+ /**
85
+ * Performs the request and parses the response. Throws {@link HttpError} on a
86
+ * non-2xx status; native fetch/network failures reject as they always have.
87
+ * All failures are funneled through {@link transformError} by {@link request}.
88
+ */
89
+ async send(method, requestUrl, data) {
64
90
  const init = { method, headers: this.headers };
65
91
  // Only POST/PUT/PATCH carry a body.
66
92
  if (method[0] === "P" && data !== undefined && data !== null) {
67
93
  init.body = JSON.stringify(data);
68
94
  }
69
- const response = await fetch(joinUrl(this.baseUrl, url), init);
95
+ // Resolve the fetch implementation at call time so the ambient global is
96
+ // honored when no custom `fetch` was supplied. Invoked as a bare call so
97
+ // `this` stays undefined, which native fetch requires.
98
+ const fetchImpl = this.fetchImpl ?? globalThis.fetch;
99
+ const response = await fetchImpl(requestUrl, init);
70
100
  // Return the raw Response untouched when asked.
71
101
  if (this.responseAs === "response") {
72
102
  if (response.status >= 200 && response.status < 300) {
@@ -83,4 +113,16 @@ export class HttpClient {
83
113
  }
84
114
  throw new HttpError(response, body);
85
115
  }
116
+ /**
117
+ * Runs the configured {@link HttpErrorTransformer}, if any, and returns the
118
+ * error to throw. Without a transformer, or when the transformer returns
119
+ * `undefined`, the original error is returned unchanged. A transformer that
120
+ * throws/rejects propagates its own error (and is not retried).
121
+ */
122
+ async transformError(error, context) {
123
+ if (!this.errorTransformer)
124
+ return error;
125
+ const transformed = await this.errorTransformer(error, context);
126
+ return transformed === undefined ? error : transformed;
127
+ }
86
128
  }
@@ -2,7 +2,7 @@ import { BaseApi } from "../base-api";
2
2
  import type { ApiRecordsV2 } from "../types";
3
3
  import type { CalculateAffectedRowsInput, CalculateAffectedRowsJob, CollectAccessRulesBillingDataInput, CollectAccessRulesBillingDataJob, CollectAccessRulesBillingDataResult, ColumnDetails, ColumnStatDataType, CostsInput, CostsJob, CreateTableInput, DatasetsCalculateColumnStatsJob, DatasetsCreateTableJob, DatasetsDeleteTableJob, DatasetsDeliverDataJob, DatasetsEnforceRowTtlRetentionPolicyJob, DatasetsEnforceTableTtlRetentionPolicyJob, DatasetsExecuteDmlJob, DatasetsExecuteSelectJob, DatasetsSampleJob, DatasetsSuggestMappingsJob, DatasetsTruncateTableJob, DeleteInput, DeliverInput, EnabledColumnStatFlags, EnforceRowTtlRetentionPolicyInput, EnforceTableTtlRetentionPolicyInput, ExecuteDmlInput, ExecuteDmlResult, ExecuteSelectInput, ExecuteSelectResult, ExplainInput, ExplainJob, ExplainOutput, ForecastInput, ForecastInternalJob, ForecastJob, HealthCheckJob, Job, JobExecutionCluster, JobRequestSource, JobRequestSourceApiUser, JobRequestSourceProcess, JobType, KnownJob, MaterializedViewInput, MaterializedViewJob, MaterializedViewOutput, MaterializedViewRowStats, ModelInferenceRunInput, ModelInferenceRunJob, ModelInferenceRunJobResult, ModelsDeliverModelInput, ModelsDeliverModelJob, ModelsTrainClassifierJob, ModelTrainingRunInput, ModelTrainingRunJob, RefreshMaterializedViewResult, SampleInput, StatsInput, StatsInputV2, SuggestMappingsInput, SuggestMappingsResult, TruncateTableInput } from "./types";
4
4
  import { isKnownJob, KNOWN_JOB_TYPES } from "./types";
5
- type JobStateFilter = "cancelled" | "completed" | "failed" | "pending" | "pending_cancellation" | "running";
5
+ type JobStateFilter = "cancelled" | "completed" | "failed" | "pending" | "pending_cancellation" | "running" | "scheduled";
6
6
  interface GetJobsParameters extends Record<string, string | number | boolean | string[] | undefined> {
7
7
  dataset_id?: number;
8
8
  per_page?: number;
@@ -39,6 +39,8 @@ interface BaseJob {
39
39
  idempotency_key: string;
40
40
  created_at: string;
41
41
  updated_at: string;
42
+ attempted_at: string;
43
+ attempt_version: number;
42
44
  ended_at?: string;
43
45
  tags?: string[];
44
46
  dequeued_at?: string;
package/build/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ResponseAs } from "./http-client";
1
+ import type { HttpErrorTransformer, ResponseAs } from "./http-client";
2
2
  /**
3
3
  * Environment for the API.
4
4
  *
@@ -18,6 +18,23 @@ export interface Config {
18
18
  headers?: Record<string, string>;
19
19
  verbose?: boolean;
20
20
  responseAs?: ResponseAs;
21
+ /**
22
+ * Optional hook to centrally observe or transform request failures — both
23
+ * non-2xx responses (as an {@link HttpError}) and native fetch/network
24
+ * rejections. Return a replacement error to throw, return the original error
25
+ * to preserve it, or return `undefined` to rethrow it unchanged. With no
26
+ * transformer configured, errors propagate exactly as before.
27
+ *
28
+ * The SDK only supplies the mechanism; the consuming application decides
29
+ * whether a given failure is local, authentication-related, retryable, or
30
+ * fatal. A 500, for instance, is not treated as application-fatal here.
31
+ */
32
+ errorTransformer?: HttpErrorTransformer;
33
+ /**
34
+ * Optional `fetch` implementation, useful for testing and non-browser
35
+ * runtimes. Defaults to `globalThis.fetch`.
36
+ */
37
+ fetch?: typeof globalThis.fetch;
21
38
  }
22
39
  export interface ApiRecordsV2<T> extends PaginationMetadata {
23
40
  records: T[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narrative.io/data-collaboration-sdk-ts",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "main": "build/index.js",
5
5
  "repository": "github:narrative-io/data-collaboration-sdk-ts",
6
6
  "source": "src/index.ts",