retoolrpc 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,74 @@
1
- # `retoolrpc` Javascript Client Package
1
+ # `retoolrpc` JavaScript client package
2
2
 
3
- Welcome to the `retoolrpc` client package! This package serves as the client-side component for the `retoolrpc` project. For detailed information and usage instructions, please refer to our official documentation.
3
+ Review Retool's [RPC documentation](https://docs.retool.com/docs/retool-rpc) before installing the JavaScript package.
4
4
 
5
- ## About `retoolrpc`
5
+ ## Installation
6
6
 
7
- Retool RPC (Remote Procedure Call) is a fast, secure solution for connecting your own codebase to Retool. You define functions in your backend and then call them from Retool apps using a Retool RPC resource.
7
+ You can use `npm`, `yarn`, or `pnpm` to install the package.
8
8
 
9
- ## Documentation
9
+ ### npm
10
10
 
11
- For more detailed information, examples, and usage guidelines, please refer to our official documentation. The documentation provides in-depth insights into the `retoolrpc` client package, including installation instructions and comprehensive usage guides.
11
+ ```
12
+ # Using npm
13
+ npm install retoolrpc
12
14
 
13
- [Link to Documentation](https://docs.retool.com/retool-rpc)
15
+ # Using yarn
16
+ yarn add retoolrpc
14
17
 
15
- ## Getting Started
18
+ # Using pnpm
19
+ pnpm add retoolrpc
20
+ ```
16
21
 
17
- To get started with the `retoolrpc` client package, please consult our documentation, which provides step-by-step instructions and examples to help you integrate and use this package effectively in your projects.
22
+ ## Usage example
18
23
 
19
- ## Issues and Support
24
+ ```javascript
25
+ import { RetoolRPC } from 'retoolrpc'
26
+ // for CommonJS, uses `require`, e.g.:
27
+ // var { RetoolRPC} = require('retoolrpc')
20
28
 
21
- If you encounter any issues, have questions, or need assistance, please feel free to reach out to our support team or report issues on our GitHub repository.
29
+ const rpc = new RetoolRPC({
30
+ apiToken: 'your-api-token-here', // Replace this token with your API token
31
+ host: 'http://localhost:3000/', // Replace this host with your host
32
+ resourceId: 'resource-id', // Replace this resource ID with your ID
33
+ environmentName: 'production', // Replace this environment name with your name (defaults to production)
34
+ pollingIntervalMs: 1000, // The polling interval for the RPC
35
+ version: '0.0.1', // An optional version number for functions schemas
36
+ logLevel: 'info', // Change to 'debug' for verbose logging or use own logger implementation by passing a logger param
37
+ })
22
38
 
23
- [Link to GitHub Repository](https://github.com/tryretool/retoolrpc)
39
+ rpc.register({
40
+ name: 'helloWorld',
41
+ arguments: {
42
+ name: { type: 'string', description: 'Your name', required: true },
43
+ },
44
+ implementation: async (args, context) => {
45
+ return {
46
+ message: `Hello ${args.name}!`,
47
+ context,
48
+ }
49
+ },
50
+ })
24
51
 
25
- ## License
52
+ await rpc.listen()
53
+ ```
26
54
 
27
- This project is licensed under the MIT License. Please refer to the LICENSE file in this repository for more details.
55
+ ## ORM Support
28
56
 
