Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/javascript-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ jobs:
uses: actions/cache@v3
with:
path: javascript/node_modules
key: 16.x-${{ runner.OS }}-build-${{ hashFiles('javascript/yarn.lock') }}
key: 18.x-${{ runner.OS }}-build-${{ hashFiles('javascript/yarn.lock') }}

- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '16.x'
node-version: '18.x'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
Expand All @@ -44,5 +44,5 @@ jobs:

- name: Test
run: |
yarn test
yarn test --run
working-directory: javascript
2 changes: 2 additions & 0 deletions javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Review Retool's [RPC documentation](https://docs.retool.com/docs/retool-rpc) before installing the JavaScript package.

Requires Node.js 18 or later (uses the runtime `fetch` API).

## Installation

You can use `npm`, `yarn`, or `pnpm` to install the package.
Expand Down
12 changes: 6 additions & 6 deletions javascript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "retoolrpc",
"version": "0.1.8",
"version": "0.2.0",
"description": "TypeScript package for Retool RPC",
"keywords": [],
"homepage": "https://github.com/tryretool/retoolrpc#readme",
Expand All @@ -12,6 +12,9 @@
"url": "git+https://github.com/tryretool/retoolrpc.git"
},
"license": "MIT",
"engines": {
"node": ">=18"
},
"main": "./dist/cjs/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
Expand All @@ -34,21 +37,18 @@
"test:api": "tsc --project tsconfig.json"
},
"dependencies": {
"abort-controller": "^3.0.0",
"node-fetch": "^2.6",
"ts-dedent": "^2.2.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^8.2.0",
"@types/fs-extra": "^11.0.1",
"@types/node": "15.12.1",
"@types/node-fetch": "^2.6.4",
"@types/node": "^18.19.0",
"@types/semver": "^7.5.1",
"@types/uuid": "^9.0.2",
"cross-env": "7.0.3",
"fs-extra": "^11.1.1",
"nock": "^13.3.2",
"nock": "^14.0.17",
"nodemon": "^2.0.15",
"prettier": "2.3.1",
"rollup": "^2.39.0",
Expand Down
13 changes: 11 additions & 2 deletions javascript/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export class RetoolRPC {
)
}

const { versionHash } = await registerAgentResponse.json()
const { versionHash } = (await registerAgentResponse.json()) as { versionHash: string }
this._versionHash = versionHash
this._logger.info(`Agent registered with versionHash: ${versionHash}`)

Expand All @@ -206,7 +206,16 @@ export class RetoolRPC {
throw new Error(`Server error when fetching query: ${pendingQueryFetch.status}. Retrying...`)
}

const { query } = await pendingQueryFetch.json()
const { query } = (await pendingQueryFetch.json()) as {
query: {
queryUuid: string
queryInfo: {
method: string
parameters: Record<string, unknown>
context: RetoolContext
}
} | null
}
if (query) {
this._logger.debug('Executing query', query) // This might contain sensitive information

Expand Down
94 changes: 94 additions & 0 deletions javascript/src/utils/api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import http from 'http'
import zlib from 'zlib'
import { afterEach, describe, expect, test, vi } from 'vitest'

import { RetoolAPI } from './api'

describe('RetoolAPI', () => {
afterEach(() => {
vi.restoreAllMocks()
})

test('registerAgent uses globalThis.fetch', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ versionHash: 'abc' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
)

const api = new RetoolAPI({
hostUrl: 'https://example.retool.com',
apiKey: 'token',
pollingTimeoutMs: 1000,
})

const response = await api.registerAgent({
resourceId: 'resource-id',
environmentName: 'production',
version: '0.0.1',
agentUuid: 'agent-uuid',
operations: {},
})

expect(fetchSpy).toHaveBeenCalledTimes(1)
expect(fetchSpy.mock.calls[0][0]).toBe('https://example.retool.com/api/v1/retoolrpc/registerAgent')
expect(response.ok).toBe(true)
await expect(response.json()).resolves.toEqual({ versionHash: 'abc' })
})

// Regression for RE-2830: node-fetch@2 throws FetchError Premature close on some Node 24
// gzip responses. Native fetch must consume a gzip registerAgent body cleanly.
test('registerAgent consumes a gzip Content-Encoding response body', async () => {
const payload = JSON.stringify({ versionHash: 'gzip-version-hash' })
const gzipBody = zlib.gzipSync(payload)

const server = http.createServer((req, res) => {
// Drain the request body before responding so fetch does not stall on an unread POST.
req.resume()
req.on('end', () => {
expect(req.method).toBe('POST')
expect(req.url).toBe('/api/v1/retoolrpc/registerAgent')
res.writeHead(200, {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
'Content-Length': String(gzipBody.length),
})
res.end(gzipBody)
})
})

await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => resolve())
})

const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('Expected TCP server address')
}

try {
const api = new RetoolAPI({
hostUrl: `http://127.0.0.1:${address.port}`,
apiKey: 'token',
pollingTimeoutMs: 1000,
})

const response = await api.registerAgent({
resourceId: 'resource-id',
environmentName: 'production',
version: '0.0.1',
agentUuid: 'agent-uuid',
operations: {},
})

expect(response.ok).toBe(true)
await expect(response.json()).resolves.toEqual({ versionHash: 'gzip-version-hash' })
} finally {
server.closeAllConnections()
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()))
})
}
Comment thread
kcheung-retool marked this conversation as resolved.
})
})
13 changes: 3 additions & 10 deletions javascript/src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import fetch, { RequestInit } from 'node-fetch'

import AbortControllerFallback from 'abort-controller'

// AbortController was added in node v14.17.0 globally, but we need to polyfill it for older versions
const AbortController = globalThis.AbortController || AbortControllerFallback

import { AgentServerError } from '../types'
import { RetoolRPCVersion } from '../version'

// Runtime global fetch. node-fetch@2 fails on some Node 24.x gzip responses with Premature close.
Comment thread
kcheung-retool marked this conversation as resolved.

type PopQueryRequest = {
resourceId: string
environmentName: string
Expand Down Expand Up @@ -67,9 +62,7 @@ export class RetoolAPI {
'User-Agent': `RetoolRPC/${RetoolRPCVersion} (Javascript)`,
},
body: JSON.stringify(options),
// Had to cast to RequestInit['signal'] because of a bug in the types
// https://github.com/jasonkuhrt/graphql-request/issues/481
signal: abortController.signal as RequestInit['signal'],
signal: abortController.signal,
})
} catch (error: any) {
if (abortController.signal.aborted) {
Expand Down
2 changes: 1 addition & 1 deletion javascript/src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const RetoolRPCVersion = '0.1.8'
export const RetoolRPCVersion = '0.2.0'
Loading
Loading