@narrative.io/data-collaboration-sdk-ts 3.3.0 → 3.5.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).
@@ -25,6 +25,13 @@ export interface McpServerConfig {
25
25
  alias: string;
26
26
  url: string;
27
27
  description?: string;
28
+ /**
29
+ * Reference to a registered external-MCP OAuth connection (from `POST /mcp-connections`,
30
+ * see {@link McpConnectionsApi}). Set for an external server that requires user
31
+ * authorization — the platform resolves and refreshes the connection's bearer token
32
+ * server-side; it never appears in the payload. Omit for public or Narrative-owned MCPs.
33
+ */
34
+ connection_id?: string;
28
35
  }
29
36
  export interface ToolSpec {
30
37
  /** Caller-declared tools must NOT contain a dash. MCP tools' bare name. */
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
  }
package/build/index.d.ts CHANGED
@@ -30,6 +30,8 @@ export * from "./jobs";
30
30
  export { default as JsonBigNumber } from "./json-big-number";
31
31
  export * from "./mappings";
32
32
  export * from "./mappings/types";
33
+ export * from "./mcp-connections";
34
+ export * from "./mcp-connections/types";
33
35
  export * from "./model-inference";
34
36
  export * from "./model-training";
35
37
  export * from "./models";
@@ -77,6 +79,7 @@ import { HealthCheckApi } from "./health";
77
79
  import { InstallationsApi } from "./installations";
78
80
  import { JobsApi } from "./jobs";
79
81
  import { MappingsApi } from "./mappings";
82
+ import { McpConnectionsApi } from "./mcp-connections";
80
83
  import { ModelInferenceApi } from "./model-inference";
81
84
  import { ModelTrainingApi } from "./model-training";
82
85
  import { ModelsApi } from "./models";
@@ -92,6 +95,6 @@ import { WhoAmIApi } from "./whoami";
92
95
  import { WorkflowsApi } from "./workflows";
93
96
  declare class NarrativeApi extends BaseApi {
94
97
  }