29
- ---
57
+ For users of [Sequelize](https://sequelize.org/), we offer an ORM mixin that enables the addition of fundamental model functions with a single function call, `registerModel`. When you register a model with `rpc`, it automatically registers various remote functions for the model, including `create`, `update`, `createOrUpdate`, `findByPk`, `findBy`, and `findAll`. You can find additional details [here](https://github.com/tryretool/retoolrpc/blob/main/javascript/src/addons/sequelize.ts#L5-L14).
30
58
 
31
- Feel free to replace the placeholders with the actual links and relevant information for your project. This README section provides a clear introduction to the `retoolrpc` client package and directs users to your official documentation and support channels.
59
+ Following is an example of registering a `User` model:
60
+ ```javascript
61
+ import { RetoolRPC, sequelizeMixin } from 'retoolrpc'
62
+ import { User } from './orm/models' // the path to your model may be different
63
+
64
+ const CustomRPC = sequelizeMixin(RetoolRPC)
65
+ const rpc = new CustomRPC({ ... })
66
+
67
+ rpc.registerModel({
68
+ model: Experiment,
69
+ findByAttributes: ['id', 'name'],
70
+ writeAttributes: ['name'],
71
+ })
72
+ ```
73
+
74
+ We plan to support other ORMs in the future.
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var dedent = require('ts-dedent');
4
- var rpc$1 = require('./rpc-5628288a.js');
4
+ var rpc$1 = require('./rpc-b950d603.js');
5
5
  require('uuid');
6
6
  require('node-fetch');
7
7
  require('abort-controller');
package/dist/cjs/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var rpc = require('./rpc-5628288a.js');
5
+ var rpc = require('./rpc-b950d603.js');
6
6
  require('uuid');
7
7
  require('ts-dedent');
8
8
  require('node-fetch');
@@ -0,0 +1,539 @@
1
+ 'use strict';
2
+
3
+ var uuid = require('uuid');
4
+ var dedent = require('ts-dedent');
5
+ var fetch = require('node-fetch');
6
+ var AbortControllerFallback = require('abort-controller');
7
+
8
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
+
10
+ var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
11
+ var AbortControllerFallback__default = /*#__PURE__*/_interopDefaultLegacy(AbortControllerFallback);
12
+
13
+ const AGENT_SERVER_ERROR = 'AgentServerError';
14
+ function createAgentServerError(error) {
15
+ if (error instanceof Error) {
16
+ const agentError = {
17
+ name: error.name,
18
+ message: error.message,
19
+ stack: error.stack,
20
+ };
21
+ if ('code' in error) {
22
+ if (typeof error.code === 'number') {
23
+ agentError.code = error.code;
24
+ }
25
+ else if (error.code === 'string') {
26
+ agentError.code = parseInt(error.code, 10);
27
+ }
28
+ }
29
+ if ('details' in error) {
30
+ agentError.details = error.details;
31
+ }
32
+ return agentError;
33
+ }
34
+ if (typeof error === 'string') {
35
+ return {
36
+ name: AGENT_SERVER_ERROR,
37
+ message: error,
38
+ };
39
+ }
40
+ return {
41
+ name: AGENT_SERVER_ERROR,
42
+ message: 'Unknown agent server error',
43
+ };
44
+ }
45
+ class FunctionNotFoundError extends Error {
46
+ constructor(functionName) {
47
+ super(`Function "${functionName}" not found on remote agent server.`);
48
+ this.name = 'FunctionNotFoundError';
49
+ }
50
+ }
51
+ class InvalidArgumentsError extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = 'InvalidArgumentsError';
55
+ }
56
+ }
57
+
58
+ function pick(obj, keys) {
59
+ const result = {};
60
+ for (const key of keys) {
61
+ if (key in obj) {
62
+ result[key] = obj[key];
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+ function isRecord(value) {
68
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ }
70
+ function isFalsyArgumentValue(value) {
71
+ return value === null || value === undefined || value === '';
72
+ }
73
+ function isBooleanString(value) {
74
+ if (typeof value === 'string') {
75
+ const lowercaseValue = value.toLowerCase();
76
+ return lowercaseValue === 'true' || lowercaseValue === 'false';
77
+ }
78
+ return false;
79
+ }
80
+ function isNumberString(value) {
81
+ if (typeof value === 'string') {
82
+ // Use a regular expression to check if the string is a valid number
83
+ return /^-?\d+(\.\d+)?$/.test(value);
84
+ }
85
+ return false;
86
+ }
87
+ function isClientError(status) {
88
+ return status >= 400 && status < 500;
89
+ }
90
+
91
+ class ArgumentParser {
92
+ constructor(schema) {
93
+ this.schema = schema;
94
+ }
95
+ parse(argumentsToParse) {
96
+ const parsedArguments = { ...argumentsToParse };
97
+ const parsedErrors = [];
98
+ for (const argName in this.schema) {
99
+ const argDefinition = this.schema[argName];
100
+ const argValue = argumentsToParse[argName];
101
+ const falsyArgValue = isFalsyArgumentValue(argValue);
102
+ if (falsyArgValue) {
103
+ if (argDefinition.required) {
104
+ parsedErrors.push(`Argument "${argName}" is required but missing.`);
105
+ continue;
106
+ }
107
+ }
108
+ if (!falsyArgValue) {
109
+ if (argDefinition.array) {
110
+ if (!Array.isArray(argValue)) {
111
+ parsedErrors.push(`Argument "${argName}" should be an array.`);
112
+ continue;
113
+ }
114
+ const parseValueTypeItems = argValue.map((item) => this.parseValueType(item, argDefinition.type));
115
+ if (!parseValueTypeItems.every((item) => item.isValidType)) {
116
+ parsedErrors.push(`Argument "${argName}" should be an array of type "${argDefinition.type}".`);
117
+ }
118
+ parsedArguments[argName] = parseValueTypeItems.map((item) => item.parsedValue);
119
+ }
120
+ else {
121
+ const parsedValueTypeItem = this.parseValueType(argValue, argDefinition.type);
122
+ if (!parsedValueTypeItem.isValidType) {
123
+ parsedErrors.push(`Argument "${argName}" should be of type "${argDefinition.type}".`);
124
+ }
125
+ parsedArguments[argName] = parsedValueTypeItem.parsedValue;
126
+ }
127
+ }
128
+ }
129
+ return { parsedErrors, parsedArguments };
130
+ }
131
+ parseValueType(value, expectedType) {
132
+ switch (expectedType) {
133
+ case 'string':
134
+ // For string type, we just need to convert to string.
135
+ return {
136
+ isValidType: true,
137
+ parsedValue: typeof value === 'object' ? JSON.stringify(value) : String(value), // Need to do this because String(object) returns "[object Object]".
138
+ };
139
+ case 'boolean':
140
+ // For boolean type, we need to check if the value is a boolean or a boolean string.
141
+ if (typeof value === 'boolean') {
142
+ return {
143
+ isValidType: true,
144
+ parsedValue: value,
145
+ };
146
+ }
147
+ if (isBooleanString(value)) {
148
+ return {
149
+ isValidType: true,
150
+ parsedValue: value.toLowerCase() === 'true',
151
+ };
152
+ }
153
+ return {
154
+ isValidType: false,
155
+ parsedValue: value,
156
+ };
157
+ case 'number':
158
+ // For number type, we need to check if the value is a number or a number string.
159
+ if (typeof value === 'number') {
160
+ return {
161
+ isValidType: true,
162
+ parsedValue: value,
163
+ };
164
+ }
165
+ if (isNumberString(value)) {
166
+ return {
167
+ isValidType: true,
168
+ parsedValue: parseFloat(value),
169
+ };
170
+ }
171
+ return {
172
+ isValidType: false,
173
+ parsedValue: value,
174
+ };
175
+ case 'dict':
176
+ // For dict type, we need to check if the value is a record.
177
+ if (isRecord(value)) {
178
+ return {
179
+ isValidType: true,
180
+ parsedValue: value,
181
+ };
182
+ }
183
+ return {
184
+ isValidType: false,
185
+ parsedValue: value,
186
+ };
187
+ case 'json':
188
+ // For json type, we need to check if the value is a valid JSON string.
189
+ try {
190
+ const parsedJSONValue = JSON.parse(JSON.stringify(value));
191
+ return {
192
+ isValidType: true,
193
+ parsedValue: parsedJSONValue,
194
+ };
195
+ }
196
+ catch {
197
+ return {
198
+ isValidType: false,
199
+ parsedValue: value,
200
+ };
201
+ }
202
+ default:
203
+ throw new Error(`Unknown argument type "${expectedType}".`);
204
+ }
205
+ }
206
+ }
207
+ function parseFunctionArguments(args, schema) {
208
+ if (!isRecord(args)) {
209
+ throw new Error(`The given arguments are invalid.`);
210
+ }
211
+ const argumentParser = new ArgumentParser(schema);
212
+ const { parsedArguments, parsedErrors } = argumentParser.parse(args);
213
+ if (parsedErrors.length > 0) {
214
+ const invalidArgumentsError = dedent.dedent `
215
+ Invalid parameter(s) found:
216
+ ${parsedErrors.join('\n')}
217
+ `;
218
+ throw new InvalidArgumentsError(invalidArgumentsError);
219
+ }
220
+ return pick(parsedArguments, Object.keys(schema));
221
+ }
222
+
223
+ const CONNECTION_ERROR_INITIAL_TIMEOUT_MS = 50;
224
+ const CONNECTION_ERROR_RETRY_MAX_MS = 1000 * 60 * 10; // 10 minutes
225
+ function sleep(ms) {
226
+ return new Promise((resolve) => {
227
+ setTimeout(resolve, ms);
228
+ });
229
+ }
230
+ async function loopWithBackoff(pollingIntervalMs, logger, callback) {
231
+ let delayTimeMs = CONNECTION_ERROR_INITIAL_TIMEOUT_MS;
232
+ let lastLoopTimestamp = Date.now();
233
+ while (true) {
234
+ try {
235
+ const result = await callback();
236
+ const currentTimestamp = Date.now();
237
+ const loopDurationMs = currentTimestamp - lastLoopTimestamp;
238
+ lastLoopTimestamp = currentTimestamp;
239
+ logger.debug(`Loop time: ${loopDurationMs}ms, delay time: ${delayTimeMs}ms, polling interval: ${pollingIntervalMs}ms`);
240
+ if (result !== 'continue') {
241
+ return result;
242
+ }
243
+ await sleep(pollingIntervalMs);
244
+ delayTimeMs = Math.max(delayTimeMs / 2, CONNECTION_ERROR_INITIAL_TIMEOUT_MS);
245
+ }
246
+ catch (err) {
247
+ logger.error('Error running RPC agent', err);
248
+ await sleep(delayTimeMs);
249
+ delayTimeMs = Math.min(delayTimeMs * 2, CONNECTION_ERROR_RETRY_MAX_MS);
250
+ }
251
+ }
252
+ }
253
+
254
+ const LOG_LEVEL_RANKINGS = {
255
+ debug: 0,
256
+ info: 1,
257
+ warn: 2,
258
+ error: 3,
259
+ };
260
+ class Logger {
261
+ constructor(options) {
262
+ this.currentLogLevel = options.logLevel || 'info'; // Default to 'info' if logLevel is not specified
263
+ }
264
+ shouldLog(level) {
265
+ return LOG_LEVEL_RANKINGS[level] >= LOG_LEVEL_RANKINGS[this.currentLogLevel] && process.env.NODE_ENV !== 'test';
266
+ }
267
+ debug(...messages) {
268
+ if (this.shouldLog('debug')) {
269
+ console.log(...messages);
270
+ }
271
+ }
272
+ info(...messages) {
273
+ if (this.shouldLog('info')) {
274
+ console.log(...messages);
275
+ }
276
+ }
277
+ warn(...messages) {
278
+ if (this.shouldLog('warn')) {
279
+ console.log(...messages);
280
+ }
281
+ }
282
+ error(...messages) {
283
+ if (this.shouldLog('error')) {
284
+ console.log(...messages);
285
+ }
286
+ }
287
+ }
288
+
289
+ const RetoolRPCVersion = '0.1.0';
290
+
291
+ // AbortController was added in node v14.17.0 globally, but we need to polyfill it for older versions
292
+ const AbortController = globalThis.AbortController || AbortControllerFallback__default["default"];
293
+ const POLLING_TIMEOUT_MS = 5 * 1000; // 5 seconds
294
+ class RetoolAPI {
295
+ constructor({ hostUrl, apiKey }) {
296
+ this._hostUrl = hostUrl;
297
+ this._apiKey = apiKey;
298
+ }
299
+ async popQuery(options) {
300
+ const abortController = new AbortController();
301
+ setTimeout(() => {
302
+ abortController.abort();
303
+ }, POLLING_TIMEOUT_MS);
304
+ try {
305
+ return await fetch__default["default"](`${this._hostUrl}/api/v1/retoolrpc/popQuery`, {
306
+ method: 'POST',
307
+ headers: {
308
+ Authorization: `Bearer ${this._apiKey}`,
309
+ 'Content-Type': 'application/json',
310
+ 'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
311
+ },
312
+ body: JSON.stringify(options),
313
+ // Had to cast to RequestInit['signal'] because of a bug in the types
314
+ // https://github.com/jasonkuhrt/graphql-request/issues/481
315
+ signal: abortController.signal,
316
+ });
317
+ }
318
+ catch (error) {
319
+ if (abortController.signal.aborted) {
320
+ throw new Error(`Polling timeout after ${POLLING_TIMEOUT_MS}ms`);
321
+ }
322
+ throw error;
323
+ }
324
+ }
325
+ async registerAgent(options) {
326
+ return fetch__default["default"](`${this._hostUrl}/api/v1/retoolrpc/registerAgent`, {
327
+ method: 'POST',
328
+ headers: {
329
+ Authorization: `Bearer ${this._apiKey}`,
330
+ 'Content-Type': 'application/json',
331
+ 'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
332
+ },
333
+ body: JSON.stringify(options),
334
+ });
335
+ }
336
+ async postQueryResponse(options) {
337
+ return fetch__default["default"](`${this._hostUrl}/api/v1/retoolrpc/postQueryResponse`, {
338
+ method: 'POST',
339
+ headers: {
340
+ Authorization: `Bearer ${this._apiKey}`,
341
+ 'Content-Type': 'application/json',
342
+ 'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
343
+ },
344
+ body: JSON.stringify(options),
345
+ });
346
+ }
347
+ }
348
+
349
+ const MINIMUM_POLLING_INTERVAL_MS = 100;
350
+ const DEFAULT_POLLING_INTERVAL_MS = 1000;
351
+ const DEFAULT_ENVIRONMENT_NAME = 'production';
352
+ const DEFAULT_VERSION = '0.0.1';
353
+ /**
354
+ * Represents the Retool RPC for interacting with Retool functions and contexts.
355
+ */
356
+ class RetoolRPC {
357
+ /**
358
+ * Creates an instance of the RetoolRPC class.
359
+ */
360
+ constructor(config) {
361
+ var _a;
362
+ this._functions = {};
363
+ this._apiKey = config.apiToken;
364
+ this._hostUrl = config.host.replace(/\/$/, ''); // Remove trailing / from host
365
+ this._resourceId = config.resourceId;
366
+ this._environmentName = config.environmentName || DEFAULT_ENVIRONMENT_NAME;
367
+ this._pollingIntervalMs = config.pollingIntervalMs
368
+ ? Math.max(config.pollingIntervalMs, MINIMUM_POLLING_INTERVAL_MS)
369
+ : DEFAULT_POLLING_INTERVAL_MS;
370
+ this._version = config.version || DEFAULT_VERSION;
371
+ this._agentUuid = config.agentUuid || uuid.v4();
372
+ this._retoolApi = new RetoolAPI({ hostUrl: this._hostUrl, apiKey: this._apiKey });
373
+ this._logger = (_a = config.logger) !== null && _a !== void 0 ? _a : new Logger({ logLevel: config.logLevel });
374
+ this._logger.debug('Retool RPC Configuration', {
375
+ apiKey: this._apiKey,
376
+ hostUrl: this._hostUrl,
377
+ resourceId: this._resourceId,
378
+ environmentName: this._environmentName,
379
+ agentUuid: this._agentUuid,
380
+ version: this._version,
381
+ pollingIntervalMs: this._pollingIntervalMs,
382
+ });
383
+ }
384
+ /**
385
+ * Asynchronously starts listening for incoming Retool function invocations.
386
+ */
387
+ async listen() {
388
+ this._logger.info('Starting RPC agent');
389
+ const registerResult = await loopWithBackoff(this._pollingIntervalMs, this._logger, () => this.registerAgent());
390
+ if (registerResult === 'done') {
391
+ this._logger.info('Agent registered');
392
+ this._logger.info('Starting processing query');
393
+ loopWithBackoff(this._pollingIntervalMs, this._logger, () => this.fetchQueryAndExecute());
394
+ }
395
+ }
396
+ /**
397
+ * Registers a Retool function with the specified function definition.
398
+ */
399
+ register(spec) {
400
+ this._functions[spec.name] = {
401
+ arguments: spec.arguments,
402
+ permissions: spec.permissions,
403
+ implementation: spec.implementation,
404
+ };
405
+ }
406
+ /**
407
+ * Executes a Retool function with the specified arguments and context.
408
+ */
409
+ async executeFunction(functionName, functionArguments, context) {
410
+ this._logger.info(`Executing function: ${functionName}, context: ${context}`);
411
+ if (functionName === '__testConnection__') {
412
+ return { result: this.testConnection(context), arguments: {} };
413
+ }
414
+ const fnSpec = this._functions[functionName];
415
+ if (!fnSpec) {
416
+ throw new FunctionNotFoundError(functionName);
417
+ }
418
+ const parsedArguments = parseFunctionArguments(functionArguments, fnSpec.arguments);
419
+ this._logger.debug('Parsed arguments: ', parsedArguments);
420
+ const result = await fnSpec.implementation(parsedArguments, context);
421
+ // Consider truncating large arguments
422
+ return { result, arguments: parsedArguments };
423
+ }
424
+ /**
425
+ * Tests the current connection to the Retool server.
426
+ */
427
+ testConnection(context) {
428
+ return {
429
+ success: true,
430
+ version: this._version,
431
+ agentUuid: this._agentUuid,
432
+ context,
433
+ };
434
+ }
435
+ /**
436
+ * Registers the agent with the Retool server.
437
+ */
438
+ async registerAgent() {
439
+ const functionsMetadata = {};
440
+ for (const functionName in this._functions) {
441
+ functionsMetadata[functionName] = {
442
+ arguments: this._functions[functionName].arguments,
443
+ permissions: this._functions[functionName].permissions,
444
+ };
445
+ }
446
+ const registerAgentResponse = await this._retoolApi.registerAgent({
447
+ resourceId: this._resourceId,
448
+ environmentName: this._environmentName,
449
+ version: this._version,
450
+ agentUuid: this._agentUuid,
451
+ operations: functionsMetadata,
452
+ });
453
+ if (!registerAgentResponse.ok) {
454
+ if (isClientError(registerAgentResponse.status)) {
455
+ this._logger.error(`Error registering agent: ${registerAgentResponse.status} ${await registerAgentResponse.text()}}`);
456
+ // client error, stop the client
457
+ return 'stop';
458
+ }
459
+ throw new Error(`Error connecting to retool server: ${registerAgentResponse.status} ${await registerAgentResponse.text()}}`);
460
+ }
461
+ const { versionHash } = await registerAgentResponse.json();
462
+ this._versionHash = versionHash;
463
+ this._logger.info(`Agent registered with versionHash: ${versionHash}`);
464
+ return 'done';
465
+ }
466
+ /**
467
+ * Fetches a query from the Retool server and executes it.
468
+ */
469
+ async fetchQueryAndExecute() {
470
+ const pendingQueryFetch = await this._retoolApi.popQuery({
471
+ resourceId: this._resourceId,
472
+ environmentName: this._environmentName,
473
+ agentUuid: this._agentUuid,
474
+ versionHash: this._versionHash,
475
+ });
476
+ if (!pendingQueryFetch.ok) {
477
+ if (isClientError(pendingQueryFetch.status)) {
478
+ this._logger.error(`Error fetching query (${pendingQueryFetch.status}): ${await pendingQueryFetch.text()}`);
479
+ return 'stop';
480
+ }
481
+ throw new Error(`Server error when fetching query: ${pendingQueryFetch.status}. Retrying...`);
482
+ }
483
+ const { query } = await pendingQueryFetch.json();
484
+ if (query) {
485
+ this._logger.debug('Executing query', query); // This might contain sensitive information
486
+ const agentReceivedQueryAt = new Date().toISOString();
487
+ const queryUuid = query.queryUuid;
488
+ const { method, parameters, context } = query.queryInfo;
489
+ let status;
490
+ let executionResponse = undefined;
491
+ let executionArguments = undefined;
492
+ let agentError = undefined;
493
+ try {
494
+ const executionResult = await this.executeFunction(method, parameters, context);
495
+ executionResponse = executionResult.result;
496
+ executionArguments = executionResult.arguments;
497
+ status = 'success';
498
+ }
499
+ catch (err) {
500
+ agentError = createAgentServerError(err);
501
+ status = 'error';
502
+ }
503
+ const agentFinishedQueryAt = new Date().toISOString();
504
+ const updateQueryResponse = await this._retoolApi.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,
517
+ parameters: executionArguments,
518
+ },
519
+ error: agentError,
520
+ });
521
+ this._logger.debug('Update query response status: ', updateQueryResponse.status, await updateQueryResponse.text());
522
+ }
523
+ return 'continue';
524
+ }
525
+ /**
526
+ * Export registerAgent as a protected method for testing purpose only. Lookup TestRetoolRPC for usage.
527
+ */
528
+ async protectedRegisterAgent() {
529
+ return this.registerAgent();
530
+ }
531
+ /**
532
+ * Export fetchQueryAndExecute as a protected method for testing purpose only. Lookup TestRetoolRPC for usage.
533
+ */
534
+ async protectedFetchQueryAndExecute() {
535
+ return this.fetchQueryAndExecute();
536
+ }
537
+ }
538
+
539
+ exports.RetoolRPC = RetoolRPC;
package/dist/example.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import dedent from 'ts-dedent';
2
- import { R as RetoolRPC } from './rpc-fb0623fd.js';
2
+ import { R as RetoolRPC } from './rpc-aa760f24.js';
3
3
  import 'uuid';
