TypeScript/JavaScript client library for creating, managing, and monitoring reports via the Riverscapes Reports GraphQL API.
| Requirement | Version | Notes |
|---|---|---|
| Node.js | ≥ 22 | Use node --version to check |
| npm | ≥ 9 | Bundled with Node.js |
| Internet access | — | Required for API calls and browser login |
No GraphQL experience required. The
ReportsAPIclass wraps every query and mutation so you call ordinary async methods. See the GraphQL Primer section if you want to understand what is happening under the hood.
git clone https://github.com/Riverscapes/rs-reports-scripts.git
cd rs-reports-scripts/typescriptnpm installnpm run buildThe TypeScript code follows the same formatting conventions as the rs-web-monorepo: no semicolons, single quotes, trailing commas (es5), 120-char line width.
npm run formatRun the interactive report-creation script against the staging environment:
npx tsx scripts/createReport.ts stagingOr using the package.json script:
npm run create-report -- stagingThe script will:
- Open a browser tab for you to log in with your Riverscapes account.
- Present an interactive menu to select a report type.
- Prompt for a report name, picker layer, and unit system.
- Create the report, attach inputs, start it, and poll until it finishes.
- Print a direct link to the finished report.
Use production instead of staging to target the live platform.
When you use ReportsAPI without providing machineAuth, it starts a
temporary local web server, opens your browser to the Riverscapes Auth0 login
page, and captures the authorization code automatically via the OAuth 2.0
PKCE flow.
import { ReportsAPI } from 'rs-reports'
const api = new ReportsAPI({ stage: 'production' })
await api.open()
try {
const profile = await api.getProfile()
console.log(profile.name)
} finally {
api.close()
}Port note: The callback server listens on port
4721by default. If that port is already in use, set the environment variableRSAPI_ALTPORT=1to use port4723instead.
For CI/CD or server-side scripts where no browser is available, pass
machineAuth with a client ID and secret issued by the Riverscapes team:
const api = new ReportsAPI({
stage: 'production',
machineAuth: {
clientId: 'YOUR_CLIENT_ID',
secretId: 'YOUR_CLIENT_SECRET',
},
})
await api.open()Keep your credentials out of source code — load them from environment variables:
const api = new ReportsAPI({
stage: 'production',
machineAuth: {
clientId: process.env.RS_CLIENT_ID!,
secretId: process.env.RS_CLIENT_SECRET!,
},
})Skip this section if you only want to run the scripts.
GraphQL is a query language for APIs created by Meta. Instead of many REST
endpoints (GET /reports, POST /reports/{id}/start, …), a GraphQL API
exposes a single endpoint (e.g. https://api.reports.riverscapes.net).
Every request is a POST with a JSON body containing a query string and an
optional variables object.
- Query files — GraphQL strings live in
src/graphql/so they are readable and reusable, not buried in template literals. runQuery()— handles setting theAuthorizationheader, JSON encoding, error parsing, and automatic token refresh.- Data classes — raw objects from the API are wrapped in
RSReportandRSReportType, giving you typed properties (report.status,report.isComplete()) instead of manual indexing.
The full schema is at src/graphql/rs-reports.schema.graphql. You can
also introspect the live API with any GraphQL client (e.g.
Altair, Insomnia) by
pointing it at the API URL and adding an Authorization: Bearer <token> header.
typescript/
├── package.json # Package metadata and dependencies
├── tsconfig.json # TypeScript config (type-checking, VS Code)
├── tsconfig.build.json # TypeScript config (build / emit)
├── .prettierrc.cjs # Prettier formatting rules
│
├── src/
│ ├── index.ts # Public exports
│ ├── ReportsAPI.ts # Main API client class
│ ├── reportsHelpers.ts # Data classes (RSReport, RSReportType) + utils
│ │
│ └── graphql/
│ ├── rs-reports.schema.graphql # Full API schema
│ ├── queries/ # Read-only GraphQL operations
│ └── mutations/ # Write GraphQL operations
│
└── scripts/
└── createReport.ts # Interactive CLI script
- GraphQL files are separate from TypeScript code. This keeps queries readable, allows editor syntax highlighting, and means you can copy-paste them directly into a GraphQL client for testing.
open()/close()— callawait api.open()to authenticate andapi.close()to clean up. Use atry/finallyblock.RSReportandRSReportTypeclasses — wrap raw API objects and add helper methods likeisComplete(),isRunning(), andisFailed().- Formatting matches the rs-web-monorepo conventions: no semicolons, single quotes, trailing commas (es5), 120 chars.
The main client class:
const api = new ReportsAPI({ stage: 'production' })
await api.open()
try {
// ...
} finally {
api.close()
}| Parameter | Type | Description |
|---|---|---|
stage |
string |
'production', 'staging', or 'local' |
machineAuth |
MachineAuth | undefined |
{ clientId, secretId } for non-browser auth |
devHeaders |
Record<string, string> | undefined |
Raw headers for local dev |
Returns the authenticated user's profile (id, name, email, etc.).
const profile = await api.getProfile()
console.log(profile.name)Returns all available report types.
for (const rt of await api.listReportTypes()) {
console.log(rt.id, rt.name, rt.version)
}Fetch a single report type by its UUID.
Returns a page of the current user's reports plus the total count.
const { reports, total } = await api.listReports(10, 0)
console.log(`Showing ${reports.length} of ${total}`)Yields every report for the current user, handling pagination automatically.
for await (const report of api.iterReports()) {
console.log(report.id, report.status)
}Fetch a single report by its UUID.
Admin method — returns reports across all users.
The typical lifecycle is: create → (attach inputs) → start → poll.
Creates a new report with status CREATED.
const report = await api.createReport({
name: 'My Watershed Report',
reportTypeId: '<uuid-of-report-type>',
parameters: { units: 'imperial' },
})Links a picker selection to the report.
Submits the report to the processing queue.
Cancels a running report.
Permanently deletes a report and its stored files from S3.
Blocks until the report reaches a terminal state, then returns the final RSReport.
const report = await api.pollReport(report.id!, 10)
if (report.isComplete()) {
console.log('Done!')
}Uploads a local file to the report's S3 storage with retry.
Returns raw pre-signed S3 PUT URLs.
Returns pre-signed S3 GET URLs for a report's files.
Downloads from a pre-signed URL to a local path.
| Status | Meaning |
|---|---|
CREATED |
Report exists but has not been started |
QUEUED |
Submitted, waiting for a processing slot |
RUNNING |
Currently being processed |
COMPLETE |
Finished successfully |
ERROR |
Processing failed — check statusMessage |
STOPPED |
Manually stopped by the user |
DELETED |
Report has been deleted |
| Property | Type | Description |
|---|---|---|
id |
string | undefined |
UUID |
name |
string | undefined |
Human-readable name |
description |
string | undefined |
Optional description |
status |
string | undefined |
See status values above |
statusMessage |
string | undefined |
Status detail |
progress |
number |
0–100 percentage |
parameters |
Record | undefined |
Input parameters |
outputs |
unknown[] |
Output file metadata |
extent |
Record | undefined |
GeoJSON geometry |
centroid |
Record | undefined |
GeoJSON point |
createdAt |
Date | null |
Creation timestamp |
updatedAt |
Date | null |
Last-updated timestamp |
reportType |
RSReportType | null |
Embedded report type info |
createdById |
string | undefined |
Owner user ID |
createdByName |
string | undefined |
Owner display name |
Helper methods: isComplete(), isRunning(), isFailed()
import { ReportsAPI } from 'rs-reports'
const api = new ReportsAPI({ stage: 'production' })
await api.open()
try {
// 1. Pick a report type
const reportTypes = await api.listReportTypes()
const rt = reportTypes.find((r) => r.shortName === 'watershed-summary')!
// 2. Create the report
let report = await api.createReport({
name: 'My Test Report',
reportTypeId: rt.id!,
parameters: { units: 'imperial' },
})
// 3. Attach a picker selection
await api.attachPickerOption(report.id!, 'huc', '1302020710')
// 4. Start it
report = await api.startReport(report.id!)
// 5. Wait for completion
report = await api.pollReport(report.id!, 10)
if (report.isComplete()) {
console.log('Report complete!')
const urls = await api.getDownloadUrls(report.id!, ['OUTPUTS'])
for (const item of urls) {
await api.downloadFile(item.url, `/tmp/${(item as any).filePath}`)
}
} else {
console.log(`Report failed: ${report.statusMessage}`)
}
} finally {
api.close()
}const result = await api.runQuery(
`query MyCustomQuery($reportId: ID!) {
report(reportId: $reportId) {
id
status
}
}`,
{ reportId: 'YOUR-REPORT-UUID' }
)
console.log((result as any).data.report)Set the environment variable to switch to the alternate port:
export RSAPI_ALTPORT=1
npx tsx scripts/createReport.ts stagingYour token expired and the library failed to refresh it. Try running the script again — a fresh browser login will be triggered automatically.
Make sure you've run npm install and npm run build in this directory.
ReportsAPIException includes the raw errors array from the API in its
message. The most useful fields are message and extensions.code.
Use the NODE_DEBUG=http environment variable or add console logging to your script.