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
16 changes: 16 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 2.1
# Build and test on CircleCI (as the other QRun-IO repos); GitHub Actions also
# runs .github/workflows/ci.yml and publishes on version tags.
jobs:
build:
docker:
- image: cimg/node:22.12
steps:
- checkout
- run: npm ci
- run: npm run build
- run: npm test
workflows:
build:
jobs:
- build
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Generated client is up to date with the vendored document
run: |
npm run generate
git diff --exit-code packages/ui-api-client/src/schema.ts
- run: npm run build
- run: npm test
- name: Mock server smoke test (Prism on the merged document)
run: |
npx prism mock --port 4010 packages/ui-api-client/openapi/qqq-v1.json > prism.log 2>&1 &
for i in $(seq 1 30); do curl -fsS http://127.0.0.1:4010/qqq/v1/version && break; sleep 1; done
curl -fsS http://127.0.0.1:4010/qqq/v1/metaData/authentication
33 changes: 33 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: publish
# Publishes the three packages to GitHub Packages (npm.pkg.github.com, scope @qrun-io)
# when a version tag (v*) is pushed. The tag must match the client package version.
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://npm.pkg.github.com
scope: '@qrun-io'
- run: npm ci
- run: npm run build
- run: npm test
- name: Tag matches the client version
run: test "v$(node -p "require('./packages/ui-api-client/package.json').version")" = "${GITHUB_REF_NAME}"
- run: npm publish -w @qrun-io/qqq-ui-api-client --tag rc
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npm publish -w @qrun-io/qqq-ui-api-conformance --tag rc
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npm publish -w @qrun-io/qqq-cli --tag rc
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
*.tgz
.env
.spec-src/
137 changes: 137 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# qqq-cli