4
4
  import 'node-fetch';
5
5
  import 'abort-controller';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { R as RetoolRPC } from './rpc-fb0623fd.js';
1
+ export { R as RetoolRPC } from './rpc-aa760f24.js';
2
2
  import 'uuid';
3
3
  import 'ts-dedent';
4
4
  import 'node-fetch';
@@ -0,0 +1,532 @@
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.0';
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
+ const POLLING_TIMEOUT_MS = 5 * 1000; // 5 seconds
287
+ class RetoolAPI {
288
+ constructor({ hostUrl, apiKey }) {
289
+ this._hostUrl = hostUrl;
290
+ this._apiKey = apiKey;
291
+ }
292
+ async popQuery(options) {
293
+ const abortController = new AbortController();
294
+ setTimeout(() => {
295
+ abortController.abort();
296
+ }, POLLING_TIMEOUT_MS);
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 ${POLLING_TIMEOUT_MS}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_ENVIRONMENT_NAME = 'production';
345
+ const DEFAULT_VERSION = '0.0.1';
346
+ /**
347
+ * Represents the Retool RPC for interacting with Retool functions and contexts.
348
+ */
349
+ class RetoolRPC {
350
+ /**
351
+ * Creates an instance of the RetoolRPC class.
352
+ */
353
+ constructor(config) {
354
+ var _a;
355
+ this._functions = {};
356
+ this._apiKey = config.apiToken;
357
+ this._hostUrl = config.host.replace(/\/$/, ''); // Remove trailing / from host
358
+ this._resourceId = config.resourceId;
359
+ this._environmentName = config.environmentName || DEFAULT_ENVIRONMENT_NAME;
360
+ this._pollingIntervalMs = config.pollingIntervalMs
361
+ ? Math.max(config.pollingIntervalMs, MINIMUM_POLLING_INTERVAL_MS)
362
+ : DEFAULT_POLLING_INTERVAL_MS;
363
+ this._version = config.version || DEFAULT_VERSION;
364
+ this._agentUuid = config.agentUuid || v4();
365
+ this._retoolApi = new RetoolAPI({ hostUrl: this._hostUrl, apiKey: this._apiKey });
366
+ this._logger = (_a = config.logger) !== null && _a !== void 0 ? _a : new Logger({ logLevel: config.logLevel });
367
+ this._logger.debug('Retool RPC Configuration', {
368
+ apiKey: this._apiKey,
369
+ hostUrl: this._hostUrl,
370
+ resourceId: this._resourceId,
371
+ environmentName: this._environmentName,
372
+ agentUuid: this._agentUuid,
373
+ version: this._version,
374
+ pollingIntervalMs: this._pollingIntervalMs,
375
+ });
376
+ }
377
+ /**
378
+ * Asynchronously starts listening for incoming Retool function invocations.
379
+ */
380
+ async listen() {
381
+ this._logger.info('Starting RPC agent');
382
+ const registerResult = await loopWithBackoff(this._pollingIntervalMs, this._logger, () => this.registerAgent());
383
+ if (registerResult === 'done') {
384
+ this._logger.info('Agent registered');
385
+ this._logger.info('Starting processing query');
386
+ loopWithBackoff(this._pollingIntervalMs, this._logger, () => this.fetchQueryAndExecute());
387
+ }
388
+ }
389
+ /**
390
+ * Registers a Retool function with the specified function definition.
391
+ */
392
+ register(spec) {
393
+ this._functions[spec.name] = {
394
+ arguments: spec.arguments,
395
+ permissions: spec.permissions,
396
+ implementation: spec.implementation,
397
+ };
398
+ }
399
+ /**
400
+ * Executes a Retool function with the specified arguments and context.
401
+ */
402
+ async executeFunction(functionName, functionArguments, context) {
403
+ this._logger.info(`Executing function: ${functionName}, context: ${context}`);
404
+ if (functionName === '__testConnection__') {
405
+ return { result: this.testConnection(context), arguments: {} };
406
+ }
407
+ const fnSpec = this._functions[functionName];
408
+ if (!fnSpec) {
409
+ throw new FunctionNotFoundError(functionName);
410
+ }
411
+ const parsedArguments = parseFunctionArguments(functionArguments, fnSpec.arguments);
412
+ this._logger.debug('Parsed arguments: ', parsedArguments);
413
+ const result = await fnSpec.implementation(parsedArguments, context);
414
+ // Consider truncating large arguments
415
+ return { result, arguments: parsedArguments };
416
+ }
417
+ /**
418
+ * Tests the current connection to the Retool server.
419
+ */
420
+ testConnection(context) {
421
+ return {
422
+ success: true,
423
+ version: this._version,
424
+ agentUuid: this._agentUuid,
425
+ context,
426
+ };
427
+ }
428
+ /**
429
+ * Registers the agent with the Retool server.
430
+ */
431
+ async registerAgent() {
432
+ const functionsMetadata = {};
433
+ for (const functionName in this._functions) {
434
+ functionsMetadata[functionName] = {
435
+ arguments: this._functions[functionName].arguments,
436
+ permissions: this._functions[functionName].permissions,
437
+ };
438
+ }
439
+ const registerAgentResponse = await this._retoolApi.registerAgent({
440
+ resourceId: this._resourceId,
441
+ environmentName: this._environmentName,
442
+ version: this._version,
443
+ agentUuid: this._agentUuid,
444
+ operations: functionsMetadata,
445
+ });
446
+ if (!registerAgentResponse.ok) {
447
+ if (isClientError(registerAgentResponse.status)) {
448
+ this._logger.error(`Error registering agent: ${registerAgentResponse.status} ${await registerAgentResponse.text()}}`);
449
+ // client error, stop the client
450
+ return 'stop';
451
+ }
452
+ throw new Error(`Error connecting to retool server: ${registerAgentResponse.status} ${await registerAgentResponse.text()}}`);
453
+ }
454
+ const { versionHash } = await registerAgentResponse.json();
455
+ this._versionHash = versionHash;
456
+ this._logger.info(`Agent registered with versionHash: ${versionHash}`);
457
+ return 'done';
458
+ }
459
+ /**
460
+ * Fetches a query from the Retool server and executes it.
461
+ */
462
+ async fetchQueryAndExecute() {
463
+ const pendingQueryFetch = await this._retoolApi.popQuery({
464
+ resourceId: this._resourceId,
465
+ environmentName: this._environmentName,
466
+ agentUuid: this._agentUuid,
467
+ versionHash: this._versionHash,
468
+ });
469
+ if (!pendingQueryFetch.ok) {
470
+ if (isClientError(pendingQueryFetch.status)) {
471
+ this._logger.error(`Error fetching query (${pendingQueryFetch.status}): ${await pendingQueryFetch.text()}`);
472
+ return 'stop';
473
+ }
474
+ throw new Error(`Server error when fetching query: ${pendingQueryFetch.status}. Retrying...`);
475
+ }
476
+ const { query } = await pendingQueryFetch.json();
477
+ if (query) {
478
+ this._logger.debug('Executing query', query); // This might contain sensitive information
479
+ const agentReceivedQueryAt = new Date().toISOString();
480
+ const queryUuid = query.queryUuid;
481
+ const { method, parameters, context } = query.queryInfo;
482
+ let status;
483
+ let executionResponse = undefined;
484
+ let executionArguments = undefined;
485
+ let agentError = undefined;
486
+ try {
487
+ const executionResult = await this.executeFunction(method, parameters, context);
488
+ executionResponse = executionResult.result;
489
+ executionArguments = executionResult.arguments;
490
+ status = 'success';
491
+ }
492
+ catch (err) {
493
+ agentError = createAgentServerError(err);
494
+ status = 'error';
495
+ }
496
+ const agentFinishedQueryAt = new Date().toISOString();
497
+ const updateQueryResponse = await this._retoolApi.postQueryResponse({
498
+ resourceId: this._resourceId,
499
+ environmentName: this._environmentName,
500
+ versionHash: this._versionHash,
501
+ agentUuid: this._agentUuid,
502
+ queryUuid,
503
+ status,
504
+ data: executionResponse,
505
+ metadata: {
506
+ packageLanguage: 'javascript',
507
+ packageVersion: RetoolRPCVersion,
508
+ agentReceivedQueryAt,
509
+ agentFinishedQueryAt,
510
+ parameters: executionArguments,
511
+ },
512
+ error: agentError,
513
+ });
514
+ this._logger.debug('Update query response status: ', updateQueryResponse.status, await updateQueryResponse.text());
515
+ }
516
+ return 'continue';
517
+ }
518
+ /**
519
+ * Export registerAgent as a protected method for testing purpose only. Lookup TestRetoolRPC for usage.
520
+ */
521
+ async protectedRegisterAgent() {
522
+ return this.registerAgent();
523
+ }
524
+ /**
525
+ * Export fetchQueryAndExecute as a protected method for testing purpose only. Lookup TestRetoolRPC for usage.
526
+ */
527
+ async protectedFetchQueryAndExecute() {
528
+ return this.fetchQueryAndExecute();
529
+ }
530
+ }
531
+
532
+ export { RetoolRPC as R };
@@ -1,3 +1,4 @@
1
+ import type { LoggerService } from './utils/logger';
1
2
  /**
2
3
  * Configuration options for the Retool RPC.
3
4
  */
@@ -21,6 +22,8 @@ export type RetoolRPCConfig = {
21
22
  agentUuid?: string;
22
23
  /** The optional log level. */
23
24
  logLevel?: 'debug' | 'info' | 'warn' | 'error';
25
+ /** The optional logger. */
26
+ logger?: LoggerService;
24
27
  };
25
28
  /** Represents the type of the argument. Right now we are supporting only string, boolean, number, dict, and json. */
26
29
  export type ArgumentType = 'string' | 'boolean' | 'number' | 'dict' | 'json';
@@ -3,13 +3,15 @@ type LogLevel = typeof LOG_LEVELS[number];
3
3
  type LoggerOptions = {
4
4
  logLevel?: LogLevel;
5
5
  };
6
- export declare class Logger {
6
+ type LogFn = (...messages: unknown[]) => void;
7
+ export type LoggerService = Record<LogLevel, LogFn>;
8
+ export declare class Logger implements LoggerService {
7
9
  private currentLogLevel;
8
10
  constructor(options: LoggerOptions);
9
11
  private shouldLog;
10
- debug(...messages: any[]): void;
11
- info(...messages: any[]): void;
12
- warn(...messages: any[]): void;
13
- error(...messages: any[]): void;
12
+ debug(...messages: unknown[]): void;
13
+ info(...messages: unknown[]): void;
14
+ warn(...messages: unknown[]): void;
15
+ error(...messages: unknown[]): void;
14
16
  }
15
17
  export {};
@@ -1,3 +1,3 @@
1
1
  import { AgentServerStatus } from '../types';
2
- import { Logger } from './logger';
3
- export declare function loopWithBackoff(pollingIntervalMs: number, logger: Logger, callback: () => Promise<AgentServerStatus>): Promise<AgentServerStatus>;
2
+ import type { LoggerService } from './logger';
3
+ export declare function loopWithBackoff(pollingIntervalMs: number, logger: LoggerService, callback: () => Promise<AgentServerStatus>): Promise<AgentServerStatus>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retoolrpc",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TypeScript package for Retool RPC",
5
5
  "exports": {
6
6
  ".": {
package/src/rpc.ts CHANGED
@@ -13,7 +13,7 @@ import { createAgentServerError, FunctionNotFoundError } from './utils/errors'
13
13
  import { parseFunctionArguments } from './utils/schema'
14
14
  import { isClientError } from './utils/helpers'
15
15
  import { loopWithBackoff } from './utils/polling'
16
- import { Logger } from './utils/logger'
16
+ import { Logger, LoggerService } from './utils/logger';
17
17
  import { RetoolAPI } from './utils/api'
18
18
  import { RetoolRPCVersion } from './version'
19
19
 
@@ -36,7 +36,7 @@ export class RetoolRPC {
36
36
  private _versionHash: string | undefined
37
37
  private _functions: Record<string, Omit<RegisterFunctionSpec<any>, 'name'>> = {}
38
38
  private _retoolApi: RetoolAPI
39
- private _logger: Logger
39
+ private _logger: LoggerService
40
40
 
41
41
  /**
42
42
  * Creates an instance of the RetoolRPC class.
@@ -53,7 +53,7 @@ export class RetoolRPC {
53
53
  this._agentUuid = config.agentUuid || uuidv4()
54
54
 
55
55
  this._retoolApi = new RetoolAPI({ hostUrl: this._hostUrl, apiKey: this._apiKey })
56
- this._logger = new Logger({ logLevel: config.logLevel })
56
+ this._logger = config.logger ?? new Logger({ logLevel: config.logLevel })
57
57
 
58
58
  this._logger.debug('Retool RPC Configuration', {
59
59
  apiKey: this._apiKey,
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { LoggerService } from './utils/logger';
2
+
1
3
  /**
2
4
  * Configuration options for the Retool RPC.
3
5
  */
@@ -21,6 +23,8 @@ export type RetoolRPCConfig = {
21
23
  agentUuid?: string
22
24
  /** The optional log level. */
23
25
  logLevel?: 'debug' | 'info' | 'warn' | 'error'
26
+ /** The optional logger. */
27
+ logger?: LoggerService
24
28
  }
25
29
 
26
30
  /** Represents the type of the argument. Right now we are supporting only string, boolean, number, dict, and json. */
@@ -12,7 +12,11 @@ type LoggerOptions = {
12
12
  logLevel?: LogLevel
13
13
  }
14
14
 
15
- export class Logger {
15
+ type LogFn = (...messages: unknown[]) => void
16
+
17
+ export type LoggerService = Record<LogLevel, LogFn>
18
+
19
+ export class Logger implements LoggerService {
16
20
  private currentLogLevel: LogLevel
17
21
 
18
22
  constructor(options: LoggerOptions) {
@@ -23,25 +27,25 @@ export class Logger {
23
27
  return LOG_LEVEL_RANKINGS[level] >= LOG_LEVEL_RANKINGS[this.currentLogLevel] && process.env.NODE_ENV !== 'test'
24
28
  }
25
29
 
26
- debug(...messages: any[]) {
30
+ debug(...messages: unknown[]) {
27
31
  if (this.shouldLog('debug')) {
28
32
  console.log(...messages)
29
33
  }
30
34
  }
31
35
 
32
- info(...messages: any[]) {
36
+ info(...messages: unknown[]) {
33
37
  if (this.shouldLog('info')) {
34
38
  console.log(...messages)
35
39
  }
36
40
  }
37
41
 
38
- warn(...messages: any[]) {
42
+ warn(...messages: unknown[]) {
39
43
  if (this.shouldLog('warn')) {
40
44
  console.log(...messages)
41
45
  }
42
46
  }
43
47
 
44
- error(...messages: any[]) {
48
+ error(...messages: unknown[]) {
45
49
  if (this.shouldLog('error')) {
46
50
  console.log(...messages)
47
51
  }
@@ -1,5 +1,5 @@
1
1
  import { AgentServerStatus } from '../types'
2
- import { Logger } from './logger'
2
+ import type { LoggerService } from './logger'
3
3
 
4
4
  const CONNECTION_ERROR_INITIAL_TIMEOUT_MS = 50
5
5
  const CONNECTION_ERROR_RETRY_MAX_MS = 1000 * 60 * 10 // 10 minutes
@@ -12,7 +12,7 @@ function sleep(ms: number): Promise<void> {
12
12
 
13
13
  export async function loopWithBackoff(
14
14
  pollingIntervalMs: number,
15
- logger: Logger,
15
+ logger: LoggerService,
16
16
  callback: () => Promise<AgentServerStatus>,
17
17
  ): Promise<AgentServerStatus> {
18
18
  let delayTimeMs = CONNECTION_ERROR_INITIAL_TIMEOUT_MS
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const RetoolRPCVersion = '0.1.0'
1
+ export const RetoolRPCVersion = '0.1.1'