nuabase 1.0.1 → 1.2.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
@@ -46,9 +46,9 @@ const classifyLeads = new Nua().createArrayFn({
46
46
  **4. Invoke.** Call it like any other async function:
47
47
 
48
48
  ```ts
49
- const result = await classifyLeads.now(leads, 'id');
49
+ const result = await classifyLeads(leads, 'id');
50
50
  if (result.isError) throw new Error(result.error);
51
- console.log(result.data[0].insights);
51
+ console.log(result.data[0].leadInsights);
52
52
  // -> { industry: 'Software', company_size: 'Mid-market' }
53
53
  ```
54
54
 
@@ -107,34 +107,35 @@ const rows = [
107
107
  },
108
108
  ];
109
109
 
110
- const response = await enrichLeads.now(rows, 'leadId'); // `leadId` is the primary key field
110
+ const response = await enrichLeads(rows, 'leadId'); // `leadId` is the primary key field
111
111
 
112
112
  if (response.isError) {
113
113
  console.error(response.error);
114
114
  return;
115
115
  }
116
116
 
117
- response.data.forEach(({ id, leadInsights }) => {
117
+ response.data.forEach(({ leadId, leadInsights }) => {
118
118
  // leadInsights satisfies the LeadInsights schema
119
- console.log(id, leadInsights.company_name, leadInsights.industry, leadInsights.company_size);
119
+ console.log(leadId, leadInsights.company_name, leadInsights.industry, leadInsights.company_size);
120
120
  });
121
121
 
122
122
  console.log(response.data);
123
123
  // -> [
124
124
  // {
125
- // id: 'lead-101',
125
+ // leadId: 'lead-101',
126
126
  // leadInsights: {
127
127
  // company_name: 'Acme Analytics',
128
128
  // industry: 'Software',
129
129
  // company_size: 'Mid-market',
130
130
  // recommended_follow_up: 'Call',
131
131
  // },
132
+ // sourceRow: { leadId: 'lead-101', name: 'Acme Analytics', notes: '...' },
132
133
  // },
133
134
  // // ...
134
135
  // ];
135
136
  ```
136
137
 
137
- `map(data, primaryKey)` submits an asynchronous mapping job. But `map.now(data, primaryKey)` is immediate — it waits for completion and returns the full result.
138
+ The default invocation waits for completion and returns the full result. Use `.queue(data, primaryKey)` to submit an asynchronous job and receive streaming updates via SSE.
138
139
 
139
140
  ## Why Nuabase
140
141
 
@@ -149,14 +150,15 @@ Every call returns a discriminated union. When `isError` is `true`, you get a fl
149
150
 
150
151
  **Success**
151
152
 
152
- | Field | Type | Description |
153
- | ------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
154
- | `isSuccess` | `true` | Indicates that the request completed successfully. |
155
- | `data` | `Array<{ [primaryKey]: unknown; [outputName]: z.infer<typeof output> }>` | Rows that merged the original primary key with the validated schema output. |
156
- | `llmRequestId` | `string` | Unique identifier for tracing each Nuabase LLM execution. |
157
- | `cacheHits` | `number` | Count of input rows served from Nuabase response cache. |
158
- | `rowsWithNoResults` | `string[]` | Primary keys that could not be resolved by the model. |
159
- | `isError` | `false \| undefined` | Absent on success; included for type narrowing. |
153
+ | Field | Type | Description |
154
+ | ------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
155
+ | `isSuccess` | `true` | Indicates that the request completed successfully. |
156
+ | `data` | `Array<{ [primaryKey]: unknown; [outputName]: z.infer<typeof output>; sourceRow: InputRow }>` | Rows that merged the original primary key with the validated schema output, plus the full `sourceRow` for convenience. |
157
+ | `llmRequestId` | `string` | Unique identifier for tracing each Nuabase LLM execution. |
158
+ | `usage` | `{ promptTokens: number; completionTokens: number; totalTokens: number }` | Token usage statistics for the LLM request. |
159
+ | `cacheHits` | `number` | Count of input rows served from Nuabase response cache. |
160
+ | `rowsWithNoResults` | `string[]` | Primary keys that could not be resolved by the model. |
161
+ | `isError` | `false \| undefined` | Absent on success; included for type narrowing. |
160
162
 
161
163
  **Error**
162
164
 
@@ -173,28 +175,33 @@ Creates a client bound to your Nuabase account. Provide:
173
175
  - `apiKey?: string` – API key for authentication. Defaults to `process.env.NUABASE_API_KEY`.
174
176
  - `baseUrl?: string` – Override the API host. Defaults to `https://api.nuabase.com`.
175
177
 
176
- **`nua.map({ name, prompt, output })`**
177
- Declares a typed mapping function backed by Nuabase. Parameters:
178
+ **`nua.createArrayFn({ prompt, output })`**
179
+ Declares a typed array mapping function backed by Nuabase. Parameters:
178
180
 
179
- - `name: string` – Label applied to the enriched payload in each response row (e.g., `result.data[i][name]`).
180
181
  - `prompt: string` – Natural-language instructions sent to the LLM.
181
- - `output: ZodSchema` – Zod schema describing the output shape. Nuabase stores the JSON Schema twin and validates responses against it before returning.
182
+ - `output: { name: string; schema: ZodSchema }` – Output configuration with a name and Zod schema describing the output shape.
182
183
 
183
- Returns a callable object (the mapper) with two entry points:
184
+ Returns a callable function with two modes:
184
185
 
185
- - `mapper(data, primaryKey)` – Submits the job asynchronously and returns immediately with metadata and eventual results.
186
- - `mapper.now(data, primaryKey)` – Runs the job and waits until the full enriched result set is ready.
186
+ - `fn(data, primaryKey)` – Default mode. Runs the job and waits until the full enriched result set is ready.
187
+ - `fn.queue(data, primaryKey)` – Submits the job asynchronously and returns streaming metadata via SSE.
187
188
 
188
- Both entry points expect:
189
+ Both modes expect:
189
190
 
190
191
  - `data: Array<Record<string, unknown>>` – Rows you want Nuabase to enrich. Each row must include the primary key.
191
192
  - `primaryKey: string` – Field name that uniquely identifies each row within `data`. The SDK throws if the field is missing on any item.
192
193
 
193
- Both entry points resolve to either:
194
+ Both modes resolve to either:
194
195
 
195
- - **Success** – `{ isSuccess: true, data, llmRequestId, cacheHits, rowsWithNoResults }`.
196
+ - **Success** – `{ isSuccess: true, data, llmRequestId, usage, cacheHits, rowsWithNoResults }`.
196
197
  - **Error** – `{ isError: true, error }`.
197
198
 
199
+ **`nua.createFn({ prompt, output })`**
200
+ Declares a typed single-value function backed by Nuabase. Similar to `createArrayFn` but for processing a single input value instead of an array. Returns a function that accepts a single input value.
201
+
202
+ **`nua.getRequest(llmRequestId)`**
203
+ Retrieves request details for a completed LLM execution using its request ID.
204
+
198
205
  ## Next Steps
199
206
 
200
207
  - Create an API key or inspect request logs in the [Nuabase Console](https://console.nuabase.com/).
@@ -2,6 +2,16 @@
2
2
 
3
3
  var zod = require('zod');
4
4
 
5
+ function resolveConfigValue(config, key, envVar) {
6
+ if (config[key]) return config[key];
7
+ const pe = globalThis.process;
8
+ const isNode = !!pe?.versions?.node;
9
+ if (isNode) {
10
+ return pe?.env?.[envVar];
11
+ }
12
+ return void 0;
13
+ }
14
+
5
15
  async function parseErrorResponse(response, urlPath) {
6
16
  const fallbackMessage = `Request to ${urlPath} failed with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
7
17
  let responseBody = "";
@@ -45,27 +55,69 @@ function getErrorMessageFromException(error) {
45
55
  return String(error);
46
56
  }
47
57
 
48
- class NuabaseAPIClient {
49
- constructor(config = {}) {
50
- this.apiKey = config.apiKey || process.env.NUABASE_API_KEY || "";
51
- this.baseUrl = config.baseUrl || process.env.NUABASE_API_URL || "https://api.nuabase.com";
52
- if (!this.apiKey) {
58
+ const isBrowserEnvironment = () => typeof window !== "undefined" && !!window.document;
59
+ class NuabaseAuthTokenManager {
60
+ constructor(config) {
61
+ this.apiKey = resolveConfigValue(config, "apiKey", "NUABASE_API_KEY") || void 0;
62
+ this.fetchToken = config.fetchToken;
63
+ if (this.apiKey && this.fetchToken) {
64
+ throw new Error("Provide either an apiKey or a fetchToken function, but not both.");
65
+ }
66
+ if (!this.apiKey && !this.fetchToken) {
67
+ throw new Error(
68
+ "Authentication is required. Provide config.apiKey/NUABASE_API_KEY or a fetchToken function."
69
+ );
70
+ }
71
+ if (this.apiKey && isBrowserEnvironment()) {
53
72
  throw new Error(
54
- "API key is required. Provide it via config.apiKey or NUABASE_API_KEY environment variable."
73
+ "found config.apiKey. It is a private secret that must only be used in server environments."
55
74
  );
56
75
  }
57
76
  }
58
- async request(urlPath, params) {
77
+ async getToken() {
78
+ if (this.apiKey) {
79
+ return this.apiKey;
80
+ }
81
+ if (!this.fetchToken) {
82
+ throw new Error("Unable to resolve API token because no fetchToken function is configured.");
83
+ }
84
+ if (this.apiToken) {
85
+ return this.apiToken;
86
+ }
87
+ if (!this.pendingTokenPromise) {
88
+ this.pendingTokenPromise = this.fetchToken().then((token) => {
89
+ if (!token) {
90
+ throw new Error("fetchToken must resolve a non-empty API token.");
91
+ }
92
+ this.apiToken = token;
93
+ return token;
94
+ }).finally(() => {
95
+ this.pendingTokenPromise = void 0;
96
+ });
97
+ }
98
+ return this.pendingTokenPromise;
99
+ }
100
+ }
101
+ class NuabaseAPIClient {
102
+ constructor(config = {}) {
103
+ this.baseUrl = resolveConfigValue(config, "baseUrl", "NUABASE_API_URL") || "https://api.nuabase.com";
104
+ this.authTokenManager = new NuabaseAuthTokenManager(config);
105
+ }
106
+ async request(method, urlPath, params) {
59
107
  const url = `${this.baseUrl}/${urlPath}`;
60
108
  let response;
109
+ const hasBody = method !== "GET" && params !== void 0;
110
+ const headers = {
111
+ Authorization: `Bearer ${await this.authTokenManager.getToken()}`
112
+ };
113
+ if (hasBody) {
114
+ headers["Content-Type"] = "application/json";
115
+ }
61
116
  try {
62
117
  response = await fetch(url, {
63
- method: "POST",
64
- headers: {
65
- "Content-Type": "application/json",
66
- Authorization: `Bearer ${this.apiKey}`
67
- },
68
- body: JSON.stringify(params)
118
+ method,
119
+ headers,
120
+ body: hasBody ? JSON.stringify(params) : void 0
69
121
  });
70
122
  } catch (e) {
71
123
  return { error: `Error calling Nuabase API: ${getErrorMessageFromException(e)}` };
@@ -105,6 +157,12 @@ const validateArrayRequestParams = (data, primaryKeyName) => {
105
157
  });
106
158
  };
107
159
 
160
+ const zs_NormalizedUsage = zod.z.object({
161
+ promptTokens: zod.z.number(),
162
+ completionTokens: zod.z.number(),
163
+ totalTokens: zod.z.number()
164
+ }).strict();
165
+
108
166
  const zs_ResponseRecord = (primaryKeyName, outputKey, outputSchema) => {
109
167
  const primaryKeySchema = zod.z.any();
110
168
  const shape = {
@@ -127,7 +185,8 @@ const createNuaApiResponseCastArray = (primaryKeyName, outputKey, outputSchema)
127
185
  cacheHits: zod.z.number(),
128
186
  rowsWithNoResults: zod.z.array(zod.z.string()),
129
187
  isSuccess: zod.z.literal(true),
130
- isError: zod.z.optional(zod.z.literal(false))
188
+ isError: zod.z.optional(zod.z.literal(false)),
189
+ usage: zs_NormalizedUsage
131
190
  }).strict();
132
191
  };
133
192
  const zs_NuaApiResponse_CastArray = createNuaApiResponseCastArray;
@@ -151,6 +210,25 @@ const validateArrayFnDef = (fnDef) => {
151
210
  return parsed.data;
152
211
  };
153
212
 
213
+ function addOriginalInputRows(inputRows, outputRows, primaryKeyName) {
214
+ const lookup = /* @__PURE__ */ new Map();
215
+ for (const row of inputRows) {
216
+ lookup.set(row[primaryKeyName], row);
217
+ }
218
+ return outputRows.map((row) => {
219
+ const primaryKeyValue = row[primaryKeyName];
220
+ const sourceRow = lookup.get(primaryKeyValue);
221
+ if (!sourceRow) {
222
+ throw new Error(
223
+ `Unable to locate source row for primary key "${String(primaryKeyValue)}" in cast array response.`
224
+ );
225
+ }
226
+ return {
227
+ ...row,
228
+ sourceRow
229
+ };
230
+ });
231
+ }
154
232
  const toCastArrayApiRequest = (prompt, data, primaryKeyName, outputName, outputSchema) => ({
155
233
  input: {
156
234
  prompt,
@@ -170,9 +248,10 @@ const createArrayFn = (client, inputFnDef) => {
170
248
  return { error: String(response.error), isError: true };
171
249
  return void 0;
172
250
  };
173
- const baseFn = async (data, primaryKeyName) => {
251
+ const queueFn = async (data, primaryKeyName) => {
174
252
  validateArrayRequestParams(data, primaryKeyName);
175
253
  const response = await client.request(
254
+ "POST",
176
255
  "cast/array",
177
256
  toCastArrayApiRequest(
178
257
  fnDef.prompt,
@@ -191,9 +270,10 @@ const createArrayFn = (client, inputFnDef) => {
191
270
  }
192
271
  return parsedResponse.data;
193
272
  };
194
- const nowFn = async (data, primaryKeyName) => {
273
+ const baseNowFn = async (data, primaryKeyName) => {
195
274
  validateArrayRequestParams(data, primaryKeyName);
196
275
  const response = await client.request(
276
+ "POST",
197
277
  "cast/array/now",
198
278
  toCastArrayApiRequest(
199
279
  fnDef.prompt,
@@ -216,13 +296,18 @@ const createArrayFn = (client, inputFnDef) => {
216
296
  isSuccess: true,
217
297
  llmRequestId: parsedResponse.data.llmRequestId,
218
298
  kind: parsedResponse.data.kind,
219
- data: parsedResponse.data.data,
299
+ data: addOriginalInputRows(
300
+ data,
301
+ parsedResponse.data.data,
302
+ primaryKeyName
303
+ ),
220
304
  rowsWithNoResults: parsedResponse.data.rowsWithNoResults,
221
- cacheHits: parsedResponse.data.cacheHits
305
+ cacheHits: parsedResponse.data.cacheHits,
306
+ usage: parsedResponse.data.usage
222
307
  };
223
308
  };
224
- return Object.assign(baseFn, {
225
- now: nowFn
309
+ return Object.assign(baseNowFn, {
310
+ queue: queueFn
226
311
  });
227
312
  };
228
313
 
@@ -233,7 +318,8 @@ const createNuaApiResponseCastValue = (outputKey, outputSchema) => {
233
318
  data: outputSchema,
234
319
  isCacheHit: zod.z.boolean(),
235
320
  isSuccess: zod.z.literal(true),
236
- isError: zod.z.optional(zod.z.literal(false))
321
+ isError: zod.z.optional(zod.z.literal(false)),
322
+ usage: zs_NormalizedUsage
237
323
  }).strict();
238
324
  };
239
325
  const zs_NuaApiResponse_CastValue = createNuaApiResponseCastValue;
@@ -276,10 +362,11 @@ const createValueFn = (client, inputFnDef) => {
276
362
  return void 0;
277
363
  };
278
364
  const sendRequest = async (path, data) => client.request(
365
+ "POST",
279
366
  path,
280
367
  toCastValueApiRequest(fnDef.prompt, data, fnDef.output.name, outputJsonSchema)
281
368
  );
282
- const baseFn = async (data) => {
369
+ const queueFn = async (data) => {
283
370
  const response = await sendRequest("cast/value", data);
284
371
  const error = toNuabaseError(response);
285
372
  if (error) return error;
@@ -290,7 +377,7 @@ const createValueFn = (client, inputFnDef) => {
290
377
  }
291
378
  return parsedResponse.data;
292
379
  };
293
- const nowFn = async (data) => {
380
+ const baseNowFn = async (data) => {
294
381
  const response = await sendRequest("cast/value/now", data);
295
382
  const error = toNuabaseError(response);
296
383
  if (error) return error;
@@ -309,14 +396,93 @@ const createValueFn = (client, inputFnDef) => {
309
396
  llmRequestId: parsedResponse.data.llmRequestId,
310
397
  kind: parsedResponse.data.kind,
311
398
  data: parsedResponse.data.data,
312
- isCacheHit: parsedResponse.data.isCacheHit
399
+ isCacheHit: parsedResponse.data.isCacheHit,
400
+ usage: parsedResponse.data.usage
313
401
  };
314
402
  };
315
- return Object.assign(baseFn, {
316
- now: nowFn
403
+ return Object.assign(baseNowFn, {
404
+ queue: queueFn
317
405
  });
318
406
  };
319
407
 
408
+ const errorParseFailed = {
409
+ kind: "internal-error",
410
+ message: "Unable to parse stored LLM result"
411
+ };
412
+ const dateFromIso = zod.z.preprocess(
413
+ (value) => {
414
+ if (value == null) return null;
415
+ if (value instanceof Date) return value;
416
+ if (typeof value === "string") {
417
+ const parsed = new Date(value);
418
+ if (!Number.isNaN(parsed.getTime())) return parsed;
419
+ }
420
+ return zod.z.NEVER;
421
+ },
422
+ zod.z.union([zod.z.date(), zod.z.null()])
423
+ );
424
+ const mandatoryDateFromIso = zod.z.preprocess((value) => {
425
+ if (value instanceof Date) return value;
426
+ if (typeof value === "string") {
427
+ const parsed = new Date(value);
428
+ if (!Number.isNaN(parsed.getTime())) return parsed;
429
+ }
430
+ return zod.z.NEVER;
431
+ }, zod.z.date());
432
+ const apiResponseSchema = zod.z.object({
433
+ id: zod.z.string(),
434
+ requestType: zod.z.string(),
435
+ llmStatus: zod.z.union([
436
+ zod.z.literal("pending"),
437
+ zod.z.literal("processing"),
438
+ zod.z.literal("success"),
439
+ zod.z.literal("failed")
440
+ ]),
441
+ sseStatus: zod.z.union([
442
+ zod.z.literal("n/a"),
443
+ zod.z.literal("pending"),
444
+ zod.z.literal("sent"),
445
+ zod.z.literal("failed")
446
+ ]),
447
+ webhookStatus: zod.z.union([
448
+ zod.z.literal("n/a"),
449
+ zod.z.literal("pending"),
450
+ zod.z.literal("sent"),
451
+ zod.z.literal("failed")
452
+ ]),
453
+ input: zod.z.object({
454
+ prompt: zod.z.union([zod.z.string(), zod.z.null()]),
455
+ data: zod.z.union([zod.z.string(), zod.z.null()]),
456
+ primaryKey: zod.z.union([zod.z.string(), zod.z.null()])
457
+ }),
458
+ output: zod.z.object({
459
+ name: zod.z.string(),
460
+ schema: zod.z.string(),
461
+ effectiveSchema: zod.z.string()
462
+ }),
463
+ result: zod.z.union([zod.z.record(zod.z.string(), zod.z.unknown()), zod.z.null()]).optional(),
464
+ error: zod.z.union([zod.z.string(), zod.z.null()]),
465
+ fullPrompt: zod.z.string(),
466
+ model: zod.z.string(),
467
+ provider: zod.z.string(),
468
+ startedAt: dateFromIso,
469
+ finishedAt: dateFromIso,
470
+ createdAt: mandatoryDateFromIso,
471
+ updatedAt: mandatoryDateFromIso
472
+ });
473
+ const isNuabaseError = (value) => Boolean(value && typeof value === "object" && "isError" in value && value.isError);
474
+ async function getRequest(client, llmRequestId) {
475
+ const response = await client.request("GET", `requests/${encodeURIComponent(llmRequestId)}`);
476
+ if (isNuabaseError(response)) {
477
+ return response;
478
+ }
479
+ const parsedResponse = apiResponseSchema.safeParse(response);
480
+ if (!parsedResponse.success) {
481
+ return errorParseFailed;
482
+ }
483
+ return parsedResponse.data;
484
+ }
485
+
320
486
  class Nua {
321
487
  constructor(config) {
322
488
  this.apiClient = new NuabaseAPIClient(config);
@@ -327,6 +493,9 @@ class Nua {
327
493
  createFn(fnDef) {
328
494
  return createValueFn(this.apiClient, fnDef);
329
495
  }
496
+ async getRequest(llmRequestId) {
497
+ return getRequest(this.apiClient, llmRequestId);
498
+ }
330
499
  }
331
500
 
332
501
  exports.Nua = Nua;
@@ -1,5 +1,15 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ function resolveConfigValue(config, key, envVar) {
4
+ if (config[key]) return config[key];
5
+ const pe = globalThis.process;
6
+ const isNode = !!pe?.versions?.node;
7
+ if (isNode) {
8
+ return pe?.env?.[envVar];
9
+ }
10
+ return void 0;
11
+ }
12
+
3
13
  async function parseErrorResponse(response, urlPath) {
4
14
  const fallbackMessage = `Request to ${urlPath} failed with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
5
15
  let responseBody = "";
@@ -43,27 +53,69 @@ function getErrorMessageFromException(error) {
43
53
  return String(error);
44
54
  }
45
55
 
46
- class NuabaseAPIClient {
47
- constructor(config = {}) {
48
- this.apiKey = config.apiKey || process.env.NUABASE_API_KEY || "";
49
- this.baseUrl = config.baseUrl || process.env.NUABASE_API_URL || "https://api.nuabase.com";
50
- if (!this.apiKey) {
56
+ const isBrowserEnvironment = () => typeof window !== "undefined" && !!window.document;
57
+ class NuabaseAuthTokenManager {
58
+ constructor(config) {
59
+ this.apiKey = resolveConfigValue(config, "apiKey", "NUABASE_API_KEY") || void 0;
60
+ this.fetchToken = config.fetchToken;
61
+ if (this.apiKey && this.fetchToken) {
62
+ throw new Error("Provide either an apiKey or a fetchToken function, but not both.");
63
+ }
64
+ if (!this.apiKey && !this.fetchToken) {
65
+ throw new Error(
66
+ "Authentication is required. Provide config.apiKey/NUABASE_API_KEY or a fetchToken function."
67
+ );
68
+ }
69
+ if (this.apiKey && isBrowserEnvironment()) {
51
70
  throw new Error(
52
- "API key is required. Provide it via config.apiKey or NUABASE_API_KEY environment variable."
71
+ "found config.apiKey. It is a private secret that must only be used in server environments."
53
72
  );
54
73
  }
55
74
  }
56
- async request(urlPath, params) {
75
+ async getToken() {
76
+ if (this.apiKey) {
77
+ return this.apiKey;
78
+ }
79
+ if (!this.fetchToken) {
80
+ throw new Error("Unable to resolve API token because no fetchToken function is configured.");
81
+ }
82
+ if (this.apiToken) {
83
+ return this.apiToken;
84
+ }
85
+ if (!this.pendingTokenPromise) {
86
+ this.pendingTokenPromise = this.fetchToken().then((token) => {
87
+ if (!token) {
88
+ throw new Error("fetchToken must resolve a non-empty API token.");
89
+ }
90
+ this.apiToken = token;
91
+ return token;
92
+ }).finally(() => {
93
+ this.pendingTokenPromise = void 0;
94
+ });
95
+ }
96
+ return this.pendingTokenPromise;
97
+ }
98
+ }
99
+ class NuabaseAPIClient {
100
+ constructor(config = {}) {
101
+ this.baseUrl = resolveConfigValue(config, "baseUrl", "NUABASE_API_URL") || "https://api.nuabase.com";
102
+ this.authTokenManager = new NuabaseAuthTokenManager(config);
103
+ }
104
+ async request(method, urlPath, params) {
57
105
  const url = `${this.baseUrl}/${urlPath}`;
58
106
  let response;
107
+ const hasBody = method !== "GET" && params !== void 0;
108
+ const headers = {
109
+ Authorization: `Bearer ${await this.authTokenManager.getToken()}`
110
+ };
111
+ if (hasBody) {
112
+ headers["Content-Type"] = "application/json";
113
+ }
59
114
  try {
60
115
  response = await fetch(url, {
61
- method: "POST",
62
- headers: {
63
- "Content-Type": "application/json",
64
- Authorization: `Bearer ${this.apiKey}`
65
- },
66
- body: JSON.stringify(params)
116
+ method,
117
+ headers,
118
+ body: hasBody ? JSON.stringify(params) : void 0
67
119
  });
68
120
  } catch (e) {
69
121
  return { error: `Error calling Nuabase API: ${getErrorMessageFromException(e)}` };
@@ -103,6 +155,12 @@ const validateArrayRequestParams = (data, primaryKeyName) => {
103
155
  });
104
156
  };
105
157
 
158
+ const zs_NormalizedUsage = z.object({
159
+ promptTokens: z.number(),
160
+ completionTokens: z.number(),
161
+ totalTokens: z.number()
162
+ }).strict();
163
+
106
164
  const zs_ResponseRecord = (primaryKeyName, outputKey, outputSchema) => {
107
165
  const primaryKeySchema = z.any();
108
166
  const shape = {
@@ -125,7 +183,8 @@ const createNuaApiResponseCastArray = (primaryKeyName, outputKey, outputSchema)
125
183
  cacheHits: z.number(),
126
184
  rowsWithNoResults: z.array(z.string()),
127
185
  isSuccess: z.literal(true),
128
- isError: z.optional(z.literal(false))
186
+ isError: z.optional(z.literal(false)),
187
+ usage: zs_NormalizedUsage
129
188
  }).strict();
130
189
  };
131
190
  const zs_NuaApiResponse_CastArray = createNuaApiResponseCastArray;
@@ -149,6 +208,25 @@ const validateArrayFnDef = (fnDef) => {
149
208
  return parsed.data;
150
209
  };
151
210
 
211
+ function addOriginalInputRows(inputRows, outputRows, primaryKeyName) {
212
+ const lookup = /* @__PURE__ */ new Map();
213
+ for (const row of inputRows) {
214
+ lookup.set(row[primaryKeyName], row);
215
+ }
216
+ return outputRows.map((row) => {
217
+ const primaryKeyValue = row[primaryKeyName];
218
+ const sourceRow = lookup.get(primaryKeyValue);
219
+ if (!sourceRow) {
220
+ throw new Error(
221
+ `Unable to locate source row for primary key "${String(primaryKeyValue)}" in cast array response.`
222
+ );
223
+ }
224
+ return {
225
+ ...row,
226
+ sourceRow
227
+ };
228
+ });
229
+ }
152
230
  const toCastArrayApiRequest = (prompt, data, primaryKeyName, outputName, outputSchema) => ({
153
231
  input: {
154
232
  prompt,
@@ -168,9 +246,10 @@ const createArrayFn = (client, inputFnDef) => {
168
246
  return { error: String(response.error), isError: true };
169
247
  return void 0;
170
248
  };
171
- const baseFn = async (data, primaryKeyName) => {
249
+ const queueFn = async (data, primaryKeyName) => {
172
250
  validateArrayRequestParams(data, primaryKeyName);
173
251
  const response = await client.request(
252
+ "POST",
174
253
  "cast/array",
175
254
  toCastArrayApiRequest(
176
255
  fnDef.prompt,
@@ -189,9 +268,10 @@ const createArrayFn = (client, inputFnDef) => {
189
268
  }
190
269
  return parsedResponse.data;
191
270
  };
192
- const nowFn = async (data, primaryKeyName) => {
271
+ const baseNowFn = async (data, primaryKeyName) => {
193
272
  validateArrayRequestParams(data, primaryKeyName);
194
273
  const response = await client.request(
274
+ "POST",
195
275
  "cast/array/now",
196
276
  toCastArrayApiRequest(
197
277
  fnDef.prompt,
@@ -214,13 +294,18 @@ const createArrayFn = (client, inputFnDef) => {
214
294
  isSuccess: true,
215
295
  llmRequestId: parsedResponse.data.llmRequestId,
216
296
  kind: parsedResponse.data.kind,
217
- data: parsedResponse.data.data,
297
+ data: addOriginalInputRows(
298
+ data,
299
+ parsedResponse.data.data,
300
+ primaryKeyName
301
+ ),
218
302
  rowsWithNoResults: parsedResponse.data.rowsWithNoResults,
219
- cacheHits: parsedResponse.data.cacheHits
303
+ cacheHits: parsedResponse.data.cacheHits,
304
+ usage: parsedResponse.data.usage
220
305
  };
221
306
  };
222
- return Object.assign(baseFn, {
223
- now: nowFn
307
+ return Object.assign(baseNowFn, {
308
+ queue: queueFn
224
309
  });
225
310
  };
226
311
 
@@ -231,7 +316,8 @@ const createNuaApiResponseCastValue = (outputKey, outputSchema) => {
231
316
  data: outputSchema,
232
317
  isCacheHit: z.boolean(),
233
318
  isSuccess: z.literal(true),
234
- isError: z.optional(z.literal(false))
319
+ isError: z.optional(z.literal(false)),
320
+ usage: zs_NormalizedUsage
235
321
  }).strict();
236
322
  };
237
323
  const zs_NuaApiResponse_CastValue = createNuaApiResponseCastValue;
@@ -274,10 +360,11 @@ const createValueFn = (client, inputFnDef) => {
274
360
  return void 0;
275
361
  };
276
362
  const sendRequest = async (path, data) => client.request(
363
+ "POST",
277
364
  path,
278
365
  toCastValueApiRequest(fnDef.prompt, data, fnDef.output.name, outputJsonSchema)
279
366
  );
280
- const baseFn = async (data) => {
367
+ const queueFn = async (data) => {
281
368
  const response = await sendRequest("cast/value", data);
282
369
  const error = toNuabaseError(response);
283
370
  if (error) return error;
@@ -288,7 +375,7 @@ const createValueFn = (client, inputFnDef) => {
288
375
  }
289
376
  return parsedResponse.data;
290
377
  };
291
- const nowFn = async (data) => {
378
+ const baseNowFn = async (data) => {
292
379
  const response = await sendRequest("cast/value/now", data);
293
380
  const error = toNuabaseError(response);
294
381
  if (error) return error;
@@ -307,14 +394,93 @@ const createValueFn = (client, inputFnDef) => {
307
394
  llmRequestId: parsedResponse.data.llmRequestId,
308
395
  kind: parsedResponse.data.kind,
309
396
  data: parsedResponse.data.data,
310
- isCacheHit: parsedResponse.data.isCacheHit
397
+ isCacheHit: parsedResponse.data.isCacheHit,
398
+ usage: parsedResponse.data.usage
311
399
  };
312
400
  };
313
- return Object.assign(baseFn, {
314
- now: nowFn
401
+ return Object.assign(baseNowFn, {
402
+ queue: queueFn
315
403
  });
316
404
  };
317
405
 
406
+ const errorParseFailed = {
407
+ kind: "internal-error",
408
+ message: "Unable to parse stored LLM result"
409
+ };
410
+ const dateFromIso = z.preprocess(
411
+ (value) => {
412
+ if (value == null) return null;
413
+ if (value instanceof Date) return value;
414
+ if (typeof value === "string") {
415
+ const parsed = new Date(value);
416
+ if (!Number.isNaN(parsed.getTime())) return parsed;
417
+ }
418
+ return z.NEVER;
419
+ },
420
+ z.union([z.date(), z.null()])
421
+ );
422
+ const mandatoryDateFromIso = z.preprocess((value) => {
423
+ if (value instanceof Date) return value;
424
+ if (typeof value === "string") {
425
+ const parsed = new Date(value);
426
+ if (!Number.isNaN(parsed.getTime())) return parsed;
427
+ }
428
+ return z.NEVER;
429
+ }, z.date());
430
+ const apiResponseSchema = z.object({
431
+ id: z.string(),
432
+ requestType: z.string(),
433
+ llmStatus: z.union([
434
+ z.literal("pending"),
435
+ z.literal("processing"),
436
+ z.literal("success"),
437
+ z.literal("failed")
438
+ ]),
439
+ sseStatus: z.union([
440
+ z.literal("n/a"),
441
+ z.literal("pending"),
442
+ z.literal("sent"),
443
+ z.literal("failed")
444
+ ]),
445
+ webhookStatus: z.union([
446
+ z.literal("n/a"),
447
+ z.literal("pending"),
448
+ z.literal("sent"),
449
+ z.literal("failed")
450
+ ]),
451
+ input: z.object({
452
+ prompt: z.union([z.string(), z.null()]),
453
+ data: z.union([z.string(), z.null()]),
454
+ primaryKey: z.union([z.string(), z.null()])
455
+ }),
456
+ output: z.object({
457
+ name: z.string(),
458
+ schema: z.string(),
459
+ effectiveSchema: z.string()
460
+ }),
461
+ result: z.union([z.record(z.string(), z.unknown()), z.null()]).optional(),
462
+ error: z.union([z.string(), z.null()]),
463
+ fullPrompt: z.string(),
464
+ model: z.string(),
465
+ provider: z.string(),
466
+ startedAt: dateFromIso,
467
+ finishedAt: dateFromIso,
468
+ createdAt: mandatoryDateFromIso,
469
+ updatedAt: mandatoryDateFromIso
470
+ });
471
+ const isNuabaseError = (value) => Boolean(value && typeof value === "object" && "isError" in value && value.isError);
472
+ async function getRequest(client, llmRequestId) {
473
+ const response = await client.request("GET", `requests/${encodeURIComponent(llmRequestId)}`);
474
+ if (isNuabaseError(response)) {
475
+ return response;
476
+ }
477
+ const parsedResponse = apiResponseSchema.safeParse(response);
478
+ if (!parsedResponse.success) {
479
+ return errorParseFailed;
480
+ }
481
+ return parsedResponse.data;
482
+ }
483
+
318
484
  class Nua {
319
485
  constructor(config) {
320
486
  this.apiClient = new NuabaseAPIClient(config);
@@ -325,6 +491,9 @@ class Nua {
325
491
  createFn(fnDef) {
326
492
  return createValueFn(this.apiClient, fnDef);
327
493
  }
494
+ async getRequest(llmRequestId) {
495
+ return getRequest(this.apiClient, llmRequestId);
496
+ }
328
497
  }
329
498
 
330
499
  export { Nua };
@@ -1,11 +1,19 @@
1
1
  import { NuabaseError } from './error-response';
2
- export type NuabaseConfig = {
2
+ type NuabaseApiKeyConfig = {
3
3
  apiKey?: string;
4
+ fetchToken?: never;
4
5
  baseUrl?: string;
5
6
  };
7
+ type NuabaseTokenConfig = {
8
+ fetchToken: () => Promise<string>;
9
+ apiKey?: never;
10
+ baseUrl?: string;
11
+ };
12
+ export type NuabaseConfig = NuabaseApiKeyConfig | NuabaseTokenConfig;
6
13
  export declare class NuabaseAPIClient {
7
- private readonly apiKey;
8
14
  private readonly baseUrl;
15
+ private readonly authTokenManager;
9
16
  constructor(config?: NuabaseConfig);
10
- request(urlPath: string, params: object): Promise<NuabaseError | unknown>;
17
+ request(method: 'GET' | 'POST', urlPath: string, params?: object): Promise<NuabaseError | unknown>;
11
18
  }
19
+ export {};
@@ -0,0 +1 @@
1
+ export declare function resolveConfigValue<T extends Record<string, any>>(config: T, key: keyof T, envVar: string): string | undefined;
@@ -1,3 +1,4 @@
1
+ import { NormalizedUsage } from '../common/normalized-usage';
1
2
  import { z } from 'zod';
2
3
  export type UserDataPKValue = string | number;
3
4
  export type PrimaryKeyedInputRecord<PrimaryKeyName extends string> = Record<PrimaryKeyName, UserDataPKValue> & Record<string, unknown>;
@@ -7,9 +8,10 @@ type NuaApiResponseShape<OutputZodSchema extends z.ZodTypeAny, OutputName extend
7
8
  kind: 'cast/array';
8
9
  data: Array<ResponseRow<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>>;
9
10
  cacheHits: number;
10
- rowsWithNoResults: string[];
11
+ rowsWithNoResults: UserDataPKValue[];
11
12
  isSuccess: true;
12
13
  isError?: false;
14
+ usage: NormalizedUsage;
13
15
  };
14
16
  type NuaApiResponseCastArrayBuilder = <OutputZodSchema extends z.ZodTypeAny, OutputName extends string, PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>>(primaryKeyName: PrimaryKeyName, outputKey: OutputName, outputSchema: OutputZodSchema) => z.ZodType<NuaApiResponseShape<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>>;
15
17
  export declare const zs_NuaApiResponse_CastArray: NuaApiResponseCastArrayBuilder;
@@ -10,9 +10,24 @@ export type ArrayFnDef<OutputName extends string, OutputZodSchema extends z.ZodT
10
10
  };
11
11
  };
12
12
  export declare const validateArrayFnDef: <OutputName extends string, OutputZodSchema extends z.ZodTypeAny>(fnDef: ArrayFnDef<OutputName, OutputZodSchema>) => ArrayFnDef<OutputName, OutputZodSchema>;
13
- export type ArrayFnResult<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>> = NuabaseError | NuaApiResponse_CastArray<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>;
13
+ type CastArraySuccessFromApi<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>> = NuaApiResponse_CastArray<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>;
14
+ type CastArraySourceRowAugmentation<PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>> = {
15
+ /**
16
+ * SDK-added field containing the full input row for convenience; not returned by the API.
17
+ */
18
+ sourceRow: InputRecord;
19
+ };
20
+ type CastArraySuccessRowWithSource<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>> = CastArraySuccessFromApi<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>['data'][number] & CastArraySourceRowAugmentation<PrimaryKeyName, InputRecord>;
21
+ export type CastArraySuccessWithSource<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>> = Omit<CastArraySuccessFromApi<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>, 'data'> & {
22
+ /**
23
+ * Each row mirrors the API payload and includes the SDK-provided `sourceRow`.
24
+ */
25
+ data: CastArraySuccessRowWithSource<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>[];
26
+ };
27
+ export type ArrayFnResult<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>> = NuabaseError | CastArraySuccessWithSource<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>;
14
28
  export type ArrayFnQueuedResult = NuabaseError | NuaQueuedResponse;
15
29
  export type ArrayFn<OutputZodSchema extends z.ZodTypeAny, OutputName extends string> = {
16
- <PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>>(data: InputRecord[], primaryKeyName: PrimaryKeyName): Promise<ArrayFnQueuedResult>;
17
- now: <PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>>(data: InputRecord[], primaryKeyName: PrimaryKeyName) => Promise<ArrayFnResult<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>>;
30
+ <PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>>(data: InputRecord[], primaryKeyName: PrimaryKeyName): Promise<ArrayFnResult<OutputZodSchema, OutputName, PrimaryKeyName, InputRecord>>;
31
+ queue: <PrimaryKeyName extends string, InputRecord extends PrimaryKeyedInputRecord<PrimaryKeyName>>(data: InputRecord[], primaryKeyName: PrimaryKeyName) => Promise<ArrayFnQueuedResult>;
18
32
  };
33
+ export {};
@@ -1,3 +1,4 @@
1
+ import { NormalizedUsage } from '../common/normalized-usage';
1
2
  import { z } from 'zod';
2
3
  type NuaApiResponseShape<OutputZodSchema extends z.ZodTypeAny> = {
3
4
  llmRequestId: string;
@@ -6,6 +7,7 @@ type NuaApiResponseShape<OutputZodSchema extends z.ZodTypeAny> = {
6
7
  isCacheHit: boolean;
7
8
  isSuccess: true;
8
9
  isError?: false;
10
+ usage: NormalizedUsage;
9
11
  };
10
12
  type NuaApiResponseCastValueBuilder = <OutputZodSchema extends z.ZodTypeAny, OutputName extends string>(outputKey: OutputName, outputSchema: OutputZodSchema) => z.ZodType<NuaApiResponseShape<OutputZodSchema>>;
11
13
  export declare const zs_NuaApiResponse_CastValue: NuaApiResponseCastValueBuilder;
@@ -13,6 +13,6 @@ export declare const validateValueFnDef: <OutputName extends string, OutputZodSc
13
13
  export type ValueFnResult<OutputZodSchema extends z.ZodTypeAny, OutputName extends string> = NuabaseError | NuaApiResponse_CastValue<OutputZodSchema, OutputName>;
14
14
  export type ValueFnQueuedResult = NuabaseError | NuaQueuedResponse;
15
15
  export type ValueFn<OutputZodSchema extends z.ZodTypeAny, OutputName extends string> = {
16
- (data: unknown): Promise<ValueFnQueuedResult>;
17
- now: (data: unknown) => Promise<ValueFnResult<OutputZodSchema, OutputName>>;
16
+ (data: unknown): Promise<ValueFnResult<OutputZodSchema, OutputName>>;
17
+ queue: (data: unknown) => Promise<ValueFnQueuedResult>;
18
18
  };
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ export type NormalizedUsage = {
3
+ promptTokens: number;
4
+ completionTokens: number;
5
+ totalTokens: number;
6
+ };
7
+ export declare const zs_NormalizedUsage: z.ZodType<NormalizedUsage>;
@@ -1,10 +1,12 @@
1
1
  import { NuabaseAPIClient, NuabaseConfig } from '../lib/api-client';
2
2
  import { ArrayFn, ArrayFnDef } from './cast_array/types';
3
3
  import { ValueFn, ValueFnDef } from './cast_value/types';
4
+ import { GetRequestResult } from './get-request';
4
5
  import { z } from 'zod';
5
6
  export declare class Nua {
6
7
  apiClient: NuabaseAPIClient;
7
8
  constructor(config?: NuabaseConfig);
8
9
  createArrayFn<OutputName extends string, OutputZodSchema extends z.ZodTypeAny>(fnDef: ArrayFnDef<OutputName, OutputZodSchema>): ArrayFn<OutputZodSchema, OutputName>;
9
10
  createFn<OutputName extends string, OutputZodSchema extends z.ZodTypeAny>(fnDef: ValueFnDef<OutputName, OutputZodSchema>): ValueFn<OutputZodSchema, OutputName>;
11
+ getRequest(llmRequestId: string): Promise<GetRequestResult>;
10
12
  }
@@ -0,0 +1,6 @@
1
+ import { NuabaseAPIClient } from '../../lib/api-client';
2
+ import { NuabaseError } from '../../lib/error-response';
3
+ import { ApiResponseLlmRequest, NuaInternalError } from './types';
4
+ export type GetRequestResult = ApiResponseLlmRequest | NuaInternalError | NuabaseError;
5
+ export declare function getRequest(client: NuabaseAPIClient, llmRequestId: string): Promise<GetRequestResult>;
6
+ export type { ApiResponseLlmRequest, LlmRequestStatus, NuaInternalError, SseRequestStatus, WebhookRequestStatus, } from './types';
@@ -0,0 +1,34 @@
1
+ export type LlmRequestType = string;
2
+ export type LlmRequestStatus = 'pending' | 'processing' | 'success' | 'failed';
3
+ export type SseRequestStatus = 'n/a' | 'pending' | 'sent' | 'failed';
4
+ export type WebhookRequestStatus = 'n/a' | 'pending' | 'sent' | 'failed';
5
+ export type NuaInternalError = {
6
+ kind: 'internal-error';
7
+ message: string;
8
+ };
9
+ export type ApiResponseLlmRequest = {
10
+ id: string;
11
+ requestType: LlmRequestType;
12
+ llmStatus: LlmRequestStatus;
13
+ sseStatus: SseRequestStatus;
14
+ webhookStatus: WebhookRequestStatus;
15
+ input: {
16
+ prompt: string | null;
17
+ data: string | null;
18
+ primaryKey: string | null;
19
+ };
20
+ output: {
21
+ name: string;
22
+ schema: string;
23
+ effectiveSchema: string;
24
+ };
25
+ result?: Record<string, unknown> | null;
26
+ error: string | null;
27
+ fullPrompt: string;
28
+ model: string;
29
+ provider: string;
30
+ startedAt: Date | null;
31
+ finishedAt: Date | null;
32
+ createdAt: Date;
33
+ updatedAt: Date;
34
+ };
@@ -2,3 +2,4 @@ export { Nua } from './core';
2
2
  export { createArrayFn } from './cast_array/factory';
3
3
  export { validateArrayRequestParams } from './cast_array/request-validation';
4
4
  export type { ArrayFn, ArrayFnDef } from './cast_array/types';
5
+ export type { ApiResponseLlmRequest, GetRequestResult, LlmRequestStatus, NuaInternalError, SseRequestStatus, WebhookRequestStatus, } from './get-request';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "nuabase",
3
3
  "description": "Nuabase TypeScript client to create typed functions from LLM prompts",
4
4
  "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
5
- "version": "1.0.1",
5
+ "version": "1.2.0",
6
6
  "type": "module",
7
7
  "author": "Jasim A Basheer <jasim@nuabase.com>",
8
8
  "engines": {