feat(ui-api): cap the off-spec calls, vendor the ESB extension, CODEOWNERS (#107)
- OFF_SPEC_CEILING (45) caps OFF_SPEC_CALLS; the contract test fails if it grows. - Vendor the qqq-esb extension document (extensions/esb.json, 1.0.0): the five ESB calls are now spec'd and leave the off-spec list. - CODEOWNERS on src/lib/api/openapi/; CONTRIBUTING documents the merge order (extension, then vendored document, then UI). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
35249098bb
commit
4376c28896
5 files changed
+773
-36
No files matched your search
@@ -1,2 +1,5 @@
|
||||
# Default reviewers for every change.
|
||||
* @KofTwentyTwo
|
||||
|
||||
# The UI API contract (QRun-IO/qqq-frontend-next#107): the vendored documents and the off-spec list
|
||||
/src/lib/api/openapi/ @KofTwentyTwo
|
||||
+48
-34
@@ -1,37 +1,51 @@
|
||||
# Contributing
|
||||
# Contributing
|
||||
|
||||
## Branching: trunk-based on `main`
|
||||
|
||||
`main` is the only long-lived branch. There is no `develop`, `release/*` or `hotfix/*` branch.
|
||||
|
||||
1. Branch from the current `main` (`feature/...`, `fix/...`, `docs/...`, `chore/...`).
|
||||
2. Keep the branch current by merging `main` into it; do not rebase or force-push a branch
|
||||
that is under review.
|
||||
3. Open a pull request into `main`. Every commit must be signed.
|
||||
4. A pull request merges when the required checks pass and the code owner approves.
|
||||
Head branches are deleted automatically after the merge.
|
||||
|
||||
### Required checks
|
||||
|
||||
| Check | Source |
|
||||
|-------|--------|
|
||||
| `Typecheck, lint, unit tests and coverage` | `.github/workflows/ci.yml` |
|
||||
| `Mocked e2e (Playwright, chromium)` | `.github/workflows/ci.yml` |
|
||||
| `Bundle budget (static export)` | `.github/workflows/ci.yml` |
|
||||
| `Storybook build` | `.github/workflows/ci.yml` |
|
||||
| `ci/circleci: qqq-orb/node_app_test_only` | `.circleci/config.yml` |
|
||||
|
||||
The real-backend acceptance matrix (`.github/workflows/acceptance.yml`) runs on every pull request
|
||||
and on `main`. Treat a red row as a release blocker and investigate it; never re-run it until it passes.
|
||||
|
||||
## Releases
|
||||
|
||||
A release is a signed `v<version>` tag on `main` (for example `v1.0.0-RC.14`), where `package.json`
|
||||
and `pom.xml` carry that version. The tag publishes the dashboard jar to Maven Central (CircleCI
|
||||
`publish_dashboard_jar`) and the dashboard container image (`.github/workflows/dashboard-image.yml`).
|
||||
Release notes live in `docs/releases/`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Dependabot opens grouped weekly updates for npm, GitHub Actions and Maven (`.github/dependabot.yml`),
|
||||
plus security updates as they are published.
|
||||
|
||||
## Branching: trunk-based on `main`
|
||||
## UI API calls (merge order)
|
||||
Next calls the backend only through operations described in the vendored UI API documents:
|
||||
`src/lib/api/openapi/qqq-v1.json`, plus one JSON file per extension in `extensions/`.
|
||||
`ui-api-contract.test.ts` fails on any other call.
|
||||
|
||||
`main` is the only long-lived branch. There is no `develop`, `release/*` or `hotfix/*` branch.
|
||||
A feature that needs new backend calls lands in this order:
|
||||
1. The qbit or module ships the operations as a UI API extension, with spec classes and its
|
||||
committed `openapi/qqq-v1/<name>.yaml`.
|
||||
2. That document is vendored here, as `extensions/<name>.json` or a re-vendored `qqq-v1.json`.
|
||||
3. The UI PR that calls them.
|
||||
|
||||
1. Branch from the current `main` (`feature/...`, `fix/...`, `docs/...`, `chore/...`).
|
||||
2. Keep the branch current by merging `main` into it; do not rebase or force-push a branch
|
||||
that is under review.
|
||||
3. Open a pull request into `main`. Every commit must be signed.
|
||||
4. A pull request merges when the required checks pass and the code owner approves.
|
||||
Head branches are deleted automatically after the merge.
|
||||
|
||||
### Required checks
|
||||
|
||||
| Check | Source |
|
||||
|-------|--------|
|
||||
| `Typecheck, lint, unit tests and coverage` | `.github/workflows/ci.yml` |
|
||||
| `Mocked e2e (Playwright, chromium)` | `.github/workflows/ci.yml` |
|
||||
| `Bundle budget (static export)` | `.github/workflows/ci.yml` |
|
||||
| `Storybook build` | `.github/workflows/ci.yml` |
|
||||
| `ci/circleci: qqq-orb/node_app_test_only` | `.circleci/config.yml` |
|
||||
|
||||
The real-backend acceptance matrix (`.github/workflows/acceptance.yml`) runs on every pull request
|
||||
and on `main`. Treat a red row as a release blocker and investigate it; never re-run it until it passes.
|
||||
|
||||
## Releases
|
||||
|
||||
A release is a signed `v<version>` tag on `main` (for example `v1.0.0-RC.14`), where `package.json`
|
||||
and `pom.xml` carry that version. The tag publishes the dashboard jar to Maven Central (CircleCI
|
||||
`publish_dashboard_jar`) and the dashboard container image (`.github/workflows/dashboard-image.yml`).
|
||||
Release notes live in `docs/releases/`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Dependabot opens grouped weekly updates for npm, GitHub Actions and Maven (`.github/dependabot.yml`),
|
||||
plus security updates as they are published.
|
||||
`OFF_SPEC_CALLS` in `ui-api-calls.ts` is a shrinking list of calls that predate the extension point.
|
||||
`OFF_SPEC_CEILING` caps it: lower the ceiling as entries move onto specs, and never raise it.
|
||||
@@ -0,0 +1,709 @@
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"EsbCapabilities": {
|
||||
"description": "What the provider supports for this destination - show only the actions that are true.",
|
||||
"properties": {
|
||||
"browse": {
|
||||
"description": "Messages can be browsed.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteOlderThan": {
|
||||
"description": "Messages older than a time can be deleted.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteSelected": {
|
||||
"description": "Selected messages can be deleted.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"move": {
|
||||
"description": "Messages can be moved to another destination.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"pauseQueue": {
|
||||
"description": "The queue can be paused and resumed.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"purge": {
|
||||
"description": "All messages can be deleted.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbCounter": {
|
||||
"description": "Activity counters since the server started.",
|
||||
"properties": {
|
||||
"avgMs": {
|
||||
"description": "Average processing time in milliseconds.",
|
||||
"type": "integer"
|
||||
},
|
||||
"consumed": {
|
||||
"description": "Messages consumed.",
|
||||
"type": "integer"
|
||||
},
|
||||
"deadLettered": {
|
||||
"description": "Messages moved to the dead-letter destination.",
|
||||
"type": "integer"
|
||||
},
|
||||
"failed": {
|
||||
"description": "Consumed messages that failed.",
|
||||
"type": "integer"
|
||||
},
|
||||
"inFlight": {
|
||||
"description": "Messages being processed now.",
|
||||
"type": "integer"
|
||||
},
|
||||
"lastActivity": {
|
||||
"description": "Time of the last activity (ISO-8601), or null.",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"lastError": {
|
||||
"description": "The last error message, or null.",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"maxMs": {
|
||||
"description": "Longest processing time in milliseconds.",
|
||||
"type": "integer"
|
||||
},
|
||||
"publishFailures": {
|
||||
"description": "Publish attempts that failed.",
|
||||
"type": "integer"
|
||||
},
|
||||
"published": {
|
||||
"description": "Messages published.",
|
||||
"type": "integer"
|
||||
},
|
||||
"retried": {
|
||||
"description": "Redeliveries.",
|
||||
"type": "integer"
|
||||
},
|
||||
"succeeded": {
|
||||
"description": "Consumed messages processed successfully.",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbDeadLetter": {
|
||||
"description": "A dead-letter (or subscription) destination of a trigger.",
|
||||
"properties": {
|
||||
"brokerName": {
|
||||
"description": "Name on the broker.",
|
||||
"type": "string"
|
||||
},
|
||||
"messageCount": {
|
||||
"description": "Messages waiting, or null when unknown.",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"paused": {
|
||||
"description": "Whether it is paused, or null when unknown.",
|
||||
"nullable": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbDestination": {
|
||||
"description": "A queue or topic.",
|
||||
"properties": {
|
||||
"brokerName": {
|
||||
"description": "Name of the destination on the broker.",
|
||||
"type": "string"
|
||||
},
|
||||
"capabilities": {
|
||||
"$ref": "#/components/schemas/EsbCapabilities"
|
||||
},
|
||||
"counters": {
|
||||
"$ref": "#/components/schemas/EsbCounter"
|
||||
},
|
||||
"name": {
|
||||
"description": "Destination name.",
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"description": "Name of the provider (broker connection).",
|
||||
"type": "string"
|
||||
},
|
||||
"queueInfo": {
|
||||
"$ref": "#/components/schemas/EsbQueueInfo",
|
||||
"nullable": true
|
||||
},
|
||||
"type": {
|
||||
"description": "Destination type.",
|
||||
"enum": [
|
||||
"QUEUE",
|
||||
"TOPIC"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbMessage": {
|
||||
"description": "A message on a destination.",
|
||||
"properties": {
|
||||
"deliveryCount": {
|
||||
"description": "Delivery attempts so far.",
|
||||
"type": "integer"
|
||||
},
|
||||
"event": {
|
||||
"additionalProperties": true,
|
||||
"description": "The CloudEvents envelope (specversion, id, source, type, time, subject, datacontenttype, data, qqqcausationid), or null when the body is not one.",
|
||||
"nullable": true,
|
||||
"type": "object"
|
||||
},
|
||||
"messageId": {
|
||||
"description": "Broker message id.",
|
||||
"type": "string"
|
||||
},
|
||||
"properties": {
|
||||
"additionalProperties": true,
|
||||
"description": "Message properties.",
|
||||
"type": "object"
|
||||
},
|
||||
"rawBody": {
|
||||
"description": "The message body as text.",
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"description": "When it was sent (ISO-8601).",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbMessagePageResponseV1": {
|
||||
"description": "A page of messages.",
|
||||
"properties": {
|
||||
"hasMore": {
|
||||
"description": "Whether more messages follow this page.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"messages": {
|
||||
"description": "The messages, oldest first.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbMessage"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbOverviewDestination": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EsbDestination"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"publishers": {
|
||||
"description": "Tables and processes publishing to it.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbPublisher"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"triggers": {
|
||||
"description": "Triggers consuming it.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbTrigger"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"description": "A destination with its publishers and triggers."
|
||||
},
|
||||
"EsbOverviewResponseV1": {
|
||||
"description": "GET /esb/overview: providers and every destination.",
|
||||
"properties": {
|
||||
"destinations": {
|
||||
"description": "Destinations with their publishers and triggers.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbOverviewDestination"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/components/schemas/EsbPermissions"
|
||||
},
|
||||
"providers": {
|
||||
"description": "Broker connections.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbProvider"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbPermissions": {
|
||||
"description": "What the user may do.",
|
||||
"properties": {
|
||||
"canDelete": {
|
||||
"description": "May delete messages.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"canOperate": {
|
||||
"description": "May pause, resume and retry.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbProcessResponseV1": {
|
||||
"description": "GET /esb/process/{process}: what a process publishes, and the triggers that run it.",
|
||||
"properties": {
|
||||
"permissions": {
|
||||
"$ref": "#/components/schemas/EsbPermissions"
|
||||
},
|
||||
"process": {
|
||||
"description": "Process name.",
|
||||
"type": "string"
|
||||
},
|
||||
"publications": {
|
||||
"description": "Destinations the process publishes to.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbPublication"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"triggers": {
|
||||
"description": "Triggers that run the process.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbTrigger"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbProvider": {
|
||||
"description": "A broker connection.",
|
||||
"properties": {
|
||||
"connected": {
|
||||
"description": "Whether the connection is up.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"managementEnabled": {
|
||||
"description": "Whether broker management (browse, pause, purge) is available.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "Provider name.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Broker type.",
|
||||
"enum": [
|
||||
"ACTIVEMQ_ARTEMIS",
|
||||
"RABBITMQ"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbPublication": {
|
||||
"description": "A destination that a table or process publishes events to.",
|
||||
"properties": {
|
||||
"destination": {
|
||||
"$ref": "#/components/schemas/EsbDestination"
|
||||
},
|
||||
"events": {
|
||||
"description": "Events published: INSERT, UPDATE or DELETE for tables; STARTED, COMPLETED or FAILED for processes.",
|
||||
"items": {
|
||||
"description": "Event name.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbPublisher": {
|
||||
"description": "A table or process that publishes to a destination.",
|
||||
"properties": {
|
||||
"events": {
|
||||
"description": "Events published.",
|
||||
"items": {
|
||||
"description": "Event name.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"kind": {
|
||||
"description": "What publishes.",
|
||||
"enum": [
|
||||
"TABLE",
|
||||
"PROCESS"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Table or process name.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbQueueInfo": {
|
||||
"description": "Broker-reported state of a queue.",
|
||||
"properties": {
|
||||
"consumerCount": {
|
||||
"description": "Connected consumers.",
|
||||
"type": "integer"
|
||||
},
|
||||
"messageCount": {
|
||||
"description": "Messages waiting.",
|
||||
"type": "integer"
|
||||
},
|
||||
"paused": {
|
||||
"description": "Whether the queue is paused.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbTableResponseV1": {
|
||||
"description": "GET /esb/table/{table}: what a table publishes, and who subscribes.",
|
||||
"properties": {
|
||||
"permissions": {
|
||||
"$ref": "#/components/schemas/EsbPermissions"
|
||||
},
|
||||
"publications": {
|
||||
"description": "Destinations the table publishes to.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbPublication"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"subscribers": {
|
||||
"description": "Triggers subscribed to those destinations (on processes the user can access).",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EsbTrigger"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"table": {
|
||||
"description": "Table name.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"EsbTrigger": {
|
||||
"description": "A process subscribed to a destination.",
|
||||
"properties": {
|
||||
"concurrency": {
|
||||
"description": "Consumers run in parallel.",
|
||||
"type": "integer"
|
||||
},
|
||||
"counters": {
|
||||
"$ref": "#/components/schemas/EsbCounter"
|
||||
},
|
||||
"deadLetter": {
|
||||
"$ref": "#/components/schemas/EsbDeadLetter"
|
||||
},
|
||||
"destination": {
|
||||
"$ref": "#/components/schemas/EsbDestination"
|
||||
},
|
||||
"maxAttempts": {
|
||||
"description": "Attempts before a message is dead-lettered.",
|
||||
"type": "integer"
|
||||
},
|
||||
"mode": {
|
||||
"description": "SINGLE runs the process per message; BATCH per batch of messages.",
|
||||
"enum": [
|
||||
"SINGLE",
|
||||
"BATCH"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Trigger name.",
|
||||
"type": "string"
|
||||
},
|
||||
"processLabel": {
|
||||
"description": "User-facing label of the process.",
|
||||
"type": "string"
|
||||
},
|
||||
"processName": {
|
||||
"description": "Process the trigger runs.",
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"description": "Current state of the trigger.",
|
||||
"enum": [
|
||||
"RUNNING",
|
||||
"PAUSED",
|
||||
"CONNECTING",
|
||||
"STOPPED"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"subscription": {
|
||||
"$ref": "#/components/schemas/EsbDeadLetter",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"sessionUuidCookie": {
|
||||
"in": "cookie",
|
||||
"name": "sessionUUID",
|
||||
"type": "apiKey"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"contact": {
|
||||
"email": "contact@kingsrook.com"
|
||||
},
|
||||
"description": "ESB overview, the ESB of tables and processes, and messages and dead letters on destinations.",
|
||||
"title": "QQQ Middleware API extension: esb",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"openapi": "3.0.3",
|
||||
"paths": {
|
||||
"/qqq/v1/esb/deadLetters/{trigger}": {
|
||||
"get": {
|
||||
"description": "A page of the messages on a trigger dead-letter destination, oldest first. Needs the operate permission.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Trigger name.",
|
||||
"in": "path",
|
||||
"name": "trigger",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Messages to skip (default 0).",
|
||||
"in": "query",
|
||||
"name": "offset",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Messages to return (default and maximum set by the server).",
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EsbMessagePageResponseV1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionUuidCookie": [
|
||||
"N/A"
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": "Dead letters of a trigger",
|
||||
"tags": [
|
||||
"ESB"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/qqq/v1/esb/messages/{destination}": {
|
||||
"get": {
|
||||
"description": "A page of the messages waiting on a destination (or, with trigger, on that trigger subscription of a topic), oldest first. Needs the operate permission.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Destination name.",
|
||||
"in": "path",
|
||||
"name": "destination",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "For a topic: the trigger whose subscription to browse.",
|
||||
"in": "query",
|
||||
"name": "trigger",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Messages to skip (default 0).",
|
||||
"in": "query",
|
||||
"name": "offset",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Messages to return (default and maximum set by the server).",
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EsbMessagePageResponseV1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionUuidCookie": [
|
||||
"N/A"
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": "Messages on a destination",
|
||||
"tags": [
|
||||
"ESB"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/qqq/v1/esb/overview": {
|
||||
"get": {
|
||||
"description": "Every broker connection and destination, with the tables and processes publishing to each and the triggers consuming it, and what the user may do. Needs the ESB app permission.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EsbOverviewResponseV1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionUuidCookie": [
|
||||
"N/A"
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": "ESB overview",
|
||||
"tags": [
|
||||
"ESB"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/qqq/v1/esb/process/{process}": {
|
||||
"get": {
|
||||
"description": "The destinations a process publishes its lifecycle events to, the triggers that run it, and what the user may do. Needs access to the process.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Process name.",
|
||||
"in": "path",
|
||||
"name": "process",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EsbProcessResponseV1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionUuidCookie": [
|
||||
"N/A"
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": "ESB of a process",
|
||||
"tags": [
|
||||
"ESB"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/qqq/v1/esb/table/{table}": {
|
||||
"get": {
|
||||
"description": "The destinations a table publishes its events to, the triggers subscribed to them (on processes the user can access), and what the user may do. Needs READ on the table.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Table name.",
|
||||
"in": "path",
|
||||
"name": "table",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EsbTableResponseV1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sessionUuidCookie": [
|
||||
"N/A"
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": "ESB of a table",
|
||||
"tags": [
|
||||
"ESB"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,6 @@ export const OFF_SPEC_CALLS: OffSpecCall[] = [
|
||||
'GET /assistant/turns/{}/events', 'POST /assistant/turns/{}/cancel', 'POST /assistant/messages/{}/feedback',
|
||||
'GET /assistant/actions/{}', 'POST /assistant/actions/{}/confirm', 'POST /assistant/actions/{}/reject',
|
||||
].map((c) => call(c, 'qbit-assistant'))),
|
||||
...(['GET /esb/overview', 'GET /esb/table/{}', 'GET /esb/process/{}', 'GET /esb/messages/{}', 'GET /esb/deadLetters/{}'].map((c) => call(c, 'qqq-esb'))),
|
||||
...([
|
||||
'GET /sql-workbench/capabilities', 'GET /sql-workbench/schema/{}', 'POST /sql-workbench/execute', 'POST /sql-workbench/explain',
|
||||
'GET /sql-workbench/executions/{}/rows', 'POST /sql-workbench/executions/{}/cancel', 'GET /sql-workbench/executions/{}/export',
|
||||
@@ -60,6 +59,14 @@ export const OFF_SPEC_CALLS: OffSpecCall[] = [
|
||||
call('GET {}', 'qqq-middleware-api table spec URL'),
|
||||
]
|
||||
|
||||
/**
|
||||
* The most off-spec calls allowed (QRun-IO/qqq-frontend-next#107). Lower it whenever an owner moves
|
||||
* calls onto spec'd operations; never raise it. A feature that needs new backend calls lands in this
|
||||
* order: the qbit or module ships its UI API extension, its document is vendored into
|
||||
* `extensions/`, then the UI calls it - so new calls never need this list.
|
||||
*/
|
||||
export const OFF_SPEC_CEILING = 45
|
||||
|
||||
/**
|
||||
* Call sites whose path is computed rather than written as a literal: `file#expression` (the
|
||||
* first argument as written) to the calls it can make.
|
||||
|
||||
@@ -26,7 +26,7 @@ import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { SUPPORTED_UI_API, parseSemver } from '../ui-api-version'
|
||||
import { DYNAMIC_CALL_SITES, OFF_SPEC_CALLS, PATH_HELPERS } from './ui-api-calls'
|
||||
import { DYNAMIC_CALL_SITES, OFF_SPEC_CALLS, OFF_SPEC_CEILING, PATH_HELPERS } from './ui-api-calls'
|
||||
|
||||
const SRC = path.resolve(__dirname, '../../..')
|
||||
const OPENAPI_DIR = __dirname
|
||||
@@ -191,6 +191,10 @@ describe('UI API contract', () => {
|
||||
expect(Object.keys(DYNAMIC_CALL_SITES).filter((key) => !dynamicSites.has(key))).toEqual([])
|
||||
})
|
||||
|
||||
it('never grows the off-spec list (ship the extension and vendor its document instead)', () => {
|
||||
expect(OFF_SPEC_CALLS.length, 'OFF_SPEC_CALLS may only shrink: add the operation to the backend as a UI API extension and vendor its document').toBeLessThanOrEqual(OFF_SPEC_CEILING)
|
||||
})
|
||||
|
||||
it('vendors a document inside the supported UI API range', () => {
|
||||
const version = parseSemver(loadDocuments()[0].info.version)
|
||||
expect(version).not.toBeNull()
|
||||
|
||||
Reference in new issue
Block a user