retoolrpc 0.1.1 → 0.1.3
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 +2 -0
- package/dist/cjs/example.js +2 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/rpc-17203d8f.js +533 -0
- package/dist/cjs/rpc-2c4867ea.js +533 -0
- package/dist/cjs/rpc-d4079433.js +540 -0
- package/dist/example.js +2 -1
- package/dist/example.mjs +119 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +129 -0
- package/dist/package.json +29 -29
- package/dist/rpc-2136d01d.js +526 -0
- package/dist/rpc-4cd0ba05.js +526 -0
- package/dist/rpc-c0b37172.mjs +533 -0
- package/dist/src/rpc.d.ts +2 -9
- package/dist/src/types.d.ts +2 -0
- package/dist/src/utils/api.d.ts +3 -1
- package/dist/src/version.d.ts +1 -1
- package/example.ts +1 -0
- package/package.json +29 -29
- package/rollup.config.js +2 -0
- package/src/rpc.spec.ts +86 -77
- package/src/rpc.ts +49 -47
- package/src/types.ts +3 -1
- package/src/utils/api.ts +5 -5
- package/src/version.ts +1 -1
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { v4 } from 'uuid';
|
|
2
|
+
import { dedent } from 'ts-dedent';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
import AbortControllerFallback from 'abort-controller';
|
|
5
|
+
|
|
6
|
+
const AGENT_SERVER_ERROR = 'AgentServerError';
|
|
7
|
+
function createAgentServerError(error) {
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
const agentError = {
|
|
10
|
+
name: error.name,
|
|
11
|
+
message: error.message,
|
|
12
|
+
stack: error.stack,
|
|
13
|
+
};
|
|
14
|
+
if ('code' in error) {
|
|
15
|
+
if (typeof error.code === 'number') {
|
|
16
|
+
agentError.code = error.code;
|
|
17
|
+
}
|
|
18
|
+
else if (error.code === 'string') {
|
|
19
|
+
agentError.code = parseInt(error.code, 10);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if ('details' in error) {
|
|
23
|
+
agentError.details = error.details;
|
|
24
|
+
}
|
|
25
|
+
return agentError;
|
|
26
|
+
}
|
|
27
|
+
if (typeof error === 'string') {
|
|
28
|
+
return {
|
|
29
|
+
name: AGENT_SERVER_ERROR,
|
|
30
|
+
message: error,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
name: AGENT_SERVER_ERROR,
|
|
35
|
+
message: 'Unknown agent server error',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
class FunctionNotFoundError extends Error {
|
|
39
|
+
constructor(functionName) {
|
|
40
|
+
super(`Function "${functionName}" not found on remote agent server.`);
|
|
41
|
+
this.name = 'FunctionNotFoundError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
class InvalidArgumentsError extends Error {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = 'InvalidArgumentsError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pick(obj, keys) {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
if (key in obj) {
|
|
55
|
+
result[key] = obj[key];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function isRecord(value) {
|
|
61
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
function isFalsyArgumentValue(value) {
|
|
64
|
+
return value === null || value === undefined || value === '';
|
|
65
|
+
}
|
|
66
|
+
function isBooleanString(value) {
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
const lowercaseValue = value.toLowerCase();
|
|
69
|
+
return lowercaseValue === 'true' || lowercaseValue === 'false';
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
function isNumberString(value) {
|
|
74
|
+
if (typeof value === 'string') {
|
|
75
|
+
// Use a regular expression to check if the string is a valid number
|
|
76
|
+
return /^-?\d+(\.\d+)?$/.test(value);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function isClientError(status) {
|
|
81
|
+
return status >= 400 && status < 500;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class ArgumentParser {
|
|
85
|
+
constructor(schema) {
|
|
86
|
+
this.schema = schema;
|
|
87
|
+
}
|
|
88
|
+
parse(argumentsToParse) {
|
|
89
|
+
const parsedArguments = { ...argumentsToParse };
|
|
90
|
+
const parsedErrors = [];
|
|
91
|
+
for (const argName in this.schema) {
|
|
92
|
+
const argDefinition = this.schema[argName];
|
|
93
|
+
const argValue = argumentsToParse[argName];
|
|
94
|
+
const falsyArgValue = isFalsyArgumentValue(argValue);
|
|
95
|
+
if (falsyArgValue) {
|
|
96
|
+
if (argDefinition.required) {
|
|
97
|
+
parsedErrors.push(`Argument "${argName}" is required but missing.`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!falsyArgValue) {
|
|
102
|
+
if (argDefinition.array) {
|
|
103
|
+
if (!Array.isArray(argValue)) {
|
|
104
|
+
parsedErrors.push(`Argument "${argName}" should be an array.`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const parseValueTypeItems = argValue.map((item) => this.parseValueType(item, argDefinition.type));
|
|
108
|
+
if (!parseValueTypeItems.every((item) => item.isValidType)) {
|
|
109
|
+
parsedErrors.push(`Argument "${argName}" should be an array of type "${argDefinition.type}".`);
|
|
110
|
+
}
|
|
111
|
+
parsedArguments[argName] = parseValueTypeItems.map((item) => item.parsedValue);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const parsedValueTypeItem = this.parseValueType(argValue, argDefinition.type);
|
|
115
|
+
if (!parsedValueTypeItem.isValidType) {
|
|
116
|
+
parsedErrors.push(`Argument "${argName}" should be of type "${argDefinition.type}".`);
|
|
117
|
+
}
|
|
118
|
+
parsedArguments[argName] = parsedValueTypeItem.parsedValue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { parsedErrors, parsedArguments };
|
|
123
|
+
}
|
|
124
|
+
parseValueType(value, expectedType) {
|
|
125
|
+
switch (expectedType) {
|
|
126
|
+
case 'string':
|
|
127
|
+
// For string type, we just need to convert to string.
|
|
128
|
+
return {
|
|
129
|
+
isValidType: true,
|
|
130
|
+
parsedValue: typeof value === 'object' ? JSON.stringify(value) : String(value), // Need to do this because String(object) returns "[object Object]".
|
|
131
|
+
};
|
|
132
|
+
case 'boolean':
|
|
133
|
+
// For boolean type, we need to check if the value is a boolean or a boolean string.
|
|
134
|
+
if (typeof value === 'boolean') {
|
|
135
|
+
return {
|
|
136
|
+
isValidType: true,
|
|
137
|
+
parsedValue: value,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (isBooleanString(value)) {
|
|
141
|
+
return {
|
|
142
|
+
isValidType: true,
|
|
143
|
+
parsedValue: value.toLowerCase() === 'true',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
isValidType: false,
|
|
148
|
+
parsedValue: value,
|
|
149
|
+
};
|
|
150
|
+
case 'number':
|
|
151
|
+
// For number type, we need to check if the value is a number or a number string.
|
|
152
|
+
if (typeof value === 'number') {
|
|
153
|
+
return {
|
|
154
|
+
isValidType: true,
|
|
155
|
+
parsedValue: value,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (isNumberString(value)) {
|
|
159
|
+
return {
|
|
160
|
+
isValidType: true,
|
|
161
|
+
parsedValue: parseFloat(value),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
isValidType: false,
|
|
166
|
+
parsedValue: value,
|
|
167
|
+
};
|
|
168
|
+
case 'dict':
|
|
169
|
+
// For dict type, we need to check if the value is a record.
|
|
170
|
+
if (isRecord(value)) {
|
|
171
|
+
return {
|
|
172
|
+
isValidType: true,
|
|
173
|
+
parsedValue: value,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
isValidType: false,
|
|
178
|
+
parsedValue: value,
|
|
179
|
+
};
|
|
180
|
+
case 'json':
|
|
181
|
+
// For json type, we need to check if the value is a valid JSON string.
|
|
182
|
+
try {
|
|
183
|
+
const parsedJSONValue = JSON.parse(JSON.stringify(value));
|
|
184
|
+
return {
|
|
185
|
+
isValidType: true,
|
|
186
|
+
parsedValue: parsedJSONValue,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return {
|
|
191
|
+
isValidType: false,
|
|
192
|
+
parsedValue: value,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
throw new Error(`Unknown argument type "${expectedType}".`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function parseFunctionArguments(args, schema) {
|
|
201
|
+
if (!isRecord(args)) {
|
|
202
|
+
throw new Error(`The given arguments are invalid.`);
|
|
203
|
+
}
|
|
204
|
+
const argumentParser = new ArgumentParser(schema);
|
|
205
|
+
const { parsedArguments, parsedErrors } = argumentParser.parse(args);
|
|
206
|
+
if (parsedErrors.length > 0) {
|
|
207
|
+
const invalidArgumentsError = dedent `
|
|
208
|
+
Invalid parameter(s) found:
|
|
209
|
+
${parsedErrors.join('\n')}
|
|
210
|
+
`;
|
|
211
|
+
throw new InvalidArgumentsError(invalidArgumentsError);
|
|
212
|
+
}
|
|
213
|
+
return pick(parsedArguments, Object.keys(schema));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const CONNECTION_ERROR_INITIAL_TIMEOUT_MS = 50;
|
|
217
|
+
const CONNECTION_ERROR_RETRY_MAX_MS = 1000 * 60 * 10; // 10 minutes
|
|
218
|
+
function sleep(ms) {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
setTimeout(resolve, ms);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async function loopWithBackoff(pollingIntervalMs, logger, callback) {
|
|
224
|
+
let delayTimeMs = CONNECTION_ERROR_INITIAL_TIMEOUT_MS;
|
|
225
|
+
let lastLoopTimestamp = Date.now();
|
|
226
|
+
while (true) {
|
|
227
|
+
try {
|
|
228
|
+
const result = await callback();
|
|
229
|
+
const currentTimestamp = Date.now();
|
|
230
|
+
const loopDurationMs = currentTimestamp - lastLoopTimestamp;
|
|
231
|
+
lastLoopTimestamp = currentTimestamp;
|
|
232
|
+
logger.debug(`Loop time: ${loopDurationMs}ms, delay time: ${delayTimeMs}ms, polling interval: ${pollingIntervalMs}ms`);
|
|
233
|
+
if (result !== 'continue') {
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
await sleep(pollingIntervalMs);
|
|
237
|
+
delayTimeMs = Math.max(delayTimeMs / 2, CONNECTION_ERROR_INITIAL_TIMEOUT_MS);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
logger.error('Error running RPC agent', err);
|
|
241
|
+
await sleep(delayTimeMs);
|
|
242
|
+
delayTimeMs = Math.min(delayTimeMs * 2, CONNECTION_ERROR_RETRY_MAX_MS);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const LOG_LEVEL_RANKINGS = {
|
|
248
|
+
debug: 0,
|
|
249
|
+
info: 1,
|
|
250
|
+
warn: 2,
|
|
251
|
+
error: 3,
|
|
252
|
+
};
|
|
253
|
+
class Logger {
|
|
254
|
+
constructor(options) {
|
|
255
|
+
this.currentLogLevel = options.logLevel || 'info'; // Default to 'info' if logLevel is not specified
|
|
256
|
+
}
|
|
257
|
+
shouldLog(level) {
|
|
258
|
+
return LOG_LEVEL_RANKINGS[level] >= LOG_LEVEL_RANKINGS[this.currentLogLevel] && process.env.NODE_ENV !== 'test';
|
|
259
|
+
}
|
|
260
|
+
debug(...messages) {
|
|
261
|
+
if (this.shouldLog('debug')) {
|
|
262
|
+
console.log(...messages);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
info(...messages) {
|
|
266
|
+
if (this.shouldLog('info')) {
|
|
267
|
+
console.log(...messages);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
warn(...messages) {
|
|
271
|
+
if (this.shouldLog('warn')) {
|
|
272
|
+
console.log(...messages);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
error(...messages) {
|
|
276
|
+
if (this.shouldLog('error')) {
|
|
277
|
+
console.log(...messages);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const RetoolRPCVersion = '0.1.3';
|
|
283
|
+
|
|
284
|
+
// AbortController was added in node v14.17.0 globally, but we need to polyfill it for older versions
|
|
285
|
+
const AbortController = globalThis.AbortController || AbortControllerFallback;
|
|
286
|
+
class RetoolAPI {
|
|
287
|
+
constructor({ hostUrl, apiKey, pollingTimeoutMs }) {
|
|
288
|
+
this._hostUrl = hostUrl;
|
|
289
|
+
this._apiKey = apiKey;
|
|
290
|
+
this._pollingTimeoutMs = pollingTimeoutMs;
|
|
291
|
+
}
|
|
292
|
+
async popQuery(options) {
|
|
293
|
+
const abortController = new AbortController();
|
|
294
|
+
setTimeout(() => {
|
|
295
|
+
abortController.abort();
|
|
296
|
+
}, this._pollingTimeoutMs);
|
|
297
|
+
try {
|
|
298
|
+
return await fetch(`${this._hostUrl}/api/v1/retoolrpc/popQuery`, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: {
|
|
301
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
302
|
+
'Content-Type': 'application/json',
|
|
303
|
+
'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify(options),
|
|
306
|
+
// Had to cast to RequestInit['signal'] because of a bug in the types
|
|
307
|
+
// https://github.com/jasonkuhrt/graphql-request/issues/481
|
|
308
|
+
signal: abortController.signal,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (abortController.signal.aborted) {
|
|
313
|
+
throw new Error(`Polling timeout after ${this._pollingTimeoutMs}ms`);
|
|
314
|
+
}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async registerAgent(options) {
|
|
319
|
+
return fetch(`${this._hostUrl}/api/v1/retoolrpc/registerAgent`, {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: {
|
|
322
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
323
|
+
'Content-Type': 'application/json',
|
|
324
|
+
'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
|
|
325
|
+
},
|
|
326
|
+
body: JSON.stringify(options),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async postQueryResponse(options) {
|
|
330
|
+
return fetch(`${this._hostUrl}/api/v1/retoolrpc/postQueryResponse`, {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: {
|
|
333
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
334
|
+
'Content-Type': 'application/json',
|
|
335
|
+
'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify(options),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const MINIMUM_POLLING_INTERVAL_MS = 100;
|
|
343
|
+
const DEFAULT_POLLING_INTERVAL_MS = 1000;
|
|
344
|
+
const DEFAULT_POLLING_TIMEOUT_MS = 5000;
|
|
345
|
+
const DEFAULT_ENVIRONMENT_NAME = 'production';
|
|
346
|
+
const DEFAULT_VERSION = '0.0.1';
|
|
347
|
+
/**
|
|
348
|
+
* Represents the Retool RPC for interacting with Retool functions and contexts.
|
|
349
|
+
*/
|
|
350
|
+
class RetoolRPC {
|
|
351
|
+
/**
|
|
352
|
+
* Creates an instance of the RetoolRPC class.
|
|
353
|
+
*/
|
|
354
|
+
constructor(config) {
|
|
355
|
+
var _a;
|
|
356
|
+
this._functions = {};
|
|
357
|
+
this._apiKey = config.apiToken;
|
|
358
|
+
this._hostUrl = config.host.replace(/\/$/, ''); // Remove trailing / from host
|
|
359
|
+
this._resourceId = config.resourceId;
|
|
360
|
+
this._environmentName = config.environmentName || DEFAULT_ENVIRONMENT_NAME;
|
|
361
|
+
this._pollingIntervalMs = config.pollingIntervalMs
|
|
362
|
+
? Math.max(config.pollingIntervalMs, MINIMUM_POLLING_INTERVAL_MS)
|
|
363
|
+
: DEFAULT_POLLING_INTERVAL_MS;
|
|
364
|
+
this._pollingTimeoutMs = config.pollingTimeoutMs || DEFAULT_POLLING_TIMEOUT_MS;
|
|
365
|
+
this._version = config.version || DEFAULT_VERSION;
|
|
366
|
+
this._agentUuid = config.agentUuid || v4();
|
|
367
|
+
this._retoolApi = new RetoolAPI({
|
|
368
|
+
hostUrl: this._hostUrl,
|
|
369
|
+
apiKey: this._apiKey,
|
|
370
|
+
pollingTimeoutMs: this._pollingTimeoutMs || DEFAULT_POLLING_TIMEOUT_MS,
|
|
371
|
+
});
|
|
372
|
+
this._logger = (_a = config.logger) !== null && _a !== void 0 ? _a : new Logger({ logLevel: config.logLevel });
|
|
373
|
+
this._logger.debug('Retool RPC Configuration', {
|
|
374
|
+
apiKey: this._apiKey,
|
|
375
|
+
hostUrl: this._hostUrl,
|
|
376
|
+
resourceId: this._resourceId,
|
|
377
|
+
environmentName: this._environmentName,
|
|
378
|
+
agentUuid: this._agentUuid,
|
|
379
|
+
version: this._version,
|
|
380
|
+
pollingIntervalMs: this._pollingIntervalMs,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Asynchronously starts listening for incoming Retool function invocations.
|
|
385
|
+
*/
|
|
386
|
+
async listen() {
|
|
387
|
+
this._logger.info('Starting RPC agent');
|
|
388
|
+
const registerResult = await loopWithBackoff(this._pollingIntervalMs, this._logger, () => this.registerAgent());
|
|
389
|
+
if (registerResult === 'done') {
|
|
390
|
+
this._logger.info('Agent registered');
|
|
391
|
+
this._logger.info('Starting processing query');
|
|
392
|
+
loopWithBackoff(this._pollingIntervalMs, this._logger, () => this.fetchQueryAndExecute());
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Registers a Retool function with the specified function definition.
|
|
397
|
+
*/
|
|
398
|
+
register(spec) {
|
|
399
|
+
this._functions[spec.name] = {
|
|
400
|
+
arguments: spec.arguments,
|
|
401
|
+
permissions: spec.permissions,
|
|
402
|
+
implementation: spec.implementation,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Executes a Retool function with the specified arguments and context.
|
|
407
|
+
*/
|
|
408
|
+
async executeFunction(functionName, functionArguments, context) {
|
|
409
|
+
this._logger.info(`Executing function: ${functionName}, context: ${context}`);
|
|
410
|
+
if (functionName === '__testConnection__') {
|
|
411
|
+
return { result: this.testConnection(context), arguments: {} };
|
|
412
|
+
}
|
|
413
|
+
const fnSpec = this._functions[functionName];
|
|
414
|
+
if (!fnSpec) {
|
|
415
|
+
throw new FunctionNotFoundError(functionName);
|
|
416
|
+
}
|
|
417
|
+
const parsedArguments = parseFunctionArguments(functionArguments, fnSpec.arguments);
|
|
418
|
+
this._logger.debug('Parsed arguments: ', parsedArguments);
|
|
419
|
+
const result = await fnSpec.implementation(parsedArguments, context);
|
|
420
|
+
// Consider truncating large arguments
|
|
421
|
+
return { result, arguments: parsedArguments };
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Tests the current connection to the Retool server.
|
|
425
|
+
*/
|
|
426
|
+
testConnection(context) {
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
version: this._version,
|
|
430
|
+
agentUuid: this._agentUuid,
|
|
431
|
+
context,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Registers the agent with the Retool server.
|
|
436
|
+
*/
|
|
437
|
+
async registerAgent() {
|
|
438
|
+
const functionsMetadata = {};
|
|
439
|
+
for (const functionName in this._functions) {
|
|
440
|
+
functionsMetadata[functionName] = {
|
|
441
|
+
arguments: this._functions[functionName].arguments,
|
|
442
|
+
permissions: this._functions[functionName].permissions,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const registerAgentResponse = await this._retoolApi.registerAgent({
|
|
446
|
+
resourceId: this._resourceId,
|
|
447
|
+
environmentName: this._environmentName,
|
|
448
|
+
version: this._version,
|
|
449
|
+
agentUuid: this._agentUuid,
|
|
450
|
+
operations: functionsMetadata,
|
|
451
|
+
});
|
|
452
|
+
if (!registerAgentResponse.ok) {
|
|
453
|
+
if (isClientError(registerAgentResponse.status)) {
|
|
454
|
+
this._logger.error(`Error registering agent: ${registerAgentResponse.status} ${await registerAgentResponse.text()}}`);
|
|
455
|
+
// client error, stop the client
|
|
456
|
+
return 'stop';
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`Error connecting to retool server: ${registerAgentResponse.status} ${await registerAgentResponse.text()}}`);
|
|
459
|
+
}
|
|
460
|
+
const { versionHash } = await registerAgentResponse.json();
|
|
461
|
+
this._versionHash = versionHash;
|
|
462
|
+
this._logger.info(`Agent registered with versionHash: ${versionHash}`);
|
|
463
|
+
return 'done';
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Fetches a query from the Retool server and executes it.
|
|
467
|
+
*/
|
|
468
|
+
async fetchQueryAndExecute() {
|
|
469
|
+
const pendingQueryFetch = await this._retoolApi.popQuery({
|
|
470
|
+
resourceId: this._resourceId,
|
|
471
|
+
environmentName: this._environmentName,
|
|
472
|
+
agentUuid: this._agentUuid,
|
|
473
|
+
versionHash: this._versionHash,
|
|
474
|
+
});
|
|
475
|
+
if (!pendingQueryFetch.ok) {
|
|
476
|
+
if (isClientError(pendingQueryFetch.status)) {
|
|
477
|
+
this._logger.error(`Error fetching query (${pendingQueryFetch.status}): ${await pendingQueryFetch.text()}`);
|
|
478
|
+
return 'stop';
|
|
479
|
+
}
|
|
480
|
+
throw new Error(`Server error when fetching query: ${pendingQueryFetch.status}. Retrying...`);
|
|
481
|
+
}
|
|
482
|
+
const { query } = await pendingQueryFetch.json();
|
|
483
|
+
if (query) {
|
|
484
|
+
this._logger.debug('Executing query', query); // This might contain sensitive information
|
|
485
|
+
const agentReceivedQueryAt = new Date().toISOString();
|
|
486
|
+
const queryUuid = query.queryUuid;
|
|
487
|
+
const { method, parameters, context } = query.queryInfo;
|
|
488
|
+
let status;
|
|
489
|
+
let executionResponse = undefined;
|
|
490
|
+
let executionArguments = undefined;
|
|
491
|
+
let agentError = undefined;
|
|
492
|
+
this.executeFunction(method, parameters, context)
|
|
493
|
+
.then((executionResult) => {
|
|
494
|
+
executionResponse = executionResult.result;
|
|
495
|
+
executionArguments = executionResult.arguments;
|
|
496
|
+
status = 'success';
|
|
497
|
+
})
|
|
498
|
+
.catch((err) => {
|
|
499
|
+
agentError = createAgentServerError(err);
|
|
500
|
+
status = 'error';
|
|
501
|
+
})
|
|
502
|
+
.finally(() => {
|
|
503
|
+
this._retoolApi
|
|
504
|
+
.postQueryResponse({
|
|
505
|
+
resourceId: this._resourceId,
|
|
506
|
+
environmentName: this._environmentName,
|
|
507
|
+
versionHash: this._versionHash,
|
|
508
|
+
agentUuid: this._agentUuid,
|
|
509
|
+
queryUuid,
|
|
510
|
+
status,
|
|
511
|
+
data: executionResponse,
|
|
512
|
+
metadata: {
|
|
513
|
+
packageLanguage: 'javascript',
|
|
514
|
+
packageVersion: RetoolRPCVersion,
|
|
515
|
+
agentReceivedQueryAt,
|
|
516
|
+
agentFinishedQueryAt: new Date().toISOString(),
|
|
517
|
+
parameters: executionArguments,
|
|
518
|
+
},
|
|
519
|
+
error: agentError,
|
|
520
|
+
})
|
|
521
|
+
.then(async (updateQueryResponse) => {
|
|
522
|
+
this._logger.debug('Update query response status: ', updateQueryResponse.status, await updateQueryResponse.text());
|
|
523
|
+
})
|
|
524
|
+
.catch((err) => {
|
|
525
|
+
this._logger.error(`Error updating query response: `, err);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return 'continue';
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export { RetoolRPC as R };
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RetoolRPCConfig, Arguments, RetoolContext, RegisterFunctionSpec
|
|
1
|
+
import { RetoolRPCConfig, Arguments, RetoolContext, RegisterFunctionSpec } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Represents the Retool RPC for interacting with Retool functions and contexts.
|
|
4
4
|
*/
|
|
@@ -8,6 +8,7 @@ export declare class RetoolRPC {
|
|
|
8
8
|
private _resourceId;
|
|
9
9
|
private _environmentName;
|
|
10
10
|
private _pollingIntervalMs;
|
|
11
|
+
private _pollingTimeoutMs;
|
|
11
12
|
private _version;
|
|
12
13
|
private _agentUuid;
|
|
13
14
|
private _versionHash;
|
|
@@ -45,12 +46,4 @@ export declare class RetoolRPC {
|
|
|
45
46
|
* Fetches a query from the Retool server and executes it.
|
|
46
47
|
*/
|
|
47
48
|
private fetchQueryAndExecute;
|
|
48
|
-
/**
|
|
49
|
-
* Export registerAgent as a protected method for testing purpose only. Lookup TestRetoolRPC for usage.
|
|
50
|
-
*/
|
|
51
|
-
protected protectedRegisterAgent(): Promise<AgentServerStatus>;
|
|
52
|
-
/**
|
|
53
|
-
* Export fetchQueryAndExecute as a protected method for testing purpose only. Lookup TestRetoolRPC for usage.
|
|
54
|
-
*/
|
|
55
|
-
protected protectedFetchQueryAndExecute(): Promise<AgentServerStatus>;
|
|
56
49
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type RetoolRPCConfig = {
|
|
|
18
18
|
version?: string;
|
|
19
19
|
/** The optional polling interval in milliseconds. Defaults to 1000. Minimum is 100. */
|
|
20
20
|
pollingIntervalMs?: number;
|
|
21
|
+
/** The optional polling timeout in milliseconds. Defaults to 5000. */
|
|
22
|
+
pollingTimeoutMs?: number;
|
|
21
23
|
/** The optional UUID of the agent. Will be automatically generated by default */
|
|
22
24
|
agentUuid?: string;
|
|
23
25
|
/** The optional log level. */
|
package/dist/src/utils/api.d.ts
CHANGED
|
@@ -32,9 +32,11 @@ type PostQueryResponseRequest = {
|
|
|
32
32
|
export declare class RetoolAPI {
|
|
33
33
|
private _hostUrl;
|
|
34
34
|
private _apiKey;
|
|
35
|
-
|
|
35
|
+
private _pollingTimeoutMs;
|
|
36
|
+
constructor({ hostUrl, apiKey, pollingTimeoutMs }: {
|
|
36
37
|
hostUrl: string;
|
|
37
38
|
apiKey: string;
|
|
39
|
+
pollingTimeoutMs: number;
|
|
38
40
|
});
|
|
39
41
|
popQuery(options: PopQueryRequest): Promise<import("node-fetch").Response>;
|
|
40
42
|
registerAgent(options: RegisterAgentRequest): Promise<import("node-fetch").Response>;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RetoolRPCVersion = "0.1.
|
|
1
|
+
export declare const RetoolRPCVersion = "0.1.3";
|
package/example.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retoolrpc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "TypeScript package for Retool RPC",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"homepage": "https://github.com/tryretool/retoolrpc#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/tryretool/retoolrpc/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/tryretool/retoolrpc.git"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
5
15
|
"exports": {
|
|
6
16
|
".": {
|
|
7
|
-
"import": "./dist/index.
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
8
18
|
"require": "./dist/cjs/index.js"
|
|
9
19
|
},
|
|
10
20
|
"./package.json": "./package.json"
|
|
11
21
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "yarn build:cjs && yarn build:esm",
|
|
24
|
+
"build:cjs": "cross-env MODE=cjs rollup -c",
|
|
25
|
+
"build:esm": "rollup -c",
|
|
26
|
+
"bumpVersion": "ts-node scripts/bumpVersion.ts",
|
|
27
|
+
"example": "nodemon -V -L --watch src -e ts example.ts",
|
|
28
|
+
"release": "yarn build && npm publish --access public",
|
|
29
|
+
"test": "vitest",
|
|
30
|
+
"test:api": "tsc --project tsconfig.json"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@types/node-fetch": "^2.6.4",
|
|
34
|
+
"@types/uuid": "^9.0.2",
|
|
35
|
+
"abort-controller": "^3.0.0",
|
|
36
|
+
"node-fetch": "^2.6",
|
|
37
|
+
"ts-dedent": "^2.2.0",
|
|
38
|
+
"uuid": "^9.0.0"
|
|
15
39
|
},
|
|
16
|
-
"license": "MIT",
|
|
17
|
-
"keywords": [],
|
|
18
40
|
"devDependencies": {
|
|
19
41
|
"@rollup/plugin-typescript": "^8.2.0",
|
|
20
42
|
"@types/fs-extra": "^11.0.1",
|
|
@@ -31,27 +53,5 @@
|
|
|
31
53
|
"tslib": "^2.1.0",
|
|
32
54
|
"typescript": "5.2.2",
|
|
33
55
|
"vitest": "^0.34.5"
|
|
34
|
-
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"@types/node-fetch": "^2.6.4",
|
|
37
|
-
"@types/uuid": "^9.0.2",
|
|
38
|
-
"abort-controller": "^3.0.0",
|
|
39
|
-
"node-fetch": "^2.6",
|
|
40
|
-
"ts-dedent": "^2.2.0",
|
|
41
|
-
"uuid": "^9.0.0"
|
|
42
|
-
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build:cjs": "cross-env MODE=cjs rollup -c",
|
|
45
|
-
"build:esm": "rollup -c",
|
|
46
|
-
"build": "yarn build:cjs && yarn build:esm",
|
|
47
|
-
"example": "nodemon -V -L --watch src -e ts example.ts",
|
|
48
|
-
"bumpVersion": "ts-node scripts/bumpVersion.ts",
|
|
49
|
-
"release": "yarn build && npm publish --access public",
|
|
50
|
-
"test": "vitest",
|
|
51
|
-
"test:api": "tsc --project tsconfig.json"
|
|
52
|
-
},
|
|
53
|
-
"homepage": "https://github.com/tryretool/retoolrpc#readme",
|
|
54
|
-
"bugs": {
|
|
55
|
-
"url": "https://github.com/tryretool/retoolrpc/issues"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/rollup.config.js
CHANGED
|
@@ -27,6 +27,8 @@ export default {
|
|
|
27
27
|
{
|
|
28
28
|
dir: isCJSBuild ? 'dist/cjs' : 'dist',
|
|
29
29
|
format: isCJSBuild ? 'cjs' : 'esm',
|
|
30
|
+
entryFileNames: isCJSBuild ? '[name].js' : '[name].mjs', // Output file name pattern
|
|
31
|
+
chunkFileNames: isCJSBuild ? '[name]-[hash].js' : '[name]-[hash].mjs', // For code splitting
|
|
30
32
|
},
|
|
31
33
|
],
|
|
32
34
|
plugins: [ts({ tsconfig: isCJSBuild ? 'tsconfig.cjs.json' : 'tsconfig.json' }), commonjsPkgJSONPlugin()],
|