retoolrpc 0.1.3 → 0.1.5

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.
@@ -0,0 +1,534 @@
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.5';
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
+ return spec.implementation;
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
+ this.executeFunction(method, parameters, context)
494
+ .then((executionResult) => {
495
+ executionResponse = executionResult.result;
496
+ executionArguments = executionResult.arguments;
497
+ status = 'success';
498
+ })
499
+ .catch((err) => {
500
+ agentError = createAgentServerError(err);
501
+ status = 'error';
502
+ })
503
+ .finally(() => {
504
+ this._retoolApi
505
+ .postQueryResponse({
506
+ resourceId: this._resourceId,
507
+ environmentName: this._environmentName,
508
+ versionHash: this._versionHash,
509
+ agentUuid: this._agentUuid,
510
+ queryUuid,
511
+ status,
512
+ data: executionResponse,
513
+ metadata: {
514
+ packageLanguage: 'javascript',
515
+ packageVersion: RetoolRPCVersion,
516
+ agentReceivedQueryAt,
517
+ agentFinishedQueryAt: new Date().toISOString(),
518
+ parameters: executionArguments,
519
+ },
520
+ error: agentError,
521
+ })
522
+ .then(async (updateQueryResponse) => {
523
+ this._logger.debug('Update query response status: ', updateQueryResponse.status, await updateQueryResponse.text());
524
+ })
525
+ .catch((err) => {
526
+ this._logger.error(`Error updating query response: `, err);
527
+ });
528
+ });
529
+ }
530
+ return 'continue';
531
+ }
532
+ }
533
+
534
+ export { RetoolRPC as R };
package/dist/src/rpc.d.ts CHANGED
@@ -26,7 +26,7 @@ export declare class RetoolRPC {
26
26
  /**
27
27
  * Registers a Retool function with the specified function definition.
28
28
  */
29
- register<TArgs extends Arguments>(spec: RegisterFunctionSpec<TArgs>): void;
29
+ register<TArgs extends Arguments, TReturn>(spec: RegisterFunctionSpec<TArgs, TReturn>): RegisterFunctionSpec<TArgs, TReturn>['implementation'];
30
30
  /**
31
31
  * Executes a Retool function with the specified arguments and context.
32
32
  */
@@ -55,13 +55,13 @@ export type TransformedArguments<TArgs extends Arguments> = {
55
55
  [TArg in keyof TArgs]: TransformedArgument<TArgs[TArg]>;
56
56
  };
57
57
  /** Represents the specification for registering a Retool function. */
58
- export type RegisterFunctionSpec<TArgs extends Arguments> = {
58
+ export type RegisterFunctionSpec<TArgs extends Arguments, TReturn> = {
59
59
  /** The name of the function. */
60
60
  name: string;
61
61
  /** The arguments of the function. */
62
62
  arguments: Pick<TArgs, keyof TArgs>;
63
63
  /** The implementation of the function. */
64
- implementation: (args: TransformedArguments<TArgs>, context: RetoolContext) => Promise<any>;
64
+ implementation: (args: TransformedArguments<TArgs>, context: RetoolContext) => Promise<TReturn>;
65
65
  /** The permissions configuration for the function. */
66
66
  permissions?: {
67
67
  /** The list of group names that have permission to execute the function. */
@@ -1 +1 @@
1
- export declare const RetoolRPCVersion = "0.1.3";
1
+ export declare const RetoolRPCVersion = "0.1.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retoolrpc",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "TypeScript package for Retool RPC",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/tryretool/retoolrpc#readme",
@@ -12,10 +12,14 @@
12
12
  "url": "git+https://github.com/tryretool/retoolrpc.git"
13
13
  },
14
14
  "license": "MIT",
15
+ "main": "./dist/cjs/index.js",
16
+ "module": "./dist/index.mjs",
17
+ "types": "./dist/index.d.ts",
15
18
  "exports": {
16
19
  ".": {
17
20
  "import": "./dist/index.mjs",
18
- "require": "./dist/cjs/index.js"
21
+ "require": "./dist/cjs/index.js",
22
+ "types": "./dist/index.d.ts"
19
23
  },
20
24
  "./package.json": "./package.json"
21
25
  },
@@ -30,8 +34,6 @@
30
34
  "test:api": "tsc --project tsconfig.json"
31
35
  },
32
36
  "dependencies": {
33
- "@types/node-fetch": "^2.6.4",
34
- "@types/uuid": "^9.0.2",
35
37
  "abort-controller": "^3.0.0",
36
38
  "node-fetch": "^2.6",
37
39
  "ts-dedent": "^2.2.0",
@@ -41,7 +43,9 @@
41
43
  "@rollup/plugin-typescript": "^8.2.0",
42
44
  "@types/fs-extra": "^11.0.1",
43
45
  "@types/node": "15.12.1",
46
+ "@types/node-fetch": "^2.6.4",
44
47
  "@types/semver": "^7.5.1",
48
+ "@types/uuid": "^9.0.2",
45
49
  "cross-env": "7.0.3",
46
50
  "fs-extra": "^11.1.1",
47
51
  "nock": "^13.3.2",
package/src/rpc.spec.ts CHANGED
@@ -2,11 +2,11 @@ import fs from 'fs-extra'
2
2
  import nock from 'nock'
3
3
  import crypto from 'crypto'
4
4
  import { dedent } from 'ts-dedent'
5
- import { describe, expect, beforeEach, afterEach, test, vi } from 'vitest'
5
+ import { describe, expect, beforeEach, afterEach, test, vi, expectTypeOf } from 'vitest'
6
6
  import { v4 as uuidv4 } from 'uuid'
7
7
 
8
8
  import { RetoolRPC } from './rpc'
9
- import { Arguments } from './types'
9
+ import { Arguments, RetoolContext, TransformedArguments } from './types'
10
10
  import { parseFunctionArguments } from './utils/schema'
11
11
  import { RetoolRPCVersion } from './version'
12
12
 
@@ -751,6 +751,25 @@ describe('RetoolRPC', () => {
751
751
  )
752
752
  })
753
753
  })
