nuabase 1.0.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/LICENSE +19 -0
- package/README.md +210 -0
- package/dist/cjs/index.cjs +178 -0
- package/dist/esm/index.mjs +176 -0
- package/dist/types/index.d.cts +3 -0
- package/dist/types/index.d.mts +3 -0
- package/dist/types/lib/api-client.d.ts +11 -0
- package/dist/types/lib/error-response.d.ts +6 -0
- package/dist/types/lib/error-utils.d.ts +6 -0
- package/dist/types/nua.d.ts +33 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Nuabase TypeScript client
|
|
2
|
+
|
|
3
|
+
Nuabase turns LLM prompts into type-safe functions in under five lines.
|
|
4
|
+
|
|
5
|
+
Example use-cases:
|
|
6
|
+
* Enrich a list of food items with nutrition facts
|
|
7
|
+
* Tag sales leads with more details about the company
|
|
8
|
+
* Convert free-form instructions into precise order forms—directly inside your application.
|
|
9
|
+
|
|
10
|
+
You can describe the transformation once, and get back a well-typed function, and call it like any other code in your application.
|
|
11
|
+
|
|
12
|
+
You don't have to do any custom custom orchestration, or run polling, or schema validation.
|
|
13
|
+
|
|
14
|
+
Behind the scenes, Nuabase runs the async jobs, SSE streaming, webhooks, logging, caching, cost attribution, and strict JSON-schema enforcement. You also get an observability dashboard for every request so you always know what the LLM produced.
|
|
15
|
+
|
|
16
|
+
## At a Glance
|
|
17
|
+
|
|
18
|
+
**1. Input.** Start with the data you want to send to the LLM:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const leads = [
|
|
22
|
+
{ id: 'lead-101', notes: 'Growth-stage SaaS, ~80 employees, wants a demo.' },
|
|
23
|
+
];
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**2. Desired shape.** Describe the structure you expect back:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const LeadInsights = z.object({
|
|
30
|
+
industry: z.string(),
|
|
31
|
+
company_size: z.enum(['SMB', 'Mid-market', 'Enterprise']),
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**3. Declare the prompt.** Turn it into a typed function:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const classifyLeads = new Nua().map({
|
|
39
|
+
name: 'insights',
|
|
40
|
+
output: LeadInsights,
|
|
41
|
+
prompt: 'Classify each lead with industry and company_size bucket.',
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**4. Invoke.** Call it like any other async function:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const result = await classifyLeads.now(leads, 'id');
|
|
49
|
+
if (result.isError) throw new Error(result.error);
|
|
50
|
+
console.log(result.data[0].insights);
|
|
51
|
+
// -> { industry: 'Software', company_size: 'Mid-market' }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
1. Add the Nuabase SDK along with Zod:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install nuabase zod
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. Get your API key from the [Nuabase Console](https://console.nuabase.com/dashboard/api-keys/new).
|
|
63
|
+
|
|
64
|
+
3. Set `NUABASE_API_KEY` environment variable, or pass it directly in `new Nua({apiKey: "KEY"})`):
|
|
65
|
+
|
|
66
|
+
## Lead Enrichment Example
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { Nua } from 'nuabase';
|
|
70
|
+
import { z } from 'zod';
|
|
71
|
+
|
|
72
|
+
const nua = new Nua({ apiKey: 'API-KEY' });
|
|
73
|
+
|
|
74
|
+
const LeadInsights = z.object({
|
|
75
|
+
company_name: z.string(),
|
|
76
|
+
industry: z.string(),
|
|
77
|
+
company_size: z.enum(['SMB', 'Mid-market', 'Enterprise']),
|
|
78
|
+
recommended_follow_up: z.enum(['Call', 'Email', 'Event']),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const enrichLeads = nua.map({
|
|
82
|
+
name: 'leadInsights',
|
|
83
|
+
output: LeadInsights,
|
|
84
|
+
prompt:
|
|
85
|
+
'Summarize each inbound lead by extracting company_name, industry, company_size bucket (SMB, Mid-market, Enterprise), and the recommended_follow_up channel (Call, Email, or Event) based on their notes.',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const rows = [
|
|
89
|
+
{
|
|
90
|
+
leadId: 'lead-101',
|
|
91
|
+
name: 'Acme Analytics',
|
|
92
|
+
notes: 'Signed up after webinar, growth-stage SaaS, 80 employees. Wants a live demo next week.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
leadId: 'lead-102',
|
|
96
|
+
name: 'Bright Logistics',
|
|
97
|
+
notes: 'Regional freight operator asking for pricing via email. 12 depots across the Midwest.',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
leadId: 'lead-103',
|
|
101
|
+
name: 'Nimbus Retail',
|
|
102
|
+
notes:
|
|
103
|
+
'Enterprise retailer. Mentioned they will send the procurement team to our booth at NRF.',
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const response = await enrichLeads.now(rows, 'leadId'); // `leadId` is the primary key field
|
|
108
|
+
|
|
109
|
+
if (response.isError) {
|
|
110
|
+
console.error(response.error);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
response.data.forEach(({ id, leadInsights }) => {
|
|
115
|
+
// leadInsights satisfies the LeadInsights schema
|
|
116
|
+
console.log(id, leadInsights.company_name, leadInsights.industry, leadInsights.company_size);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log(response.data);
|
|
120
|
+
// -> [
|
|
121
|
+
// {
|
|
122
|
+
// id: 'lead-101',
|
|
123
|
+
// leadInsights: {
|
|
124
|
+
// company_name: 'Acme Analytics',
|
|
125
|
+
// industry: 'Software',
|
|
126
|
+
// company_size: 'Mid-market',
|
|
127
|
+
// recommended_follow_up: 'Call',
|
|
128
|
+
// },
|
|
129
|
+
// },
|
|
130
|
+
// // ...
|
|
131
|
+
// ];
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`map(data, primaryKey)` submits an asynchronous mapping job. But `map.now(data, primaryKey)` is immediate — it waits for completion and returns the full result.
|
|
135
|
+
|
|
136
|
+
## Why Nuabase
|
|
137
|
+
|
|
138
|
+
- Type-safe from prompt to runtime: Zod schemas compile to JSON Schema, and the SDK re-validates every response before you see it.
|
|
139
|
+
- Zero glue code: Nuabase runs the queueing, retries, webhooks, SSE streaming, and polling so your application code stays clean.
|
|
140
|
+
- Built-in performance: granular caching, cost attribution, and usage metrics come for free on every request.
|
|
141
|
+
- Production visibility: every run shows up in the Nuabase dashboard with logs, prompt/response history, and tracing ids.
|
|
142
|
+
|
|
143
|
+
## API Response
|
|
144
|
+
|
|
145
|
+
Every call returns a discriminated union. When `isError` is `true`, you get a flat error object. Otherwise `isSuccess` is `true`, and `data` contains rows merged with your typed output—along with metadata for observability and caching.
|
|
146
|
+
|
|
147
|
+
**Success**
|
|
148
|
+
|
|
149
|
+
| Field | Type | Description |
|
|
150
|
+
| ------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
|
|
151
|
+
| `isSuccess` | `true` | Indicates that the request completed successfully. |
|
|
152
|
+
| `data` | `Array<{ [primaryKey]: unknown; [outputName]: z.infer<typeof output> }>` | Rows that merged the original primary key with the validated schema output. |
|
|
153
|
+
| `llmRequestId` | `string` | Unique identifier for tracing each Nuabase LLM execution. |
|
|
154
|
+
| `cacheHits` | `number` | Count of input rows served from Nuabase response cache. |
|
|
155
|
+
| `rowsWithNoResults` | `string[]` | Primary keys that could not be resolved by the model. |
|
|
156
|
+
| `isError` | `false \| undefined` | Absent on success; included for type narrowing. |
|
|
157
|
+
|
|
158
|
+
**Error**
|
|
159
|
+
|
|
160
|
+
| Field | Type | Description |
|
|
161
|
+
| --------- | -------- | -------------------------------------------------------- |
|
|
162
|
+
| `isError` | `true` | Indicates the request failed. |
|
|
163
|
+
| `error` | `string` | Message describing the error returned by the API or SDK. |
|
|
164
|
+
|
|
165
|
+
## SDK API
|
|
166
|
+
|
|
167
|
+
**`new Nua(config?)`**
|
|
168
|
+
Creates a client bound to your Nuabase account. Provide:
|
|
169
|
+
|
|
170
|
+
- `apiKey?: string` – API key for authentication. Defaults to `process.env.NUABASE_API_KEY`.
|
|
171
|
+
- `baseUrl?: string` – Override the API host. Defaults to `https://api.nuabase.com`.
|
|
172
|
+
|
|
173
|
+
**`nua.map({ name, prompt, output })`**
|
|
174
|
+
Declares a typed mapping function backed by Nuabase. Parameters:
|
|
175
|
+
|
|
176
|
+
- `name: string` – Label applied to the enriched payload in each response row (e.g., `result.data[i][name]`).
|
|
177
|
+
- `prompt: string` – Natural-language instructions sent to the LLM.
|
|
178
|
+
- `output: ZodSchema` – Zod schema describing the output shape. Nuabase stores the JSON Schema twin and validates responses against it before returning.
|
|
179
|
+
|
|
180
|
+
Returns a callable object (the mapper) with two entry points:
|
|
181
|
+
|
|
182
|
+
- `mapper(data, primaryKey)` – Submits the job asynchronously and returns immediately with metadata and eventual results.
|
|
183
|
+
- `mapper.now(data, primaryKey)` – Runs the job and waits until the full enriched result set is ready.
|
|
184
|
+
|
|
185
|
+
Both entry points expect:
|
|
186
|
+
|
|
187
|
+
- `data: Array<Record<string, unknown>>` – Rows you want Nuabase to enrich. Each row must include the primary key.
|
|
188
|
+
- `primaryKey: string` – Field name that uniquely identifies each row within `data`. The SDK throws if the field is missing on any item.
|
|
189
|
+
|
|
190
|
+
Both entry points resolve to either:
|
|
191
|
+
|
|
192
|
+
- **Success** – `{ isSuccess: true, data, llmRequestId, cacheHits, rowsWithNoResults }`.
|
|
193
|
+
- **Error** – `{ isError: true, error }`.
|
|
194
|
+
|
|
195
|
+
## Next Steps
|
|
196
|
+
|
|
197
|
+
- Create an API key or inspect request logs in the [Nuabase Console](https://console.nuabase.com/).
|
|
198
|
+
- Explore more SDK usage patterns (SSE streaming, webhooks, caching) in the full docs at https://docs.nuabase.com.
|
|
199
|
+
|
|
200
|
+
## Support
|
|
201
|
+
|
|
202
|
+
Email us at [hello@nuabase.com](mailto:hello@nuabase.com). On X at [@NuabaseHQ](x.com/NuabaseHQ).
|
|
203
|
+
|
|
204
|
+
## Credits
|
|
205
|
+
|
|
206
|
+
The template for this package comes from https://github.com/rtivital/ts-package-template
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
5
|
+
async function parseErrorResponse(response, urlPath) {
|
|
6
|
+
const fallbackMessage = `Request to ${urlPath} failed with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
|
|
7
|
+
let responseBody = "";
|
|
8
|
+
try {
|
|
9
|
+
responseBody = (await response.text()).trim();
|
|
10
|
+
} catch {
|
|
11
|
+
return { error: fallbackMessage, isError: true };
|
|
12
|
+
}
|
|
13
|
+
if (!responseBody) {
|
|
14
|
+
return { error: fallbackMessage, isError: true };
|
|
15
|
+
}
|
|
16
|
+
const parsedBody = safeParseJson(responseBody);
|
|
17
|
+
if (parsedBody && typeof parsedBody === "object") {
|
|
18
|
+
const errorMessage = extractErrorMessage(parsedBody);
|
|
19
|
+
if (errorMessage) {
|
|
20
|
+
return { error: errorMessage, isError: true };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { error: responseBody, isError: true };
|
|
24
|
+
}
|
|
25
|
+
function safeParseJson(payload) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(payload);
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function extractErrorMessage(body) {
|
|
33
|
+
const e = body["error"];
|
|
34
|
+
if (e && typeof e === "string") return e;
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isErrorWithMessage(error) {
|
|
39
|
+
return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string";
|
|
40
|
+
}
|
|
41
|
+
function getErrorMessageFromException(error) {
|
|
42
|
+
if (isErrorWithMessage(error)) {
|
|
43
|
+
return error.message;
|
|
44
|
+
}
|
|
45
|
+
return String(error);
|
|
46
|
+
}
|
|
47
|
+
|
|
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) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"API key is required. Provide it via config.apiKey or NUABASE_API_KEY environment variable."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async request(urlPath, params) {
|
|
59
|
+
const url = `${this.baseUrl}/${urlPath}`;
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
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)
|
|
69
|
+
});
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return { error: `Error calling Nuabase API: ${getErrorMessageFromException(e)}` };
|
|
72
|
+
}
|
|
73
|
+
let result;
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
return await parseErrorResponse(response, urlPath);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
result = await response.json();
|
|
79
|
+
} catch {
|
|
80
|
+
return { error: "Invalid response received from Nuabase API call, unable to parse" };
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const zs_MapResponseRecord = (primaryKeyName, outputKey, outputSchema) => {
|
|
87
|
+
const primaryKeySchema = zod.z.any();
|
|
88
|
+
return zod.z.object({
|
|
89
|
+
[primaryKeyName]: primaryKeySchema,
|
|
90
|
+
[outputKey]: outputSchema
|
|
91
|
+
}).strict();
|
|
92
|
+
};
|
|
93
|
+
const zs_MapSuccessDataResult = (primaryKeyName, outputKey, outputSchema) => {
|
|
94
|
+
const recordSchema = zs_MapResponseRecord(primaryKeyName, outputKey, outputSchema);
|
|
95
|
+
return zod.z.array(recordSchema);
|
|
96
|
+
};
|
|
97
|
+
const zs_MapRequestApiResponse_Success = (primaryKeyName, outputKey, outputSchema) => {
|
|
98
|
+
const dataSchema = zs_MapSuccessDataResult(primaryKeyName, outputKey, outputSchema);
|
|
99
|
+
return zod.z.object({
|
|
100
|
+
llmRequestId: zod.z.string(),
|
|
101
|
+
kind: zod.z.literal("map"),
|
|
102
|
+
data: dataSchema,
|
|
103
|
+
cacheHits: zod.z.number(),
|
|
104
|
+
rowsWithNoResults: zod.z.array(zod.z.string()),
|
|
105
|
+
isSuccess: zod.z.literal(true),
|
|
106
|
+
isError: zod.z.optional(zod.z.literal(false))
|
|
107
|
+
}).strict();
|
|
108
|
+
};
|
|
109
|
+
class Nua {
|
|
110
|
+
constructor(config) {
|
|
111
|
+
this.apiClient = new NuabaseAPIClient(config);
|
|
112
|
+
}
|
|
113
|
+
// Some of the checks here mirror type validation, but since our code could be run from
|
|
114
|
+
// untyped JavaScript, we still want runtime checks
|
|
115
|
+
static validateMapRequestParams(data, primaryKeyName) {
|
|
116
|
+
if (!Array.isArray(data)) {
|
|
117
|
+
throw new Error("`data` must be an array of objects.");
|
|
118
|
+
}
|
|
119
|
+
if (!primaryKeyName || typeof primaryKeyName !== "string") {
|
|
120
|
+
throw new Error("`primaryKeyName` must be a non-empty string.");
|
|
121
|
+
}
|
|
122
|
+
data.forEach((item, index) => {
|
|
123
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
124
|
+
throw new Error(`data[${index}] must be a non-null object.`);
|
|
125
|
+
}
|
|
126
|
+
if (!(primaryKeyName in item)) {
|
|
127
|
+
throw new Error(`data[${index}] must contain property '${primaryKeyName}'.`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
map(fnDef) {
|
|
132
|
+
const client = this.apiClient;
|
|
133
|
+
const outputJsonSchema = zod.z.toJSONSchema(fnDef.output);
|
|
134
|
+
const makeRequest = async (path, data, primaryKeyName) => {
|
|
135
|
+
Nua.validateMapRequestParams(data, primaryKeyName);
|
|
136
|
+
const response = await client.request(path, {
|
|
137
|
+
input: {
|
|
138
|
+
prompt: fnDef.prompt,
|
|
139
|
+
data,
|
|
140
|
+
primaryKey: primaryKeyName
|
|
141
|
+
},
|
|
142
|
+
output: {
|
|
143
|
+
schema: outputJsonSchema,
|
|
144
|
+
name: fnDef.name
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (response && typeof response === "object" && "error" in response)
|
|
148
|
+
return { error: String(response.error), isError: true };
|
|
149
|
+
const outputKey = fnDef.name;
|
|
150
|
+
const successSchema = zs_MapRequestApiResponse_Success(primaryKeyName, outputKey, fnDef.output);
|
|
151
|
+
const parsedResponse = successSchema.safeParse(response);
|
|
152
|
+
if (!parsedResponse.success) {
|
|
153
|
+
const emsg = zod.z.prettifyError(parsedResponse.error);
|
|
154
|
+
return { error: `Invalid data received from Nuabase API: ${emsg}`, isError: true };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
isSuccess: true,
|
|
158
|
+
llmRequestId: parsedResponse.data.llmRequestId,
|
|
159
|
+
kind: parsedResponse.data.kind,
|
|
160
|
+
data: parsedResponse.data.data,
|
|
161
|
+
rowsWithNoResults: parsedResponse.data.rowsWithNoResults,
|
|
162
|
+
cacheHits: parsedResponse.data.cacheHits
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const baseInvoker = async (data, primaryKeyName) => {
|
|
166
|
+
return await makeRequest("map", data, primaryKeyName);
|
|
167
|
+
};
|
|
168
|
+
const nowInvoker = async (data, primaryKeyName) => {
|
|
169
|
+
return await makeRequest("map/now", data, primaryKeyName);
|
|
170
|
+
};
|
|
171
|
+
const fnReturnValue = Object.assign(baseInvoker, {
|
|
172
|
+
now: nowInvoker
|
|
173
|
+
});
|
|
174
|
+
return fnReturnValue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
exports.Nua = Nua;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
async function parseErrorResponse(response, urlPath) {
|
|
4
|
+
const fallbackMessage = `Request to ${urlPath} failed with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
|
|
5
|
+
let responseBody = "";
|
|
6
|
+
try {
|
|
7
|
+
responseBody = (await response.text()).trim();
|
|
8
|
+
} catch {
|
|
9
|
+
return { error: fallbackMessage, isError: true };
|
|
10
|
+
}
|
|
11
|
+
if (!responseBody) {
|
|
12
|
+
return { error: fallbackMessage, isError: true };
|
|
13
|
+
}
|
|
14
|
+
const parsedBody = safeParseJson(responseBody);
|
|
15
|
+
if (parsedBody && typeof parsedBody === "object") {
|
|
16
|
+
const errorMessage = extractErrorMessage(parsedBody);
|
|
17
|
+
if (errorMessage) {
|
|
18
|
+
return { error: errorMessage, isError: true };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { error: responseBody, isError: true };
|
|
22
|
+
}
|
|
23
|
+
function safeParseJson(payload) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(payload);
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function extractErrorMessage(body) {
|
|
31
|
+
const e = body["error"];
|
|
32
|
+
if (e && typeof e === "string") return e;
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isErrorWithMessage(error) {
|
|
37
|
+
return typeof error === "object" && error !== null && "message" in error && typeof error.message === "string";
|
|
38
|
+
}
|
|
39
|
+
function getErrorMessageFromException(error) {
|
|
40
|
+
if (isErrorWithMessage(error)) {
|
|
41
|
+
return error.message;
|
|
42
|
+
}
|
|
43
|
+
return String(error);
|
|
44
|
+
}
|
|
45
|
+
|
|
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) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"API key is required. Provide it via config.apiKey or NUABASE_API_KEY environment variable."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async request(urlPath, params) {
|
|
57
|
+
const url = `${this.baseUrl}/${urlPath}`;
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
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)
|
|
67
|
+
});
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { error: `Error calling Nuabase API: ${getErrorMessageFromException(e)}` };
|
|
70
|
+
}
|
|
71
|
+
let result;
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
return await parseErrorResponse(response, urlPath);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
result = await response.json();
|
|
77
|
+
} catch {
|
|
78
|
+
return { error: "Invalid response received from Nuabase API call, unable to parse" };
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const zs_MapResponseRecord = (primaryKeyName, outputKey, outputSchema) => {
|
|
85
|
+
const primaryKeySchema = z.any();
|
|
86
|
+
return z.object({
|
|
87
|
+
[primaryKeyName]: primaryKeySchema,
|
|
88
|
+
[outputKey]: outputSchema
|
|
89
|
+
}).strict();
|
|
90
|
+
};
|
|
91
|
+
const zs_MapSuccessDataResult = (primaryKeyName, outputKey, outputSchema) => {
|
|
92
|
+
const recordSchema = zs_MapResponseRecord(primaryKeyName, outputKey, outputSchema);
|
|
93
|
+
return z.array(recordSchema);
|
|
94
|
+
};
|
|
95
|
+
const zs_MapRequestApiResponse_Success = (primaryKeyName, outputKey, outputSchema) => {
|
|
96
|
+
const dataSchema = zs_MapSuccessDataResult(primaryKeyName, outputKey, outputSchema);
|
|
97
|
+
return z.object({
|
|
98
|
+
llmRequestId: z.string(),
|
|
99
|
+
kind: z.literal("map"),
|
|
100
|
+
data: dataSchema,
|
|
101
|
+
cacheHits: z.number(),
|
|
102
|
+
rowsWithNoResults: z.array(z.string()),
|
|
103
|
+
isSuccess: z.literal(true),
|
|
104
|
+
isError: z.optional(z.literal(false))
|
|
105
|
+
}).strict();
|
|
106
|
+
};
|
|
107
|
+
class Nua {
|
|
108
|
+
constructor(config) {
|
|
109
|
+
this.apiClient = new NuabaseAPIClient(config);
|
|
110
|
+
}
|
|
111
|
+
// Some of the checks here mirror type validation, but since our code could be run from
|
|
112
|
+
// untyped JavaScript, we still want runtime checks
|
|
113
|
+
static validateMapRequestParams(data, primaryKeyName) {
|
|
114
|
+
if (!Array.isArray(data)) {
|
|
115
|
+
throw new Error("`data` must be an array of objects.");
|
|
116
|
+
}
|
|
117
|
+
if (!primaryKeyName || typeof primaryKeyName !== "string") {
|
|
118
|
+
throw new Error("`primaryKeyName` must be a non-empty string.");
|
|
119
|
+
}
|
|
120
|
+
data.forEach((item, index) => {
|
|
121
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
122
|
+
throw new Error(`data[${index}] must be a non-null object.`);
|
|
123
|
+
}
|
|
124
|
+
if (!(primaryKeyName in item)) {
|
|
125
|
+
throw new Error(`data[${index}] must contain property '${primaryKeyName}'.`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
map(fnDef) {
|
|
130
|
+
const client = this.apiClient;
|
|
131
|
+
const outputJsonSchema = z.toJSONSchema(fnDef.output);
|
|
132
|
+
const makeRequest = async (path, data, primaryKeyName) => {
|
|
133
|
+
Nua.validateMapRequestParams(data, primaryKeyName);
|
|
134
|
+
const response = await client.request(path, {
|
|
135
|
+
input: {
|
|
136
|
+
prompt: fnDef.prompt,
|
|
137
|
+
data,
|
|
138
|
+
primaryKey: primaryKeyName
|
|
139
|
+
},
|
|
140
|
+
output: {
|
|
141
|
+
schema: outputJsonSchema,
|
|
142
|
+
name: fnDef.name
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
if (response && typeof response === "object" && "error" in response)
|
|
146
|
+
return { error: String(response.error), isError: true };
|
|
147
|
+
const outputKey = fnDef.name;
|
|
148
|
+
const successSchema = zs_MapRequestApiResponse_Success(primaryKeyName, outputKey, fnDef.output);
|
|
149
|
+
const parsedResponse = successSchema.safeParse(response);
|
|
150
|
+
if (!parsedResponse.success) {
|
|
151
|
+
const emsg = z.prettifyError(parsedResponse.error);
|
|
152
|
+
return { error: `Invalid data received from Nuabase API: ${emsg}`, isError: true };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
isSuccess: true,
|
|
156
|
+
llmRequestId: parsedResponse.data.llmRequestId,
|
|
157
|
+
kind: parsedResponse.data.kind,
|
|
158
|
+
data: parsedResponse.data.data,
|
|
159
|
+
rowsWithNoResults: parsedResponse.data.rowsWithNoResults,
|
|
160
|
+
cacheHits: parsedResponse.data.cacheHits
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
const baseInvoker = async (data, primaryKeyName) => {
|
|
164
|
+
return await makeRequest("map", data, primaryKeyName);
|
|
165
|
+
};
|
|
166
|
+
const nowInvoker = async (data, primaryKeyName) => {
|
|
167
|
+
return await makeRequest("map/now", data, primaryKeyName);
|
|
168
|
+
};
|
|
169
|
+
const fnReturnValue = Object.assign(baseInvoker, {
|
|
170
|
+
now: nowInvoker
|
|
171
|
+
});
|
|
172
|
+
return fnReturnValue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { Nua };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NuabaseError } from './error-response';
|
|
2
|
+
export type NuabaseConfig = {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class NuabaseAPIClient {
|
|
7
|
+
private readonly apiKey;
|
|
8
|
+
private readonly baseUrl;
|
|
9
|
+
constructor(config?: NuabaseConfig);
|
|
10
|
+
request(urlPath: string, params: object): Promise<NuabaseError | unknown>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NuabaseAPIClient, NuabaseConfig } from './lib/api-client';
|
|
2
|
+
import { NuabaseError } from './lib/error-response';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
type MappingFnDef<OutputName extends string, OutputZodSchema extends z.ZodTypeAny> = {
|
|
5
|
+
name: OutputName;
|
|
6
|
+
prompt: string;
|
|
7
|
+
output: OutputZodSchema;
|
|
8
|
+
};
|
|
9
|
+
type MapResponseRecord<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, InputRecord extends Record<string, unknown>, PrimaryKeyName extends keyof InputRecord & string> = {
|
|
10
|
+
[Key in PrimaryKeyName | OutputName]: Key extends PrimaryKeyName ? InputRecord[PrimaryKeyName] : z.infer<OutputZodSchema>;
|
|
11
|
+
};
|
|
12
|
+
type MapSuccessDataResult<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, InputRecord extends Record<string, unknown>, PrimaryKeyName extends keyof InputRecord & string> = Array<MapResponseRecord<OutputZodSchema, OutputName, InputRecord, PrimaryKeyName>>;
|
|
13
|
+
type MapRequestApiResponse_Success<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, InputRecord extends Record<string, unknown>, PrimaryKeyName extends keyof InputRecord & string> = {
|
|
14
|
+
data: MapSuccessDataResult<OutputZodSchema, OutputName, InputRecord, PrimaryKeyName>;
|
|
15
|
+
cacheHits: number;
|
|
16
|
+
llmRequestId: string;
|
|
17
|
+
kind: 'map';
|
|
18
|
+
rowsWithNoResults: string[];
|
|
19
|
+
isSuccess: true;
|
|
20
|
+
isError?: false;
|
|
21
|
+
};
|
|
22
|
+
type MapResult<OutputZodSchema extends z.ZodTypeAny, OutputName extends string, InputRecord extends Record<string, unknown>, PrimaryKeyName extends keyof InputRecord & string> = NuabaseError | MapRequestApiResponse_Success<OutputZodSchema, OutputName, InputRecord, PrimaryKeyName>;
|
|
23
|
+
type MapInvoker<OutputZodSchema extends z.ZodTypeAny, OutputName extends string> = {
|
|
24
|
+
<InputRecord extends Record<string, unknown>, PrimaryKeyName extends keyof InputRecord & string>(data: InputRecord[], primaryKeyName: PrimaryKeyName): Promise<MapResult<OutputZodSchema, OutputName, InputRecord, PrimaryKeyName>>;
|
|
25
|
+
now: <InputRecord extends Record<string, unknown>, PrimaryKeyName extends keyof InputRecord & string>(data: InputRecord[], primaryKeyName: PrimaryKeyName) => Promise<MapResult<OutputZodSchema, OutputName, InputRecord, PrimaryKeyName>>;
|
|
26
|
+
};
|
|
27
|
+
export declare class Nua {
|
|
28
|
+
apiClient: NuabaseAPIClient;
|
|
29
|
+
constructor(config?: NuabaseConfig);
|
|
30
|
+
static validateMapRequestParams(data: unknown, primaryKeyName: unknown): void;
|
|
31
|
+
map<OutputName extends string, OutputZodSchema extends z.ZodTypeAny>(fnDef: MappingFnDef<OutputName, OutputZodSchema>): MapInvoker<OutputZodSchema, OutputName>;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuabase",
|
|
3
|
+
"description": "Nuabase TypeScript client to create typed functions from LLM prompts",
|
|
4
|
+
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "Jasim A Basheer <jasim@nuabase.com>",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=16.6.0"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"typescript",
|
|
13
|
+
"nuabase",
|
|
14
|
+
"ai",
|
|
15
|
+
"llm"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/nuabase/ts-sdk.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/nuabase/ts-sdk/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/nuabase/ts-sdk#readme",
|
|
26
|
+
"main": "./dist/cjs/index.cjs",
|
|
27
|
+
"types": "./dist/types/index.d.cts",
|
|
28
|
+
"module": "./dist/esm/index.mjs",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/types/index.d.mts",
|
|
33
|
+
"default": "./dist/esm/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/types/index.d.cts",
|
|
37
|
+
"default": "./dist/cjs/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"zod": "^4.1.5"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"clean": "rimraf dist",
|
|
46
|
+
"generate-dts": "tsx scripts/generate-dts",
|
|
47
|
+
"build": "rollup -c rollup.config.js && npm run generate-dts",
|
|
48
|
+
"pack:test": "tsx scripts/pack-test",
|
|
49
|
+
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
|
50
|
+
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"lint": "eslint --cache .",
|
|
53
|
+
"jest": "jest",
|
|
54
|
+
"test": "npm run prettier:check && npm run typecheck && npm run lint && npm run jest && npm run build && npm run pack:test",
|
|
55
|
+
"release": "npm test && tsx scripts/release",
|
|
56
|
+
"knip": "knip"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@eslint/js": "^9.35.0",
|
|
60
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
61
|
+
"@rollup/plugin-commonjs": "^28.0.6",
|
|
62
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
63
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
64
|
+
"@types/fs-extra": "^11",
|
|
65
|
+
"@types/jest": "^30.0.0",
|
|
66
|
+
"@types/node": "^24.3.3",
|
|
67
|
+
"@types/signale": "^1",
|
|
68
|
+
"@types/yargs": "^17.0.33",
|
|
69
|
+
"chalk": "^5.6.2",
|
|
70
|
+
"esbuild": "^0.25.9",
|
|
71
|
+
"eslint": "^9.35.0",
|
|
72
|
+
"fs-extra": "^11.3.1",
|
|
73
|
+
"jest": "^30.1.3",
|
|
74
|
+
"knip": "^5.63.1",
|
|
75
|
+
"new-github-release-url": "^2.0.0",
|
|
76
|
+
"open": "^10.2.0",
|
|
77
|
+
"prettier": "^3.6.2",
|
|
78
|
+
"rimraf": "^6.0.1",
|
|
79
|
+
"rollup": "^4.50.1",
|
|
80
|
+
"rollup-plugin-esbuild": "^6.2.1",
|
|
81
|
+
"rollup-plugin-node-externals": "^8.1.1",
|
|
82
|
+
"signale": "^1.4.0",
|
|
83
|
+
"simple-git": "^3.28.0",
|
|
84
|
+
"ts-jest": "^29.4.1",
|
|
85
|
+
"tsx": "^4.20.5",
|
|
86
|
+
"typescript": "^5.9.2",
|
|
87
|
+
"typescript-eslint": "^8.43.0",
|
|
88
|
+
"version-next": "^1.0.2",
|
|
89
|
+
"yargs": "^18.0.0",
|
|
90
|
+
"zod": "^4.1.5",
|
|
91
|
+
"zx": "^8.8.1"
|
|
92
|
+
}
|
|
93
|
+
}
|