95
- interface NarrativeApi extends BaseApi, HealthCheckApi, AccessTokensApi, DataPlaneApi, DatasetApi, RosettaStoneApi, AttributeApi, AttributeApiV2, PingApi, CompanyInfoApi, InstallationsApi, ConnectionsApi, UploadsApi, ResourceApi, NqlApi, DataStreamsApi, ForecastApi, ContractsApi, AuthenticationApi, MappingsApi, AccessRulesApi, AppsApi, SubscriptionsApi, JobsApi, QueriesApi, ViewsApi, ModelsApi, ModelTrainingApi, ModelInferenceApi, EncryptionMaterialApi, WhoAmIApi, WorkflowsApi, ComputePoolsApi, AgentsApi, CompaniesApi {
98
+ interface NarrativeApi extends BaseApi, HealthCheckApi, AccessTokensApi, DataPlaneApi, DatasetApi, RosettaStoneApi, AttributeApi, AttributeApiV2, PingApi, CompanyInfoApi, InstallationsApi, ConnectionsApi, UploadsApi, ResourceApi, NqlApi, DataStreamsApi, ForecastApi, ContractsApi, AuthenticationApi, MappingsApi, AccessRulesApi, AppsApi, SubscriptionsApi, JobsApi, QueriesApi, ViewsApi, ModelsApi, ModelTrainingApi, ModelInferenceApi, EncryptionMaterialApi, WhoAmIApi, WorkflowsApi, ComputePoolsApi, AgentsApi, CompaniesApi, McpConnectionsApi {
96
99
  }
97
100
  export { NarrativeApi };
package/build/index.js CHANGED
@@ -31,6 +31,8 @@ export * from "./jobs";
31
31
  export { default as JsonBigNumber } from "./json-big-number";
32
32
  export * from "./mappings";
33
33
  export * from "./mappings/types";
34
+ export * from "./mcp-connections";
35
+ export * from "./mcp-connections/types";
34
36
  export * from "./model-inference";
35
37
  export * from "./model-training";
36
38
  export * from "./models";
@@ -78,6 +80,7 @@ import { HealthCheckApi } from "./health";
78
80
  import { InstallationsApi } from "./installations";
79
81
  import { JobsApi } from "./jobs";
80
82
  import { MappingsApi } from "./mappings";
83
+ import { McpConnectionsApi } from "./mcp-connections";
81
84
  import { ModelInferenceApi } from "./model-inference";
82
85
  import { ModelTrainingApi } from "./model-training";
83
86
  import { ModelsApi } from "./models";
@@ -130,5 +133,6 @@ applyMixins(NarrativeApi, [
130
133
  ComputePoolsApi,
131
134
  AgentsApi,
132
135
  CompaniesApi,
136
+ McpConnectionsApi,
133
137
  ]);
134
138
  export { NarrativeApi };
@@ -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;
@@ -0,0 +1,38 @@
1
+ import { BaseApi } from "../base-api";
2
+ import type { CreateMcpConnectionRequest, ListMcpConnectionsResponse, McpConnectionCreatedResponse, McpConnectionSummary } from "./types";
3
+ /**
4
+ * `McpConnectionsApi` wraps the external-MCP connection endpoints. A connection is a per-user
5
+ * OAuth link to an external (non-Narrative) MCP server; once `connected`, an agent conversation
6
+ * can reference it by id via `mcp_servers[].connection_id`. Tokens are never returned by the API.
7
+ *
8
+ * The OAuth callback (`GET /mcp-connections/callback`) is a browser redirect, not an SDK call, so
9
+ * it is intentionally not wrapped here.
10
+ */
11
+ export declare class McpConnectionsApi extends BaseApi {
12
+ /**
13
+ * Lists the calling user's MCP connections with their status. Credential-free.
14
+ * @returns {Promise<ListMcpConnectionsResponse>} The caller's connections (empty if none).
15
+ */
16
+ listMcpConnections(): Promise<ListMcpConnectionsResponse>;
17
+ /**
18
+ * Begins connecting an external MCP server (OAuth discovery + dynamic client registration).
19
+ * @param {CreateMcpConnectionRequest} request - The server `url` and routing `alias`.
20
+ * @returns {Promise<McpConnectionCreatedResponse>} The pending `connection_id` and the
21
+ * `authorization_url` the user must visit to consent.
22
+ */
23
+ createMcpConnection(request: CreateMcpConnectionRequest): Promise<McpConnectionCreatedResponse>;
24
+ /**
25
+ * Fetches one of the calling user's connections.
26
+ * @param {string} connectionId - The connection id.
27
+ * @returns {Promise<McpConnectionSummary>} The connection's status view. Rejects with 404 if it
28
+ * does not exist or belongs to another user.
29
+ */
30
+ getMcpConnection(connectionId: string): Promise<McpConnectionSummary>;
31
+ /**
32
+ * Deletes one of the calling user's connections and its stored tokens.
33
+ * @param {string} connectionId - The connection id.
34
+ * @returns {Promise<void>} Resolves when deleted. Rejects with 404 if it does not exist or
35
+ * belongs to another user.
36
+ */
37
+ deleteMcpConnection(connectionId: string): Promise<void>;
38
+ }
@@ -0,0 +1,46 @@
1
+ import { BaseApi } from "../base-api";
2
+ const resourceName = "mcp-connections";
3
+ /**
4
+ * `McpConnectionsApi` wraps the external-MCP connection endpoints. A connection is a per-user
5
+ * OAuth link to an external (non-Narrative) MCP server; once `connected`, an agent conversation
6
+ * can reference it by id via `mcp_servers[].connection_id`. Tokens are never returned by the API.
7
+ *
8
+ * The OAuth callback (`GET /mcp-connections/callback`) is a browser redirect, not an SDK call, so
9
+ * it is intentionally not wrapped here.
10
+ */
11
+ export class McpConnectionsApi extends BaseApi {
12
+ /**
13
+ * Lists the calling user's MCP connections with their status. Credential-free.
14
+ * @returns {Promise<ListMcpConnectionsResponse>} The caller's connections (empty if none).
15
+ */
16
+ async listMcpConnections() {
17
+ return await this.get(resourceName);
18
+ }
19
+ /**
20
+ * Begins connecting an external MCP server (OAuth discovery + dynamic client registration).
21
+ * @param {CreateMcpConnectionRequest} request - The server `url` and routing `alias`.
22
+ * @returns {Promise<McpConnectionCreatedResponse>} The pending `connection_id` and the
23
+ * `authorization_url` the user must visit to consent.
24
+ */
25
+ async createMcpConnection(request) {
26
+ return await this.post(resourceName, request);
27
+ }
28
+ /**
29
+ * Fetches one of the calling user's connections.
30
+ * @param {string} connectionId - The connection id.
31
+ * @returns {Promise<McpConnectionSummary>} The connection's status view. Rejects with 404 if it
32
+ * does not exist or belongs to another user.
33
+ */
34
+ async getMcpConnection(connectionId) {
35
+ return await this.get(`${resourceName}/${connectionId}`);
36
+ }
37
+ /**
38
+ * Deletes one of the calling user's connections and its stored tokens.
39
+ * @param {string} connectionId - The connection id.
40
+ * @returns {Promise<void>} Resolves when deleted. Rejects with 404 if it does not exist or
41
+ * belongs to another user.
42
+ */
43
+ async deleteMcpConnection(connectionId) {
44
+ await this.delete(`${resourceName}/${connectionId}`);
45
+ }
46
+ }
@@ -0,0 +1,22 @@
1
+ export type McpConnectionStatus = "pending" | "connected" | "error";
2
+ export interface CreateMcpConnectionRequest {
3
+ url: string;
4
+ alias: string;
5
+ }
6
+ export interface McpConnectionCreatedResponse {
7
+ connection_id: string;
8
+ authorization_url: string;
9
+ }
10
+ export interface McpConnectionSummary {
11
+ connection_id: string;
12
+ server_url: string;
13
+ alias: string;
14
+ authorization_server: string;
15
+ status: McpConnectionStatus;
16
+ expires_at?: string;
17
+ created_at: string;
18
+ updated_at: string;
19
+ }
20
+ export interface ListMcpConnectionsResponse {
21
+ connections: McpConnectionSummary[];
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -36,8 +36,8 @@ export interface Select {
36
36
  from: Table[] | Raw;
37
37
  where: BooleanExpression | Raw | null;
38
38
  }
39
- export type Output = ColumnRef | Lit | Raw | ast.NqlPlaceholder<ast.NqlAstType>;
40
- export type Expression = BooleanExpression | ColumnRef | Lit | Raw | ast.NqlPlaceholder<ast.NqlAstType>;
39
+ export type Output = ColumnRef | Lit | Func | Raw | ast.NqlPlaceholder<ast.NqlAstType>;
40
+ export type Expression = BooleanExpression | ColumnRef | Lit | Func | Raw | ast.NqlPlaceholder<ast.NqlAstType>;
41
41
  export interface Raw {
42
42
  type: "nql";
43
43
  as?: string;
@@ -99,6 +99,12 @@ export interface Lit {
99
99
  value_type: ast.NqlSimpleType;
100
100
  value: string | null;
101
101
  }
102
+ export interface Func {
103
+ type: "function";
104
+ as?: string;
105
+ name: string;
106
+ args: Expression[];
107
+ }
102
108
  export interface And {
103
109
  type: "and";
104
110
  as?: string;
@@ -175,13 +175,38 @@ export function parseDeduplication(n) {
175
175
  return n.qualify !== null ? nqlOrFail(n.qualify) : null;
176
176
  }
177
177
  export function parseExpression(n) {
178
- const parse = oneOf(parseBooleanExpression, parseColumnRef, parseLit, parseStringExpression, parsePlaceholder);
178
+ const parse = oneOf(parseBooleanExpression, parseColumnRef, parseLit, parseFunction, parseStringExpression, parsePlaceholder);
179
179
  return parse(n);
180
180
  }
181
181
  export function parseOutput(n) {
182
- const parse = oneOf(parseColumnRef, parseLit, parsePlaceholder);
182
+ const parse = oneOf(parseColumnRef, parseLit, parseFunction, parsePlaceholder);
183
183
  return parse(n);
184
184
  }
185
+ function parseFunction(n) {
186
+ if (n.type !== "function") {
187
+ return nqlOrFail(n);
188
+ }
189
+ // Only structure functions whose original text is exactly the standard
190
+ // call syntax `NAME(arg, arg, ...)`. Functions with special syntax —
191
+ // EXTRACT(unit FROM x), CAST(x AS type), TRIM(BOTH ' ' FROM x) — cannot
192
+ // be regenerated from name + args and must stay Raw passthroughs, or
193
+ // compiling would emit comma syntax and corrupt the query.
194
+ const argTexts = n.args.map((a) => "nql" in a ? a.nql : undefined);
195
+ if (argTexts.some((t) => t === undefined)) {
196
+ return nqlOrFail(n);
197
+ }
198
+ const reconstructed = `${n.name}(${argTexts.join(", ")})`;
199
+ if (reconstructed !== n.nql) {
200
+ return nqlOrFail(n);
201
+ }
202
+ const func = {
203
+ type: "function",
204
+ as: n.as,
205
+ name: n.name,
206
+ args: n.args.map(parseExpression),
207
+ };
208
+ return func;
209
+ }
185
210
  function parseBooleanExpression(n) {
186
211
  switch (n.type) {
187
212
  case "binary_op": {
@@ -28,6 +28,8 @@ function findTraversalTargets(node) {
28
28
  }
29
29
  return inTargets;
30
30
  }
31
+ case "function":
32
+ return node.args;
31
33
  case "deduplication":
32
34
  return node.expressions;
33
35
  case "select": {
@@ -214,6 +214,8 @@ export function compileOutput(n, dsMap) {
214
214
  return compileDatasetColumnRef(n.datasetId, n.column, n.as, dsMap);
215
215
  case "lit":
216
216
  return compileLit(n.value, n.value_type, n.as, false);
217
+ case "function":
218
+ return compileFunction(n, dsMap, false);
217
219
  case "raw_ref":
218
220
  return compileRawRef(n.nql, n.as);
219
221
  case "nql":
@@ -253,6 +255,8 @@ export function compileExpression(n, dsMap) {
253
255
  return compileLike(n.value, n.pattern, n.negated, n.as, dsMap);
254
256
  case "lit":
255
257
  return compileLit(n.value, n.value_type, n.as);
258
+ case "function":
259
+ return compileFunction(n, dsMap);
256
260
  case "not":
257
261
  return compileNot(n.operand, n.as, dsMap);
258
262
  case "or":
@@ -355,6 +359,10 @@ function compilePostfixUnaryOperator(op, operand, as, dsMap) {
355
359
  function compileRawRef(nql, as) {
356
360
  return aliased(nql, as);
357
361
  }
362
+ function compileFunction(n, dsMap, parens) {
363
+ const args = n.args.map((arg) => compileExpression(arg, dsMap)).join(", ");
364
+ return aliased(`${n.name}(${args})`, n.as, parens);
365
+ }
358
366
  export function compilePlaceholder(n) {
359
367
  switch (n.expectedType) {
360
368
  case "literal":
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.5.0",
4
4
  "main": "build/index.js",
5
5
  "repository": "github:narrative-io/data-collaboration-sdk-ts",
6
6
  "source": "src/index.ts",