754
+
755
+ test('returns the implementation when registering', async () => {
756
+ const fn = rpcAgent.register({
757
+ name: 'test',
758
+ arguments: {},
759
+ implementation: async () => {
760
+ return 1
761
+ }
762
+ })
763
+
764
+ type ExpectedImplementation = (args: TransformedArguments<Arguments>, context: RetoolContext) => Promise<number>
765
+ expectTypeOf(fn).toEqualTypeOf<ExpectedImplementation>()
766
+
767
+
768
+ const result = await fn({}, context);
769
+ expect(result).toEqual(1)
770
+ expectTypeOf(result).toEqualTypeOf(1)
771
+ })
772
+
754
773
  })
755
774
 
756
775
  describe('RetoolRPCVersion', () => {
package/src/rpc.ts CHANGED
@@ -36,7 +36,7 @@ export class RetoolRPC {
36
36
  private _version: string
37
37
  private _agentUuid: string
38
38
  private _versionHash: string | undefined
39
- private _functions: Record<string, Omit<RegisterFunctionSpec<any>, 'name'>> = {}
39
+ private _functions: Record<string, Omit<RegisterFunctionSpec<any, any>, 'name'>> = {}
40
40
  private _retoolApi: RetoolAPI
41
41
  private _logger: LoggerService
42
42
 
@@ -89,12 +89,13 @@ export class RetoolRPC {
89
89
  /**
90
90
  * Registers a Retool function with the specified function definition.
91
91
  */
92
- register<TArgs extends Arguments>(spec: RegisterFunctionSpec<TArgs>): void {
92
+ register<TArgs extends Arguments, TReturn>(spec: RegisterFunctionSpec<TArgs, TReturn>): RegisterFunctionSpec<TArgs, TReturn>['implementation'] {
93
93
  this._functions[spec.name] = {
94
94
  arguments: spec.arguments,
95
95
  permissions: spec.permissions,
96
96
  implementation: spec.implementation,
97
97
  }
98
+ return spec.implementation;
98
99
  }
99
100
 
100
101
  /**
@@ -145,7 +146,7 @@ export class RetoolRPC {
145
146
  * Registers the agent with the Retool server.
146
147
  */
147
148
  private async registerAgent(): Promise<AgentServerStatus> {
148
- const functionsMetadata: Record<string, Pick<RegisterFunctionSpec<any>, 'arguments' | 'permissions'>> = {}
149
+ const functionsMetadata: Record<string, Pick<RegisterFunctionSpec<any, any>, 'arguments' | 'permissions'>> = {}
149
150
  for (const functionName in this._functions) {
150
151
  functionsMetadata[functionName] = {
151
152
  arguments: this._functions[functionName].arguments,