roe-typescript 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 +189 -0
- package/dist/api/agents.d.ts +87 -0
- package/dist/api/agents.js +263 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +11 -0
- package/dist/client.d.ts +13 -0
- package/dist/client.js +18 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +43 -0
- package/dist/exceptions.d.ts +25 -0
- package/dist/exceptions.js +73 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/integration_test.js +686 -0
- package/dist/models/agent.d.ts +78 -0
- package/dist/models/agent.js +40 -0
- package/dist/models/file.d.ts +30 -0
- package/dist/models/file.js +85 -0
- package/dist/models/job.d.ts +37 -0
- package/dist/models/job.js +133 -0
- package/dist/models/responses.d.ts +71 -0
- package/dist/models/responses.js +36 -0
- package/dist/models/user.d.ts +6 -0
- package/dist/models/user.js +1 -0
- package/dist/src/api/agents.js +269 -0
- package/dist/src/auth.js +15 -0
- package/dist/src/client.js +22 -0
- package/dist/src/config.js +47 -0
- package/dist/src/exceptions.js +86 -0
- package/dist/src/models/agent.js +45 -0
- package/dist/src/models/file.js +92 -0
- package/dist/src/models/job.js +138 -0
- package/dist/src/models/responses.js +42 -0
- package/dist/src/models/user.js +2 -0
- package/dist/src/utils/fileDetection.js +46 -0
- package/dist/src/utils/httpClient.js +236 -0
- package/dist/src/utils/pagination.js +18 -0
- package/dist/utils/fileDetection.d.ts +11 -0
- package/dist/utils/fileDetection.js +38 -0
- package/dist/utils/httpClient.d.ts +30 -0
- package/dist/utils/httpClient.js +229 -0
- package/dist/utils/pagination.d.ts +3 -0
- package/dist/utils/pagination.js +14 -0
- package/package.json +36 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JobBatch = exports.Job = void 0;
|
|
4
|
+
const responses_1 = require("./responses");
|
|
5
|
+
class Job {
|
|
6
|
+
constructor(opts) {
|
|
7
|
+
this.agentsApi = opts.agentsApi;
|
|
8
|
+
this.jobId = opts.jobId;
|
|
9
|
+
this.timeoutSeconds = opts.timeoutSeconds ?? 7200;
|
|
10
|
+
if (this.timeoutSeconds <= 0) {
|
|
11
|
+
throw new Error(`timeoutSeconds must be positive, got ${this.timeoutSeconds}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
get id() {
|
|
15
|
+
return this.jobId;
|
|
16
|
+
}
|
|
17
|
+
async wait(params) {
|
|
18
|
+
const intervalSeconds = params?.intervalSeconds ?? 5;
|
|
19
|
+
const timeoutSeconds = params?.timeoutSeconds ?? this.timeoutSeconds;
|
|
20
|
+
if (timeoutSeconds <= 0)
|
|
21
|
+
throw new Error(`timeoutSeconds must be positive, got ${timeoutSeconds}`);
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
while (true) {
|
|
24
|
+
const status = await this.retrieveStatus();
|
|
25
|
+
const code = status.status;
|
|
26
|
+
if (code === responses_1.JobStatus.SUCCESS || code === responses_1.JobStatus.CACHED) {
|
|
27
|
+
return this.retrieveResult();
|
|
28
|
+
}
|
|
29
|
+
if (code === responses_1.JobStatus.FAILURE || code === responses_1.JobStatus.CANCELLED) {
|
|
30
|
+
throw new Error(`Job ${this.jobId} failed with status ${code}${status.error_message ? `: ${status.error_message}` : ""}`);
|
|
31
|
+
}
|
|
32
|
+
if (Date.now() - start > timeoutSeconds * 1000) {
|
|
33
|
+
throw new Error(`Job ${this.jobId} did not complete within ${timeoutSeconds} seconds`);
|
|
34
|
+
}
|
|
35
|
+
await new Promise((r) => setTimeout(r, intervalSeconds * 1000));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
retrieveStatus() {
|
|
39
|
+
return this.agentsApi.jobs.retrieveStatus(this.jobId);
|
|
40
|
+
}
|
|
41
|
+
retrieveResult() {
|
|
42
|
+
return this.agentsApi.jobs.retrieveResult(this.jobId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.Job = Job;
|
|
46
|
+
class JobBatch {
|
|
47
|
+
constructor(opts) {
|
|
48
|
+
this.completed = {};
|
|
49
|
+
this.statusCache = {};
|
|
50
|
+
this.agentsApi = opts.agentsApi;
|
|
51
|
+
this.jobIds = opts.jobIds;
|
|
52
|
+
this.timeoutSeconds = opts.timeoutSeconds ?? 7200;
|
|
53
|
+
if (this.timeoutSeconds <= 0) {
|
|
54
|
+
throw new Error(`timeoutSeconds must be positive, got ${this.timeoutSeconds}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
get jobs() {
|
|
58
|
+
return this.jobIds.map((id) => new Job({ agentsApi: this.agentsApi, jobId: id, timeoutSeconds: this.timeoutSeconds }));
|
|
59
|
+
}
|
|
60
|
+
async wait(params) {
|
|
61
|
+
const intervalSeconds = params?.intervalSeconds ?? 5;
|
|
62
|
+
const timeoutSeconds = params?.timeoutSeconds ?? this.timeoutSeconds;
|
|
63
|
+
if (timeoutSeconds <= 0)
|
|
64
|
+
throw new Error(`timeoutSeconds must be positive, got ${timeoutSeconds}`);
|
|
65
|
+
const start = Date.now();
|
|
66
|
+
while (Object.keys(this.completed).length < this.jobIds.length) {
|
|
67
|
+
const pending = this.jobIds.filter((id) => !this.completed[id]);
|
|
68
|
+
if (!pending.length)
|
|
69
|
+
break;
|
|
70
|
+
const statusBatch = await this.agentsApi.jobs.retrieveStatusMany(pending);
|
|
71
|
+
const failures = statusBatch.filter((s) => s.status === responses_1.JobStatus.FAILURE || s.status === responses_1.JobStatus.CANCELLED);
|
|
72
|
+
if (failures.length) {
|
|
73
|
+
const detail = failures.map((f) => `${f.id}:${f.status ?? "unknown"}`).join(', ');
|
|
74
|
+
throw new Error(`Jobs failed or cancelled: ${detail}`);
|
|
75
|
+
}
|
|
76
|
+
const completedIds = [];
|
|
77
|
+
for (const status of statusBatch) {
|
|
78
|
+
const code = status.status;
|
|
79
|
+
if (code === responses_1.JobStatus.SUCCESS || code === responses_1.JobStatus.CACHED) {
|
|
80
|
+
completedIds.push(status.id);
|
|
81
|
+
}
|
|
82
|
+
if (code !== undefined && code !== null) {
|
|
83
|
+
this.statusCache[status.id] = code;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (completedIds.length) {
|
|
87
|
+
const resultBatch = await this.agentsApi.jobs.retrieveResultMany(completedIds);
|
|
88
|
+
for (const res of resultBatch) {
|
|
89
|
+
if (!res.agent_id || !res.agent_version_id) {
|
|
90
|
+
const id = res.id ?? 'unknown';
|
|
91
|
+
throw new Error(`Job ${id} missing agent identifiers`);
|
|
92
|
+
}
|
|
93
|
+
// Use corrected_outputs as fallback if result is null/undefined
|
|
94
|
+
const outputs = res.result ?? res.corrected_outputs;
|
|
95
|
+
if (outputs == null) {
|
|
96
|
+
// Both result and corrected_outputs are null - this may indicate an error or incomplete job
|
|
97
|
+
throw new Error(`Job ${res.id} returned null or undefined result`);
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(outputs)) {
|
|
100
|
+
// Result exists but is not an array - unexpected format
|
|
101
|
+
throw new Error(`Job ${res.id} returned unexpected result format: ${typeof outputs}`);
|
|
102
|
+
}
|
|
103
|
+
this.completed[res.id] = {
|
|
104
|
+
agent_id: res.agent_id,
|
|
105
|
+
agent_version_id: res.agent_version_id,
|
|
106
|
+
inputs: res.inputs ?? [],
|
|
107
|
+
input_tokens: res.input_tokens,
|
|
108
|
+
output_tokens: res.output_tokens,
|
|
109
|
+
outputs,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(this.completed).length < this.jobIds.length) {
|
|
114
|
+
if (Date.now() - start > timeoutSeconds * 1000) {
|
|
115
|
+
const remaining = this.jobIds.filter((id) => !this.completed[id]);
|
|
116
|
+
throw new Error(`Jobs ${remaining.join(", ")} did not complete within ${timeoutSeconds} seconds`);
|
|
117
|
+
}
|
|
118
|
+
await new Promise((r) => setTimeout(r, intervalSeconds * 1000));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return this.jobIds.map((id) => this.completed[id]);
|
|
122
|
+
}
|
|
123
|
+
async retrieveStatus() {
|
|
124
|
+
const statusMap = { ...this.statusCache };
|
|
125
|
+
const toQuery = this.jobIds.filter((id) => statusMap[id] === undefined);
|
|
126
|
+
if (toQuery.length) {
|
|
127
|
+
const statuses = await this.agentsApi.jobs.retrieveStatusMany(toQuery);
|
|
128
|
+
for (const s of statuses) {
|
|
129
|
+
if (s.status !== undefined && s.status !== null) {
|
|
130
|
+
statusMap[s.id] = s.status;
|
|
131
|
+
this.statusCache[s.id] = s.status;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return statusMap;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.JobBatch = JobBatch;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JobStatus = void 0;
|
|
4
|
+
exports.referenceFromUrl = referenceFromUrl;
|
|
5
|
+
exports.extractReferencesFromOutputs = extractReferencesFromOutputs;
|
|
6
|
+
exports.getJobReferences = getJobReferences;
|
|
7
|
+
const uuid_1 = require("uuid");
|
|
8
|
+
var JobStatus;
|
|
9
|
+
(function (JobStatus) {
|
|
10
|
+
JobStatus[JobStatus["PENDING"] = 0] = "PENDING";
|
|
11
|
+
JobStatus[JobStatus["STARTED"] = 1] = "STARTED";
|
|
12
|
+
JobStatus[JobStatus["RETRY"] = 2] = "RETRY";
|
|
13
|
+
JobStatus[JobStatus["SUCCESS"] = 3] = "SUCCESS";
|
|
14
|
+
JobStatus[JobStatus["FAILURE"] = 4] = "FAILURE";
|
|
15
|
+
JobStatus[JobStatus["CANCELLED"] = 5] = "CANCELLED";
|
|
16
|
+
JobStatus[JobStatus["CACHED"] = 6] = "CACHED";
|
|
17
|
+
})(JobStatus || (exports.JobStatus = JobStatus = {}));
|
|
18
|
+
function referenceFromUrl(url) {
|
|
19
|
+
const resource_id = url.split("/references/").at(-1)?.replace(/\/+$/, "") ?? (0, uuid_1.v4)();
|
|
20
|
+
return { url, resource_id };
|
|
21
|
+
}
|
|
22
|
+
function extractReferencesFromOutputs(outputs) {
|
|
23
|
+
const references = [];
|
|
24
|
+
for (const output of outputs) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(output.value);
|
|
27
|
+
const refs = Array.isArray(parsed?.references) ? parsed.references : [];
|
|
28
|
+
for (const ref of refs) {
|
|
29
|
+
if (typeof ref === "string" && ref.includes("/references/")) {
|
|
30
|
+
references.push(referenceFromUrl(ref));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignore unparsable outputs
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return references;
|
|
39
|
+
}
|
|
40
|
+
function getJobReferences(result) {
|
|
41
|
+
return extractReferencesFromOutputs(result.outputs ?? []);
|
|
42
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isUuidString = isUuidString;
|
|
7
|
+
exports.isFilePath = isFilePath;
|
|
8
|
+
exports.isFilePathSync = isFilePathSync;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const uuidRegex = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
|
|
11
|
+
function isUuidString(value) {
|
|
12
|
+
return typeof value === "string" && uuidRegex.test(value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Asynchronously checks if a value is a valid file path.
|
|
16
|
+
* Uses async fs operations to avoid blocking the event loop.
|
|
17
|
+
*/
|
|
18
|
+
async function isFilePath(value) {
|
|
19
|
+
if (typeof value !== "string")
|
|
20
|
+
return false;
|
|
21
|
+
if (isUuidString(value))
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
const stat = await fs_1.default.promises.stat(value);
|
|
25
|
+
return stat.isFile();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Synchronous version of isFilePath for backward compatibility.
|
|
33
|
+
* @deprecated Use isFilePath (async) instead to avoid blocking the event loop.
|
|
34
|
+
*/
|
|
35
|
+
function isFilePathSync(value) {
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
return false;
|
|
38
|
+
if (isUuidString(value))
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
return fs_1.default.existsSync(value) && fs_1.default.statSync(value).isFile();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RoeHTTPClient = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const http_1 = __importDefault(require("http"));
|
|
11
|
+
const https_1 = __importDefault(require("https"));
|
|
12
|
+
const file_1 = require("../models/file");
|
|
13
|
+
const exceptions_1 = require("../exceptions");
|
|
14
|
+
const fileDetection_1 = require("./fileDetection");
|
|
15
|
+
// Keep-alive agents for connection reuse (improves performance under load)
|
|
16
|
+
const httpAgent = new http_1.default.Agent({ keepAlive: true });
|
|
17
|
+
const httpsAgent = new https_1.default.Agent({ keepAlive: true });
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
class RoeHTTPClient {
|
|
22
|
+
constructor(config, auth) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.auth = auth;
|
|
25
|
+
this.maxRetries = config.maxRetries;
|
|
26
|
+
this.client = axios_1.default.create({
|
|
27
|
+
baseURL: config.baseUrl,
|
|
28
|
+
timeout: config.timeoutMs,
|
|
29
|
+
headers: auth.getHeaders(),
|
|
30
|
+
httpAgent,
|
|
31
|
+
httpsAgent,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
close() {
|
|
35
|
+
// no-op; included for parity
|
|
36
|
+
}
|
|
37
|
+
async execute(fn) {
|
|
38
|
+
let attempt = 0;
|
|
39
|
+
let lastError;
|
|
40
|
+
while (attempt <= this.maxRetries) {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fn();
|
|
43
|
+
return this.handleResponse(res);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
lastError = err;
|
|
47
|
+
const status = axios_1.default.isAxiosError(err) ? err.response?.status : undefined;
|
|
48
|
+
const isNetworkError = axios_1.default.isAxiosError(err) && !err.response;
|
|
49
|
+
const retriableStatus = status !== undefined &&
|
|
50
|
+
(status >= 500 || status === 429 || status === 408);
|
|
51
|
+
const shouldRetry = (isNetworkError || retriableStatus) && attempt < this.maxRetries;
|
|
52
|
+
if (!shouldRetry) {
|
|
53
|
+
if (axios_1.default.isAxiosError(err) && err.response) {
|
|
54
|
+
const ExceptionClass = (0, exceptions_1.getExceptionForStatusCode)(err.response.status);
|
|
55
|
+
const data = err.response.data;
|
|
56
|
+
// Use improved error message extraction that handles multiple formats
|
|
57
|
+
const message = (0, exceptions_1.extractErrorMessage)(data, err.response.status);
|
|
58
|
+
throw new ExceptionClass(message, err.response.status, data ?? null);
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
const backoffMs = Math.min(1000 * 2 ** attempt, 10000);
|
|
63
|
+
await sleep(backoffMs);
|
|
64
|
+
}
|
|
65
|
+
attempt += 1;
|
|
66
|
+
}
|
|
67
|
+
if (lastError)
|
|
68
|
+
throw lastError;
|
|
69
|
+
throw new Error("Request failed");
|
|
70
|
+
}
|
|
71
|
+
handleResponse(response) {
|
|
72
|
+
if (response.status >= 200 && response.status < 300) {
|
|
73
|
+
return response.data;
|
|
74
|
+
}
|
|
75
|
+
const ExceptionClass = (0, exceptions_1.getExceptionForStatusCode)(response.status);
|
|
76
|
+
const data = response.data;
|
|
77
|
+
// Use improved error message extraction that handles multiple formats
|
|
78
|
+
const message = (0, exceptions_1.extractErrorMessage)(data, response.status);
|
|
79
|
+
throw new ExceptionClass(message, response.status, data ?? null);
|
|
80
|
+
}
|
|
81
|
+
get(url, params) {
|
|
82
|
+
return this.execute(() => this.client.get(url, { params }));
|
|
83
|
+
}
|
|
84
|
+
async getBytes(url, params) {
|
|
85
|
+
const data = await this.execute(() => this.client.get(url, { params, responseType: "arraybuffer" }));
|
|
86
|
+
// Normalize ArrayBuffer/Buffer to Buffer (axios returns ArrayBuffer in browsers)
|
|
87
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
88
|
+
}
|
|
89
|
+
async post(options) {
|
|
90
|
+
const { url, json, formData, files, params } = options;
|
|
91
|
+
const config = { params };
|
|
92
|
+
if (json !== undefined) {
|
|
93
|
+
return this.execute(() => this.client.post(url, json, config));
|
|
94
|
+
}
|
|
95
|
+
// Check if we have files that need retry-safe handling
|
|
96
|
+
const hasFiles = files && Object.keys(files).length > 0;
|
|
97
|
+
if (!hasFiles) {
|
|
98
|
+
// No files - build FormData once (safe for retries)
|
|
99
|
+
const fd = new form_data_1.default();
|
|
100
|
+
if (formData) {
|
|
101
|
+
for (const [k, v] of Object.entries(formData)) {
|
|
102
|
+
if (v === undefined || v === null)
|
|
103
|
+
continue;
|
|
104
|
+
fd.append(k, String(v));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
config.headers = { ...(config.headers || {}), ...fd.getHeaders() };
|
|
108
|
+
return this.execute(() => this.client.post(url, fd, config));
|
|
109
|
+
}
|
|
110
|
+
// With files - need to rebuild FormData on each retry attempt
|
|
111
|
+
// because file streams can only be read once
|
|
112
|
+
return this.executeWithFormData(url, formData ?? {}, files, config);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Execute a POST request with FormData that contains files.
|
|
116
|
+
* Rebuilds FormData on each retry attempt to handle stream consumption.
|
|
117
|
+
*/
|
|
118
|
+
async executeWithFormData(url, formData, files, config) {
|
|
119
|
+
// Pre-resolve file paths to avoid async in retry loop
|
|
120
|
+
const resolvedFiles = [];
|
|
121
|
+
for (const [k, v] of Object.entries(files)) {
|
|
122
|
+
if (v instanceof file_1.FileUpload) {
|
|
123
|
+
resolvedFiles.push({ key: k, type: "fileUpload", value: v });
|
|
124
|
+
}
|
|
125
|
+
else if (typeof v === "string" && await (0, fileDetection_1.isFilePath)(v)) {
|
|
126
|
+
resolvedFiles.push({ key: k, type: "path", value: v });
|
|
127
|
+
}
|
|
128
|
+
else if (typeof v === "string") {
|
|
129
|
+
resolvedFiles.push({ key: k, type: "string", value: v });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Raw streams cannot be safely retried - warn but allow first attempt
|
|
133
|
+
resolvedFiles.push({ key: k, type: "stream", value: v });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let attempt = 0;
|
|
137
|
+
let lastError;
|
|
138
|
+
while (attempt <= this.maxRetries) {
|
|
139
|
+
try {
|
|
140
|
+
// Build fresh FormData for each attempt
|
|
141
|
+
const fd = new form_data_1.default();
|
|
142
|
+
for (const [k, v] of Object.entries(formData)) {
|
|
143
|
+
if (v === undefined || v === null)
|
|
144
|
+
continue;
|
|
145
|
+
fd.append(k, String(v));
|
|
146
|
+
}
|
|
147
|
+
for (const file of resolvedFiles) {
|
|
148
|
+
if (file.type === "fileUpload") {
|
|
149
|
+
const fu = file.value;
|
|
150
|
+
fd.append(file.key, fu.open(), {
|
|
151
|
+
filename: fu.effectiveFilename,
|
|
152
|
+
contentType: fu.effectiveMimeType,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (file.type === "path") {
|
|
156
|
+
fd.append(file.key, fs_1.default.createReadStream(file.value));
|
|
157
|
+
}
|
|
158
|
+
else if (file.type === "string") {
|
|
159
|
+
fd.append(file.key, file.value);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Stream - can only be used on first attempt
|
|
163
|
+
if (attempt > 0) {
|
|
164
|
+
throw new Error(`Cannot retry request with consumed stream for field "${file.key}". ` +
|
|
165
|
+
`Use FileUpload or file path instead for retry-safe uploads.`);
|
|
166
|
+
}
|
|
167
|
+
fd.append(file.key, file.value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const reqConfig = { ...config, headers: { ...(config.headers || {}), ...fd.getHeaders() } };
|
|
171
|
+
const res = await this.client.post(url, fd, reqConfig);
|
|
172
|
+
return this.handleResponse(res);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
lastError = err;
|
|
176
|
+
const status = axios_1.default.isAxiosError(err) ? err.response?.status : undefined;
|
|
177
|
+
const isNetworkError = axios_1.default.isAxiosError(err) && !err.response;
|
|
178
|
+
const retriableStatus = status !== undefined &&
|
|
179
|
+
(status >= 500 || status === 429 || status === 408);
|
|
180
|
+
const shouldRetry = (isNetworkError || retriableStatus) && attempt < this.maxRetries;
|
|
181
|
+
if (!shouldRetry) {
|
|
182
|
+
if (axios_1.default.isAxiosError(err) && err.response) {
|
|
183
|
+
const ExceptionClass = (0, exceptions_1.getExceptionForStatusCode)(err.response.status);
|
|
184
|
+
const data = err.response.data;
|
|
185
|
+
const message = (0, exceptions_1.extractErrorMessage)(data, err.response.status);
|
|
186
|
+
throw new ExceptionClass(message, err.response.status, data ?? null);
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
const backoffMs = Math.min(1000 * 2 ** attempt, 10000);
|
|
191
|
+
await sleep(backoffMs);
|
|
192
|
+
}
|
|
193
|
+
attempt += 1;
|
|
194
|
+
}
|
|
195
|
+
if (lastError)
|
|
196
|
+
throw lastError;
|
|
197
|
+
throw new Error("Request failed");
|
|
198
|
+
}
|
|
199
|
+
put(url, json, params) {
|
|
200
|
+
return this.execute(() => this.client.put(url, json ?? {}, { params }));
|
|
201
|
+
}
|
|
202
|
+
delete(url, params) {
|
|
203
|
+
return this.execute(() => this.client.delete(url, { params }));
|
|
204
|
+
}
|
|
205
|
+
async postWithDynamicInputs(url, inputs, params) {
|
|
206
|
+
const formData = {};
|
|
207
|
+
const files = {};
|
|
208
|
+
// Process inputs and detect file paths asynchronously
|
|
209
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
210
|
+
if (value instanceof file_1.FileUpload) {
|
|
211
|
+
files[key] = value;
|
|
212
|
+
}
|
|
213
|
+
else if (value && typeof value === "object" && typeof value.pipe === "function") {
|
|
214
|
+
// More robust stream detection: check if it's an object with a pipe function
|
|
215
|
+
files[key] = value;
|
|
216
|
+
}
|
|
217
|
+
else if (typeof value === "string") {
|
|
218
|
+
if ((0, fileDetection_1.isUuidString)(value)) {
|
|
219
|
+
formData[key] = value;
|
|
220
|
+
}
|
|
221
|
+
else if (await (0, fileDetection_1.isFilePath)(value)) {
|
|
222
|
+
// Use async file detection to avoid blocking the event loop
|
|
223
|
+
files[key] = value;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
formData[key] = value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (value !== undefined && value !== null) {
|
|
230
|
+
formData[key] = value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return this.post({ url, formData, files, params });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
exports.RoeHTTPClient = RoeHTTPClient;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PaginationHelper = void 0;
|
|
4
|
+
class PaginationHelper {
|
|
5
|
+
static buildQueryParams(organizationId, page, pageSize, extra = {}) {
|
|
6
|
+
const params = { organization_id: organizationId };
|
|
7
|
+
if (page !== undefined)
|
|
8
|
+
params.page = String(page);
|
|
9
|
+
if (pageSize !== undefined)
|
|
10
|
+
params.page_size = String(pageSize);
|
|
11
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
12
|
+
if (v !== undefined && v !== null)
|
|
13
|
+
params[k] = String(v);
|
|
14
|
+
}
|
|
15
|
+
return params;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.PaginationHelper = PaginationHelper;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function isUuidString(value: unknown): value is string;
|
|
2
|
+
/**
|
|
3
|
+
* Asynchronously checks if a value is a valid file path.
|
|
4
|
+
* Uses async fs operations to avoid blocking the event loop.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isFilePath(value: unknown): Promise<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Synchronous version of isFilePath for backward compatibility.
|
|
9
|
+
* @deprecated Use isFilePath (async) instead to avoid blocking the event loop.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isFilePathSync(value: unknown): value is string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
const uuidRegex = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
|
|
3
|
+
export function isUuidString(value) {
|
|
4
|
+
return typeof value === "string" && uuidRegex.test(value);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Asynchronously checks if a value is a valid file path.
|
|
8
|
+
* Uses async fs operations to avoid blocking the event loop.
|
|
9
|
+
*/
|
|
10
|
+
export async function isFilePath(value) {
|
|
11
|
+
if (typeof value !== "string")
|
|
12
|
+
return false;
|
|
13
|
+
if (isUuidString(value))
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
const stat = await fs.promises.stat(value);
|
|
17
|
+
return stat.isFile();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Synchronous version of isFilePath for backward compatibility.
|
|
25
|
+
* @deprecated Use isFilePath (async) instead to avoid blocking the event loop.
|
|
26
|
+
*/
|
|
27
|
+
export function isFilePathSync(value) {
|
|
28
|
+
if (typeof value !== "string")
|
|
29
|
+
return false;
|
|
30
|
+
if (isUuidString(value))
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
return fs.existsSync(value) && fs.statSync(value).isFile();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FileUpload } from "../models/file";
|
|
2
|
+
import { RoeAuth } from "../auth";
|
|
3
|
+
import { RoeConfig } from "../config";
|
|
4
|
+
export declare class RoeHTTPClient {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly auth;
|
|
7
|
+
private readonly client;
|
|
8
|
+
private readonly maxRetries;
|
|
9
|
+
constructor(config: RoeConfig, auth: RoeAuth);
|
|
10
|
+
close(): void;
|
|
11
|
+
private execute;
|
|
12
|
+
private handleResponse;
|
|
13
|
+
get<T>(url: string, params?: Record<string, unknown>): Promise<T>;
|
|
14
|
+
getBytes(url: string, params?: Record<string, unknown>): Promise<Buffer>;
|
|
15
|
+
post<T>(options: {
|
|
16
|
+
url: string;
|
|
17
|
+
json?: Record<string, unknown> | unknown[];
|
|
18
|
+
formData?: Record<string, unknown>;
|
|
19
|
+
files?: Record<string, FileUpload | NodeJS.ReadableStream | string>;
|
|
20
|
+
params?: Record<string, unknown>;
|
|
21
|
+
}): Promise<T>;
|
|
22
|
+
/**
|
|
23
|
+
* Execute a POST request with FormData that contains files.
|
|
24
|
+
* Rebuilds FormData on each retry attempt to handle stream consumption.
|
|
25
|
+
*/
|
|
26
|
+
private executeWithFormData;
|
|
27
|
+
put<T>(url: string, json?: Record<string, unknown>, params?: Record<string, unknown>): Promise<T>;
|
|
28
|
+
delete(url: string, params?: Record<string, unknown>): Promise<void>;
|
|
29
|
+
postWithDynamicInputs<T>(url: string, inputs: Record<string, unknown>, params?: Record<string, unknown>): Promise<T>;
|
|
30
|
+
}
|