The reference command-line client for [QQQ](https://github.com/QRun-IO/qqq) applications, and the kit for writing your own CLI, TUI or native client. Everything here talks to a QQQ server only through the **UI API** (`/qqq/v1`), the API the web UI uses too.

| Package | What it is |
|---|---|
| [`@qrun-io/qqq-cli`](packages/cli) | `qqq`, the reference CLI: device-code sign-in, apps, tables, filtered queries, records, processes, jobs, and a live event stream |
| [`@qrun-io/qqq-ui-api-client`](packages/ui-api-client) | A typed TypeScript client, generated from the merged UI API document (core plus extensions). It also does OAuth2 device-flow sign-in, token refresh, and reads the realtime (SSE) stream |
| [`@qrun-io/qqq-ui-api-conformance`](packages/conformance) | A black-box conformance suite you can run against any server, plus `qqq-dev-idp`, a local OIDC provider for testing native clients |

The packages are published to GitHub Packages:

```sh
echo "@qrun-io:registry=https://npm.pkg.github.com" >> ~/.npmrc
npm install -g @qrun-io/qqq-cli@rc
```

Requires Node.js 20 or later and a QQQ server with UI API 1.6.0 or later (QQQ 4.1.0-RC.15 or later).

## The CLI

```console
$ qqq login --server https://admin.example.com
To sign in to https://admin.example.com, open:

https://auth.example.com/device?code=ED84-CC03

and confirm the code ED84-CC03 (it expires in 10 minutes).
Waiting for approval...
Signed in to https://admin.example.com as darin@example.com.

$ qqq tables --app peopleApp
name label insert edit delete
------- -------- ------ ---- ------
person Person yes yes yes
pet Pet yes yes yes

$ qqq query person --where "firstName ~ a" --where "id < 100" --order -id --limit 5
Id First Name Last Name Annual Salary ...
5 Morgan Sample $1,500,000.00
3 Casey Sample $300,000.00
(2 records)

$ qqq get person 1
$ qqq run greetInteractive --records 1,2 --value greetingPrefix=Hi
$ qqq jobs --all
$ qqq tail --channel table:person
```

| Command | UI API operations |
|---|---|
| `login` | `getAuthenticationMetaData`, then the provider's device-authorization grant with `values.nativeClientId` |
| `whoami` | `getVersion`, `getMetaData` |
| `apps`, `tables [--app]` | `getMetaData` |
| `query <table> [--where "field OP value"]... [--or] [--order [-]field] [--limit n] [--fields a,b]` | `getTableMetaData`, `queryTable` |
| `get <table> <pk>` | `getTableMetaData`, `getRecord` |
| `run <process> [--value k=v]... [--records ids]` | `getProcessMetaData`, `initProcess`, `stepProcess`, `getProcessStatus` |
| `jobs [--all]` | `listJobs` |
| `tail [--channel ...]` | `openRealtimeStream`, `updateRealtimeSubscriptions` (`user:me`, plus your channels) |

Add `--json` for machine-readable output. Errors print the server's `message`, `code` and `traceId`.

Where things are kept:
- **Tokens** go in `~/.config/qqq-cli/credentials.json` (mode 0600), one entry per server. The CLI refreshes the access token at the provider before it expires, and once more on a 401.
- **`QQQ_TOKEN`** overrides the stored token with any bearer token, such as a QQQ access token exchanged for a JWT, or a session UUID.
- **`QQQ_SERVER`** sets the default server.

Every request carries `X-QQQ-Client: cli/qqq-cli <version>`, so the server's audits attribute it to the CLI.

## Writing a client

```ts
import { createUiApiClient, unwrap, getNativeSignInConfig, deviceSignIn } from '@qrun-io/qqq-ui-api-client'

const config = await getNativeSignInConfig('https://admin.example.com')
const tokens = await deviceSignIn(config, (p) => console.log(`Open ${p.verificationUriComplete}`))
const api = createUiApiClient({ baseUrl: 'https://admin.example.com', token: () => tokens.accessToken, clientChannel: 'tui/my-tui 1.0' })

const page = await unwrap(api.POST('/qqq/v1/table/{tableName}/query', {
params: { path: { tableName: 'person' } },
body: { filter: { criteria: [{ fieldName: 'lastName', operator: 'STARTS_WITH', values: ['K'] }], limit: 20 } },
}))
```

Paths, parameters, bodies and responses are typed from the document. Read the QQQ docs before you write a client:
- [`UI-API-CLIENTS.md`](https://github.com/QRun-IO/qqq/blob/develop/docs/UI-API-CLIENTS.md) covers authentication, errors, widgets, processes, capabilities and validation.
- [`UI-API-CHANGELOG.md`](https://github.com/QRun-IO/qqq/blob/develop/docs/UI-API-CHANGELOG.md) covers versioning.

## Mock server

`npm run mock` serves the merged document with [Prism](https://github.com/stoplightio/prism) on http://127.0.0.1:4010, so you can build a client without a server. Responses come from the document's examples and schemas.

## Conformance suite

```sh
qqq-ui-api-conformance --server https://admin.example.com # public checks only
qqq-ui-api-conformance --server https://admin.example.com --token "$T" # all read-only checks
```

The suite only reads. It never writes. It checks that:
- the version and the served document agree;
- every operation has a unique operationId and every `$ref` resolves;
- every extension the server lists has operations;
- requests without credentials or with forged tokens (`alg: none`, garbage) get a 401 with the error schema;
- metadata, table metadata (with `clientCapabilities`), query, count, record, jobs and realtime responses validate against the document;
- a 404 has the error schema, and `X-Request-Id` comes back as `traceId`.

## Testing a client locally

`qqq-dev-idp` is a small OIDC provider for **local development only**: device grant, refresh and RS256 JWTs, with one issuer per client, as Authentik does it. Run it with the QQQ sample app:

```sh
npx qqq-dev-idp --port 9400 --client sample-app --client sample-cli --user you@example.com --groups admins --auto-approve

# in a qqq checkout (QQQ 4.1.0-RC.15 or later)
export OAUTH2_BASE_URL=http://localhost:9400/application/o/sample-app/ OAUTH2_CLIENT_ID=sample-app OAUTH2_CLIENT_SECRET=dev \
OAUTH2_SCOPES="openid profile email offline_access" \
OAUTH2_NATIVE_CLIENT_ID=sample-cli OAUTH2_NATIVE_CLIENT_BASE_URL=http://localhost:9400/application/o/sample-cli/
./mvnw -f qqq-sample-project/pom.xml compile exec:java -Dexec.mainClass=com.kingsrook.sampleapp.SampleJavalinServer -Dqqq.sample.port=8077

qqq login --server http://localhost:8077
```

## Development

```sh
npm ci
npm run spec:merge -- <core openapi.yaml> <extension yaml>... # or a server's /qqq/v1/openapi.json
npm run generate # regenerate packages/ui-api-client/src/schema.ts
npm run build && npm test
```

Releases: push a tag `v<client version>`. The `publish` workflow publishes the three packages to GitHub Packages.

## License

Apache-2.0
Loading
Loading