@permute/sdk 0.1.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 +201 -0
- package/README.md +212 -0
- package/dist/client.d.ts +100 -0
- package/dist/client.js +553 -0
- package/dist/generated/openapi.d.ts +536 -0
- package/dist/generated/openapi.js +1 -0
- package/dist/generated/runtime-contract.d.ts +12 -0
- package/dist/generated/runtime-contract.js +1757 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { publicApiRoutes } from './generated/runtime-contract.js';
|
|
4
|
+
const DEFAULT_BASE_URL = 'https://api.permute.ai';
|
|
5
|
+
const DEFAULT_POLL_TIMEOUT_MS = 20 * 60 * 1000;
|
|
6
|
+
export class PermuteApiError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
code;
|
|
9
|
+
details;
|
|
10
|
+
requestId;
|
|
11
|
+
constructor(input) {
|
|
12
|
+
super(input.message);
|
|
13
|
+
this.name = 'PermuteApiError';
|
|
14
|
+
this.status = input.status;
|
|
15
|
+
this.code = input.code;
|
|
16
|
+
this.details = input.details;
|
|
17
|
+
this.requestId = input.requestId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function routeFor(operationId) {
|
|
21
|
+
const route = publicApiRoutes[operationId];
|
|
22
|
+
if (!route) {
|
|
23
|
+
throw new PermuteApiError({
|
|
24
|
+
status: 0,
|
|
25
|
+
code: 'missing_route',
|
|
26
|
+
message: `Missing API route for ${operationId}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return route;
|
|
30
|
+
}
|
|
31
|
+
function buildPath(route, pathParams = {}) {
|
|
32
|
+
return route.path.replace(/\{([^}]+)\}/g, (_match, key) => {
|
|
33
|
+
const value = pathParams[key];
|
|
34
|
+
if (!value) {
|
|
35
|
+
throw new PermuteApiError({
|
|
36
|
+
status: 0,
|
|
37
|
+
code: 'missing_path_param',
|
|
38
|
+
message: `Missing path parameter: ${key}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return encodeURIComponent(value);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function parseJsonResponse(response) {
|
|
45
|
+
return response.text().then((text) => {
|
|
46
|
+
if (!text)
|
|
47
|
+
return {};
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new PermuteApiError({
|
|
53
|
+
status: response.status,
|
|
54
|
+
code: 'invalid_json',
|
|
55
|
+
message: 'API response was not valid JSON',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function isErrorEnvelope(value) {
|
|
61
|
+
return (!!value &&
|
|
62
|
+
typeof value === 'object' &&
|
|
63
|
+
'error' in value &&
|
|
64
|
+
!!value.error &&
|
|
65
|
+
typeof value.error.code === 'string' &&
|
|
66
|
+
typeof value.error.message === 'string');
|
|
67
|
+
}
|
|
68
|
+
function isSuccessEnvelope(value) {
|
|
69
|
+
return !!value && typeof value === 'object' && 'data' in value;
|
|
70
|
+
}
|
|
71
|
+
function validationError(operationId, kind, errors) {
|
|
72
|
+
return new PermuteApiError({
|
|
73
|
+
status: 0,
|
|
74
|
+
code: `invalid_${kind}`,
|
|
75
|
+
message: `Invalid ${kind} for ${operationId}`,
|
|
76
|
+
details: errors?.map(({ instancePath, schemaPath, keyword, message, params }) => ({
|
|
77
|
+
instancePath,
|
|
78
|
+
schemaPath,
|
|
79
|
+
keyword,
|
|
80
|
+
message,
|
|
81
|
+
params,
|
|
82
|
+
})),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const ajv = new Ajv({ allErrors: true, strict: false, validateFormats: false });
|
|
86
|
+
const validators = new WeakMap();
|
|
87
|
+
function validatorFor(schema) {
|
|
88
|
+
const existing = validators.get(schema);
|
|
89
|
+
if (existing)
|
|
90
|
+
return existing;
|
|
91
|
+
const validator = ajv.compile(schema);
|
|
92
|
+
validators.set(schema, validator);
|
|
93
|
+
return validator;
|
|
94
|
+
}
|
|
95
|
+
function inferUploadSize(data) {
|
|
96
|
+
if (typeof data === 'string')
|
|
97
|
+
return new TextEncoder().encode(data).byteLength;
|
|
98
|
+
if (data instanceof ArrayBuffer)
|
|
99
|
+
return data.byteLength;
|
|
100
|
+
if (ArrayBuffer.isView(data))
|
|
101
|
+
return data.byteLength;
|
|
102
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob)
|
|
103
|
+
return data.size;
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const DATASET_WRITE_CHUNK_ROWS = 500;
|
|
107
|
+
function encodeDatasetWrite(request) {
|
|
108
|
+
const entries = request.mode === 'replace' ? Object.entries(request.tables) : [[request.tableName, request.rows]];
|
|
109
|
+
const parts = [];
|
|
110
|
+
const hash = createHash('sha256');
|
|
111
|
+
const addChunk = (tableName, rows) => {
|
|
112
|
+
const line = `${JSON.stringify({ tableName, rows })}\n`;
|
|
113
|
+
parts.push(line);
|
|
114
|
+
hash.update(line);
|
|
115
|
+
};
|
|
116
|
+
for (const [tableName, rows] of entries) {
|
|
117
|
+
if (rows.length === 0)
|
|
118
|
+
addChunk(tableName, []);
|
|
119
|
+
for (let offset = 0; offset < rows.length; offset += DATASET_WRITE_CHUNK_ROWS) {
|
|
120
|
+
addChunk(tableName, rows.slice(offset, offset + DATASET_WRITE_CHUNK_ROWS));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const data = new Blob(parts, { type: 'application/x-ndjson' });
|
|
124
|
+
return {
|
|
125
|
+
data,
|
|
126
|
+
contentHash: hash.digest('hex'),
|
|
127
|
+
tables: entries.map(([name, rows]) => ({ name, rowCount: rows.length })),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function pollingDeadline(options) {
|
|
131
|
+
const timeoutMs = options?.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
132
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
133
|
+
throw new PermuteApiError({
|
|
134
|
+
status: 0,
|
|
135
|
+
code: 'invalid_poll_timeout',
|
|
136
|
+
message: 'pollTimeoutMs must be a positive number',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return Date.now() + timeoutMs;
|
|
140
|
+
}
|
|
141
|
+
function waitForPoll(delayMs, deadline, signal) {
|
|
142
|
+
if (signal?.aborted) {
|
|
143
|
+
return Promise.reject(new PermuteApiError({ status: 0, code: 'request_aborted', message: 'Request was aborted' }));
|
|
144
|
+
}
|
|
145
|
+
const remainingMs = deadline - Date.now();
|
|
146
|
+
if (remainingMs <= 0) {
|
|
147
|
+
return Promise.reject(new PermuteApiError({ status: 0, code: 'poll_timeout', message: 'Durable operation timed out' }));
|
|
148
|
+
}
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const onAbort = () => {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
reject(new PermuteApiError({ status: 0, code: 'request_aborted', message: 'Request was aborted' }));
|
|
153
|
+
};
|
|
154
|
+
const timeout = setTimeout(() => {
|
|
155
|
+
signal?.removeEventListener('abort', onAbort);
|
|
156
|
+
if (Date.now() >= deadline) {
|
|
157
|
+
reject(new PermuteApiError({ status: 0, code: 'poll_timeout', message: 'Durable operation timed out' }));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
resolve();
|
|
161
|
+
}
|
|
162
|
+
}, Math.min(delayMs, remainingMs));
|
|
163
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function requireCustomDataset(dataset) {
|
|
167
|
+
if (dataset.type === 'custom')
|
|
168
|
+
return dataset;
|
|
169
|
+
throw new PermuteApiError({
|
|
170
|
+
status: 0,
|
|
171
|
+
code: 'invalid_dataset_type',
|
|
172
|
+
message: `Dataset ${dataset.id} is a file dataset and does not support table writes`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function requireFileDataset(dataset) {
|
|
176
|
+
if (dataset.type === 'file')
|
|
177
|
+
return dataset;
|
|
178
|
+
throw new PermuteApiError({
|
|
179
|
+
status: 0,
|
|
180
|
+
code: 'invalid_dataset_type',
|
|
181
|
+
message: `Dataset ${dataset.id} is a custom dataset and does not expose a file upload`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
export class PermuteDataset {
|
|
185
|
+
writeTables;
|
|
186
|
+
readDataset;
|
|
187
|
+
type = 'custom';
|
|
188
|
+
snapshot;
|
|
189
|
+
constructor(snapshot, writeTables, readDataset) {
|
|
190
|
+
this.writeTables = writeTables;
|
|
191
|
+
this.readDataset = readDataset;
|
|
192
|
+
this.snapshot = snapshot;
|
|
193
|
+
}
|
|
194
|
+
get data() {
|
|
195
|
+
return this.snapshot;
|
|
196
|
+
}
|
|
197
|
+
get id() {
|
|
198
|
+
return this.snapshot.id;
|
|
199
|
+
}
|
|
200
|
+
get sourceId() {
|
|
201
|
+
return this.snapshot.sourceId;
|
|
202
|
+
}
|
|
203
|
+
get key() {
|
|
204
|
+
return this.snapshot.key;
|
|
205
|
+
}
|
|
206
|
+
get name() {
|
|
207
|
+
return this.snapshot.name;
|
|
208
|
+
}
|
|
209
|
+
get context() {
|
|
210
|
+
return this.snapshot.context;
|
|
211
|
+
}
|
|
212
|
+
get status() {
|
|
213
|
+
return this.snapshot.status;
|
|
214
|
+
}
|
|
215
|
+
get version() {
|
|
216
|
+
return this.snapshot.version;
|
|
217
|
+
}
|
|
218
|
+
async replace(tables, options) {
|
|
219
|
+
this.snapshot = await this.writeTables(this.id, { tables, mode: 'replace' }, options);
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
async replaceTable(tableName, rows, options) {
|
|
223
|
+
this.snapshot = await this.writeTables(this.id, { tableName, rows, mode: 'replaceTable' }, options);
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
async appendTable(tableName, rows, options) {
|
|
227
|
+
const { batchId, ...requestOptions } = options;
|
|
228
|
+
this.snapshot = await this.writeTables(this.id, { tableName, rows, batchId, mode: 'appendTable' }, requestOptions);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
async refresh(options) {
|
|
232
|
+
this.snapshot = requireCustomDataset(await this.readDataset(this.id, options));
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export class PermuteFileDataset {
|
|
237
|
+
readDataset;
|
|
238
|
+
type = 'file';
|
|
239
|
+
snapshot;
|
|
240
|
+
constructor(snapshot, readDataset) {
|
|
241
|
+
this.readDataset = readDataset;
|
|
242
|
+
this.snapshot = snapshot;
|
|
243
|
+
}
|
|
244
|
+
get data() {
|
|
245
|
+
return this.snapshot;
|
|
246
|
+
}
|
|
247
|
+
get id() {
|
|
248
|
+
return this.snapshot.id;
|
|
249
|
+
}
|
|
250
|
+
get sourceId() {
|
|
251
|
+
return this.snapshot.sourceId;
|
|
252
|
+
}
|
|
253
|
+
get name() {
|
|
254
|
+
return this.snapshot.name;
|
|
255
|
+
}
|
|
256
|
+
get status() {
|
|
257
|
+
return this.snapshot.status;
|
|
258
|
+
}
|
|
259
|
+
async refresh(options) {
|
|
260
|
+
this.snapshot = requireFileDataset(await this.readDataset(this.id, options));
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export class PermuteClient {
|
|
265
|
+
workspaces;
|
|
266
|
+
sources;
|
|
267
|
+
datasets;
|
|
268
|
+
apiKey;
|
|
269
|
+
baseUrl;
|
|
270
|
+
/** Workspace selected for workspace-scoped API operations. */
|
|
271
|
+
workspaceId;
|
|
272
|
+
defaultHeaders;
|
|
273
|
+
fetchImpl;
|
|
274
|
+
constructor(options) {
|
|
275
|
+
if (!options.apiKey) {
|
|
276
|
+
throw new PermuteApiError({
|
|
277
|
+
status: 0,
|
|
278
|
+
code: 'missing_api_key',
|
|
279
|
+
message: 'apiKey is required',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
283
|
+
if (!fetchImpl) {
|
|
284
|
+
throw new PermuteApiError({
|
|
285
|
+
status: 0,
|
|
286
|
+
code: 'missing_fetch',
|
|
287
|
+
message: 'A fetch implementation is required',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
this.apiKey = options.apiKey;
|
|
291
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
292
|
+
this.workspaceId = options.workspaceId;
|
|
293
|
+
this.defaultHeaders = options.headers ?? {};
|
|
294
|
+
this.fetchImpl = fetchImpl.bind(globalThis);
|
|
295
|
+
this.workspaces = {
|
|
296
|
+
list: (requestOptions) => this.request('workspacesList', {}, requestOptions),
|
|
297
|
+
create: (request, requestOptions) => this.request('workspacesCreate', { body: request }, requestOptions),
|
|
298
|
+
get: (workspaceId, requestOptions) => this.request('workspacesGet', { pathParams: { workspaceId } }, requestOptions),
|
|
299
|
+
update: (workspaceId, request, requestOptions) => this.request('workspacesUpdate', { pathParams: { workspaceId }, body: request }, requestOptions),
|
|
300
|
+
delete: (workspaceId, requestOptions) => this.request('workspacesDelete', { pathParams: { workspaceId } }, requestOptions),
|
|
301
|
+
};
|
|
302
|
+
this.sources = {
|
|
303
|
+
list: (requestOptions) => this.request('sourcesList', {}, requestOptions),
|
|
304
|
+
get: (sourceId, requestOptions) => this.request('sourcesGet', { pathParams: { sourceId } }, requestOptions),
|
|
305
|
+
};
|
|
306
|
+
const readDataset = (datasetId, requestOptions) => this.request('datasetsGet', { pathParams: { datasetId } }, requestOptions);
|
|
307
|
+
const writeDatasetTables = async (datasetId, request, requestOptions) => {
|
|
308
|
+
const encoded = encodeDatasetWrite(request);
|
|
309
|
+
const body = request.mode === 'replace'
|
|
310
|
+
? {
|
|
311
|
+
mode: request.mode,
|
|
312
|
+
tables: encoded.tables,
|
|
313
|
+
sizeBytes: encoded.data.size,
|
|
314
|
+
contentHash: encoded.contentHash,
|
|
315
|
+
}
|
|
316
|
+
: request.mode === 'replaceTable'
|
|
317
|
+
? {
|
|
318
|
+
mode: request.mode,
|
|
319
|
+
tableName: request.tableName,
|
|
320
|
+
rowCount: request.rows.length,
|
|
321
|
+
sizeBytes: encoded.data.size,
|
|
322
|
+
contentHash: encoded.contentHash,
|
|
323
|
+
}
|
|
324
|
+
: {
|
|
325
|
+
mode: request.mode,
|
|
326
|
+
tableName: request.tableName,
|
|
327
|
+
rowCount: request.rows.length,
|
|
328
|
+
sizeBytes: encoded.data.size,
|
|
329
|
+
contentHash: encoded.contentHash,
|
|
330
|
+
batchId: request.batchId,
|
|
331
|
+
};
|
|
332
|
+
const initialized = await this.request('datasetsWriteTables', { pathParams: { datasetId }, body }, requestOptions);
|
|
333
|
+
if (initialized.write.status === 'completed')
|
|
334
|
+
return initialized.dataset;
|
|
335
|
+
if (!initialized.upload) {
|
|
336
|
+
throw new PermuteApiError({ status: 0, code: 'missing_upload', message: 'Dataset write has no upload target' });
|
|
337
|
+
}
|
|
338
|
+
const uploadResponse = await this.fetchImpl(initialized.upload.url, {
|
|
339
|
+
method: initialized.upload.method,
|
|
340
|
+
headers: initialized.upload.headers,
|
|
341
|
+
body: encoded.data,
|
|
342
|
+
signal: requestOptions?.signal,
|
|
343
|
+
});
|
|
344
|
+
if (!uploadResponse.ok) {
|
|
345
|
+
throw new PermuteApiError({
|
|
346
|
+
status: uploadResponse.status,
|
|
347
|
+
code: 'upload_failed',
|
|
348
|
+
message: uploadResponse.statusText || 'Dataset write upload failed',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const deadline = pollingDeadline(requestOptions);
|
|
352
|
+
while (true) {
|
|
353
|
+
await waitForPoll(initialized.pollAfterMs, deadline, requestOptions?.signal);
|
|
354
|
+
const dataset = requireCustomDataset(await readDataset(datasetId, requestOptions));
|
|
355
|
+
if (dataset.write?.id !== initialized.write.id) {
|
|
356
|
+
if (dataset.version > initialized.dataset.version)
|
|
357
|
+
return dataset;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (dataset.write.status === 'completed')
|
|
361
|
+
return dataset;
|
|
362
|
+
if (dataset.write.status === 'failed') {
|
|
363
|
+
throw new PermuteApiError({
|
|
364
|
+
status: 0,
|
|
365
|
+
code: 'dataset_write_failed',
|
|
366
|
+
message: dataset.write.error ?? 'Dataset write failed',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const wrapDataset = (dataset) => dataset.type === 'custom'
|
|
372
|
+
? new PermuteDataset(dataset, writeDatasetTables, readDataset)
|
|
373
|
+
: new PermuteFileDataset(dataset, readDataset);
|
|
374
|
+
const createDataset = async (request, requestOptions) => {
|
|
375
|
+
if (request.type === 'custom') {
|
|
376
|
+
const dataset = await this.request('datasetsCreate', { body: request }, requestOptions);
|
|
377
|
+
return new PermuteDataset(requireCustomDataset(dataset), writeDatasetTables, readDataset);
|
|
378
|
+
}
|
|
379
|
+
const { data, headers, sizeBytes: declaredSize, ...createRequest } = request;
|
|
380
|
+
const inferredSize = inferUploadSize(data);
|
|
381
|
+
if (declaredSize !== undefined && inferredSize !== undefined && declaredSize !== inferredSize) {
|
|
382
|
+
throw new PermuteApiError({
|
|
383
|
+
status: 0,
|
|
384
|
+
code: 'invalid_file_size',
|
|
385
|
+
message: `sizeBytes was ${declaredSize}, but the upload body contains ${inferredSize} bytes`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
const sizeBytes = declaredSize ?? inferredSize;
|
|
389
|
+
if (sizeBytes === undefined) {
|
|
390
|
+
throw new PermuteApiError({
|
|
391
|
+
status: 0,
|
|
392
|
+
code: 'missing_file_size',
|
|
393
|
+
message: 'sizeBytes is required when the SDK cannot determine the upload body size',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const response = await this.request('datasetsCreate', { body: { ...createRequest, sizeBytes } }, requestOptions);
|
|
397
|
+
const dataset = new PermuteFileDataset(requireFileDataset(response), readDataset);
|
|
398
|
+
const target = dataset.data.upload;
|
|
399
|
+
if (!target) {
|
|
400
|
+
throw new PermuteApiError({
|
|
401
|
+
status: 0,
|
|
402
|
+
code: 'missing_upload_target',
|
|
403
|
+
message: `Dataset ${dataset.id} did not include a file upload target`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const uploadResponse = await this.fetchImpl(target.url, {
|
|
407
|
+
method: target.method,
|
|
408
|
+
headers: { ...headers, ...target.headers },
|
|
409
|
+
body: data,
|
|
410
|
+
signal: requestOptions?.signal,
|
|
411
|
+
});
|
|
412
|
+
if (!uploadResponse.ok) {
|
|
413
|
+
throw new PermuteApiError({
|
|
414
|
+
status: uploadResponse.status,
|
|
415
|
+
code: 'upload_failed',
|
|
416
|
+
message: uploadResponse.statusText || `Upload failed with status ${uploadResponse.status}`,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return dataset;
|
|
420
|
+
};
|
|
421
|
+
this.datasets = {
|
|
422
|
+
create: createDataset,
|
|
423
|
+
getByKey: async (key, requestOptions) => {
|
|
424
|
+
const dataset = await this.request('datasetsGetByKey', { query: { key } }, requestOptions);
|
|
425
|
+
return wrapDataset(dataset);
|
|
426
|
+
},
|
|
427
|
+
get: async (datasetId, requestOptions) => {
|
|
428
|
+
const dataset = await readDataset(datasetId, requestOptions);
|
|
429
|
+
return wrapDataset(dataset);
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async search(request, options) {
|
|
434
|
+
return await this.request('searchCreate', { body: request }, options);
|
|
435
|
+
}
|
|
436
|
+
async answer(request, options) {
|
|
437
|
+
let job = await this.answerJobs(request, options);
|
|
438
|
+
const deadline = pollingDeadline(options);
|
|
439
|
+
while (job.status === 'queued' || job.status === 'running') {
|
|
440
|
+
await waitForPoll(job.pollAfterMs ?? 1500, deadline, options?.signal);
|
|
441
|
+
job = await this.getAnswerJob(job.answerJobId, options);
|
|
442
|
+
}
|
|
443
|
+
if (job.status === 'completed') {
|
|
444
|
+
if (job.result)
|
|
445
|
+
return job.result;
|
|
446
|
+
throw new PermuteApiError({
|
|
447
|
+
status: 0,
|
|
448
|
+
code: 'answer_missing_result',
|
|
449
|
+
message: `Answer job ${job.answerJobId} completed without a result`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
throw new PermuteApiError({
|
|
453
|
+
status: 0,
|
|
454
|
+
code: `answer_${job.status}`,
|
|
455
|
+
message: job.error ?? `Answer job ${job.status}`,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async answerJobs(request, options) {
|
|
459
|
+
return await this.request('answerJobsCreate', { body: request }, options);
|
|
460
|
+
}
|
|
461
|
+
async getAnswerJob(answerJobId, options) {
|
|
462
|
+
return await this.request('answerJobsGet', { pathParams: { answerJobId } }, options);
|
|
463
|
+
}
|
|
464
|
+
withWorkspace(workspaceId) {
|
|
465
|
+
if (!workspaceId.trim()) {
|
|
466
|
+
throw new PermuteApiError({ status: 0, code: 'missing_workspace_id', message: 'workspaceId is required' });
|
|
467
|
+
}
|
|
468
|
+
return new PermuteClient({
|
|
469
|
+
apiKey: this.apiKey,
|
|
470
|
+
baseUrl: this.baseUrl,
|
|
471
|
+
workspaceId,
|
|
472
|
+
fetch: this.fetchImpl,
|
|
473
|
+
headers: this.defaultHeaders,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async query(request, options) {
|
|
477
|
+
return await this.request('queriesCreate', { body: request }, options);
|
|
478
|
+
}
|
|
479
|
+
async request(operationId, input, options) {
|
|
480
|
+
const route = routeFor(operationId);
|
|
481
|
+
const body = this.validateRequestBody(route, operationId, input.body);
|
|
482
|
+
const query = this.validateRequestQuery(route, operationId, input.query);
|
|
483
|
+
const headers = {
|
|
484
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
485
|
+
...this.defaultHeaders,
|
|
486
|
+
...options?.headers,
|
|
487
|
+
};
|
|
488
|
+
const workspaceId = options?.workspaceId ?? this.workspaceId;
|
|
489
|
+
if (route.scope === 'workspace' && workspaceId) {
|
|
490
|
+
headers['X-Workspace-Id'] = workspaceId;
|
|
491
|
+
}
|
|
492
|
+
if (body !== undefined) {
|
|
493
|
+
headers['Content-Type'] = 'application/json';
|
|
494
|
+
}
|
|
495
|
+
const url = new URL(`${this.baseUrl.replace(/\/+$/, '')}${buildPath(route, input.pathParams)}`);
|
|
496
|
+
for (const [key, value] of Object.entries(query))
|
|
497
|
+
url.searchParams.set(key, value);
|
|
498
|
+
const response = await this.fetchImpl(url.toString(), {
|
|
499
|
+
method: route.method,
|
|
500
|
+
headers,
|
|
501
|
+
signal: options?.signal,
|
|
502
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
503
|
+
});
|
|
504
|
+
const payload = await parseJsonResponse(response);
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
if (isErrorEnvelope(payload)) {
|
|
507
|
+
throw new PermuteApiError({
|
|
508
|
+
status: response.status,
|
|
509
|
+
code: payload.error.code,
|
|
510
|
+
message: payload.error.message,
|
|
511
|
+
details: payload.error.details,
|
|
512
|
+
requestId: payload.error.requestId,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
throw new PermuteApiError({
|
|
516
|
+
status: response.status,
|
|
517
|
+
code: 'request_failed',
|
|
518
|
+
message: response.statusText || `Request failed with status ${response.status}`,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (!isSuccessEnvelope(payload)) {
|
|
522
|
+
throw new PermuteApiError({
|
|
523
|
+
status: response.status,
|
|
524
|
+
code: 'invalid_response',
|
|
525
|
+
message: 'API response did not include a data envelope',
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const responseValidator = validatorFor(route.responseSchema);
|
|
529
|
+
if (!responseValidator(payload.data)) {
|
|
530
|
+
throw validationError(operationId, 'response', responseValidator.errors);
|
|
531
|
+
}
|
|
532
|
+
return payload.data;
|
|
533
|
+
}
|
|
534
|
+
validateRequestBody(route, operationId, body) {
|
|
535
|
+
if (!route.bodySchema)
|
|
536
|
+
return undefined;
|
|
537
|
+
const requestValidator = validatorFor(route.bodySchema);
|
|
538
|
+
if (!requestValidator(body)) {
|
|
539
|
+
throw validationError(operationId, 'request', requestValidator.errors);
|
|
540
|
+
}
|
|
541
|
+
return body;
|
|
542
|
+
}
|
|
543
|
+
validateRequestQuery(route, operationId, query) {
|
|
544
|
+
if (!route.querySchema)
|
|
545
|
+
return {};
|
|
546
|
+
const requestQuery = query ?? {};
|
|
547
|
+
const queryValidator = validatorFor(route.querySchema);
|
|
548
|
+
if (!queryValidator(requestQuery)) {
|
|
549
|
+
throw validationError(operationId, 'request', queryValidator.errors);
|
|
550
|
+
}
|
|
551
|
+
return requestQuery;
|
|
552
|
+
}
|
|
553
|
+
}
|