mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-30 19:32:46 +08:00
fix(docs): restore api-reference URL continuity and fix translated SDK bodies (#6617)
Two docs-only defects introduced by #5273 (`263e3ca67e`), which re-founded the public API reference on the v2 surface. 1. Ten translated SDK snippets produce a deterministic 400. The streaming example in the five translated `api-reference/typescript.mdx` and `python.mdx` pages was repointed from `/api/workflows/{id}/execute` to `/api/v2/workflows/{id}/execute` and nothing else was changed — fr/ja/zh typescript.mdx are literally one-line diffs. `message` stayed at the body root. That was correct against v1, whose route treats the whole non-control body as workflow input, but `v2ExecuteWorkflowBodySchema` ends in `.strict()` and the route parses before executing, so every copied snippet returns `400 Unrecognized key: "message"`. The same commit fixed the English bodies to `input: { … }`, so this is an oversight, not a decision. The ten fences now match `en/api-reference/typescript.mdx:959` and `python.mdx:681`. Not relaxing `.strict()`: it is deliberate house style across the v2 contract and is what makes a typo'd option fail loudly instead of silently. 2. Thirty-two published operation pages 404 with no redirect. Replacing the single v1 `openapi.json` with seven v2-only specs changes page identity, because fumadocs derives every generated page as `slugify(tag)/operationId` from the specs at build time. Re-deriving both sets gives 52 old slugs and 128 new ones: 32 disappear and 20 keep their URL while silently retargeting v1 -> v2 (`knowledge-bases/updateKnowledgeBase` also flips PUT -> PATCH). All 52 are in the live sitemap — parsing `<loc>` from docs.sim.ai/sitemap.xml gives 458 URLs of which 56 are `/api-reference/`: the four static pages plus all 52 generated ones by name, including every one of the 32 that die. They are 200 today under an allow-all robots.txt. The spec swap itself is deliberate and CI-enforced (`check-openapi-specs.ts` requires every published operation under `/api/v2/`), so restoring the v1 operations is not an option. The missing piece is the redirect map, in a file that already carried 56 such rules from earlier doc moves. `permanent: true` (308) is used only for a true 1:1 successor — same operation, renamed. A 308 is cached indefinitely and effectively unrecallable, so anything that collapses two pages onto one, changes the identifier model, or lands on a merely adjacent operation is `permanent: false` (307). That splits 21/11. Four destinations differ from the mapping proposed in review, each on evidence from the specs rather than from the operation names: - `workflows/getJobStatus` is not destination-less. The v2 queued-execution receipt (`QueuedWorkflowRun`) returns `statusUrl` `/api/v2/workflows/{id}/runs/{runId}`, so `workflow-runs/getWorkflowRunV2` is the successor poll target — far better than a generic landing page. - The three HITL read operations go to `getWorkflowRunV2`, not to the resume page: `WorkflowRunStatus` carries a `paused` object with `contextId`, `pausedAt`, and `pauseKind`. Pointing a GET doc at a POST doc would be wrong. - `human-in-the-loop/listPausedExecutions` goes to `listWorkflowRunsV2`, whose `status` filter includes `paused`. - `tables/batchUpdateRows` is 307, not 308. v2 `updateTableRows` is "Update Rows by Filter" — the successor of v1 `updateRows` (PUT, predicate-based), which keeps its 308. v2 has no by-id batch update at all, so batchUpdateRows lands on a genuinely different operation. 3. A guard, so the map cannot rot silently. `scripts/openapi/docs-redirects.test.ts` recomputes the generated slug set the way fumadocs does and asserts no `/api-reference/` source shadows a live page and every destination resolves. Nothing else in the repo reads docs URLs, so a future spec regeneration would otherwise break the map with no signal. It needs no wiring: `check-openapi.ts` already runs this vitest config. The redirect array moves to `apps/docs/lib/redirects.ts` because the guard cannot import `next.config.ts` — `createMDX()` runs the fumadocs-mdx generator at import time, which made vitest emit an unhandled build error and warn about false positives. The 56 pre-existing rules are byte-identical to before, verified programmatically; `next.config.ts` keeps the same public shape and Next's own `checkCustomRoutes` accepts all 88 rules. Open question for the owner, larger than the redirects: all 82 `/api/v1` route files survive on staging, so a live public API now ships with zero reference docs, while the documented `/api/v2` surface returns 404 for any caller outside the off-by-default `v2-api` flag cohort. Is that the intended end state or transitional?
This commit is contained in:
@@ -662,7 +662,7 @@ def stream_workflow():
|
||||
'X-API-Key': os.getenv('SIM_API_KEY')
|
||||
},
|
||||
json={
|
||||
'message': 'Generate a story',
|
||||
'input': {'message': 'Generate a story'},
|
||||
'stream': True,
|
||||
'selectedOutputs': ['agent1.content']
|
||||
},
|
||||
|
||||
@@ -972,7 +972,7 @@ function StreamingWorkflow() {
|
||||
'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Generate a story',
|
||||
input: { message: 'Generate a story' },
|
||||
stream: true,
|
||||
selectedOutputs: ['agent1.content']
|
||||
})
|
||||
|
||||
@@ -662,7 +662,7 @@ def stream_workflow():
|
||||
'X-API-Key': os.getenv('SIM_API_KEY')
|
||||
},
|
||||
json={
|
||||
'message': 'Generate a story',
|
||||
'input': {'message': 'Generate a story'},
|
||||
'stream': True,
|
||||
'selectedOutputs': ['agent1.content']
|
||||
},
|
||||
|
||||
@@ -972,7 +972,7 @@ function StreamingWorkflow() {
|
||||
'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Generate a story',
|
||||
input: { message: 'Generate a story' },
|
||||
stream: true,
|
||||
selectedOutputs: ['agent1.content']
|
||||
})
|
||||
|
||||
@@ -662,7 +662,7 @@ def stream_workflow():
|
||||
'X-API-Key': os.getenv('SIM_API_KEY')
|
||||
},
|
||||
json={
|
||||
'message': 'Generate a story',
|
||||
'input': {'message': 'Generate a story'},
|
||||
'stream': True,
|
||||
'selectedOutputs': ['agent1.content']
|
||||
},
|
||||
|
||||
@@ -972,7 +972,7 @@ function StreamingWorkflow() {
|
||||
'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Generate a story',
|
||||
input: { message: 'Generate a story' },
|
||||
stream: true,
|
||||
selectedOutputs: ['agent1.content']
|
||||
})
|
||||
|
||||
@@ -662,7 +662,7 @@ def stream_workflow():
|
||||
'X-API-Key': os.getenv('SIM_API_KEY')
|
||||
},
|
||||
json={
|
||||
'message': 'Generate a story',
|
||||
'input': {'message': 'Generate a story'},
|
||||
'stream': True,
|
||||
'selectedOutputs': ['agent1.content']
|
||||
},
|
||||
|
||||
@@ -972,7 +972,7 @@ function StreamingWorkflow() {
|
||||
'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Generate a story',
|
||||
input: { message: 'Generate a story' },
|
||||
stream: true,
|
||||
selectedOutputs: ['agent1.content']
|
||||
})
|
||||
|
||||
@@ -663,7 +663,7 @@ def stream_workflow():
|
||||
'X-API-Key': os.getenv('SIM_API_KEY')
|
||||
},
|
||||
json={
|
||||
'message': 'Generate a story',
|
||||
'input': {'message': 'Generate a story'},
|
||||
'stream': True,
|
||||
'selectedOutputs': ['agent1.content']
|
||||
},
|
||||
|
||||
@@ -971,7 +971,7 @@ function StreamingWorkflow() {
|
||||
'X-API-Key': process.env.SIM_API_KEY! // Server-side environment variable only
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Generate a story',
|
||||
input: { message: 'Generate a story' },
|
||||
stream: true,
|
||||
selectedOutputs: ['agent1.content']
|
||||
})
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
/** The shape Next expects back from `next.config.ts`'s `redirects()`. */
|
||||
type DocsRedirect = Awaited<ReturnType<NonNullable<NextConfig['redirects']>>>[number]
|
||||
|
||||
/**
|
||||
* Every redirect the docs site serves, in match order — Next applies the first
|
||||
* matching rule.
|
||||
*
|
||||
* This lives outside `next.config.ts` so it can be read without evaluating that
|
||||
* module. `createMDX()` runs at import time and bundles `source.config.ts`
|
||||
* against `process.cwd()`, so importing the config from the root Vitest project
|
||||
* fails with `The entry point "source.config.ts" cannot be marked as external`.
|
||||
* `scripts/openapi/docs-redirects.test.ts` reads this array directly to keep the
|
||||
* `/api-reference/` rules honest against the specs.
|
||||
*
|
||||
* The whole table lives here rather than only the `/api-reference/` block: Next
|
||||
* applies the first matching rule, so splitting one ordered list across two
|
||||
* modules would make match order an emergent property of two files.
|
||||
*/
|
||||
export const DOCS_REDIRECTS: DocsRedirect[] = [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/introduction',
|
||||
permanent: true,
|
||||
},
|
||||
// building-agents/agents merged into the building-agents overview
|
||||
{ source: '/building-agents/agents', destination: '/agents', permanent: true },
|
||||
// form deployment removed
|
||||
{ source: '/deployment/form', destination: '/workflows/deployment', permanent: true },
|
||||
// copilot deprecated and removed
|
||||
{ source: '/copilot', destination: '/chat', permanent: true },
|
||||
{ source: '/copilot/:path*', destination: '/chat', permanent: true },
|
||||
// mothership/* renamed to chat/* — the surface is "Chat", the agent is "Sim"
|
||||
{ source: '/mothership', destination: '/chat', permanent: true },
|
||||
{ source: '/mothership/:path*', destination: '/chat/:path*', permanent: true },
|
||||
// connections/* and variables/* collapsed into single pages under workflows/
|
||||
{ source: '/connections', destination: '/workflows/connections', permanent: true },
|
||||
{ source: '/connections/:path*', destination: '/workflows/connections', permanent: true },
|
||||
{ source: '/variables', destination: '/workflows/variables', permanent: true },
|
||||
{ source: '/variables/:path*', destination: '/workflows/variables', permanent: true },
|
||||
// capabilities/* renamed to building-agents/*
|
||||
{ source: '/capabilities', destination: '/agents', permanent: true },
|
||||
{ source: '/capabilities/agents', destination: '/agents', permanent: true },
|
||||
{
|
||||
source: '/capabilities/choosing',
|
||||
destination: '/agents/choosing',
|
||||
permanent: true,
|
||||
},
|
||||
// execution/* was broken up; redirect old URLs to their new homes
|
||||
{ source: '/execution', destination: '/workflows', permanent: true },
|
||||
{ source: '/execution/index', destination: '/workflows', permanent: true },
|
||||
{ source: '/execution/basics', destination: '/workflows/how-it-runs', permanent: true },
|
||||
{ source: '/execution/files', destination: '/files/passing-files', permanent: true },
|
||||
{ source: '/execution/logging', destination: '/logs-debugging/logging', permanent: true },
|
||||
{ source: '/execution/costs', destination: '/platform/costs', permanent: true },
|
||||
{ source: '/execution/api', destination: '/api-reference/getting-started', permanent: true },
|
||||
{
|
||||
source: '/execution/api-deployment',
|
||||
destination: '/workflows/deployment/api',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/execution/chat', destination: '/workflows/deployment/chat', permanent: true },
|
||||
// Points at the final page, not at `/deployment/form` — that is itself a
|
||||
// redirect source above, and Next does not chain rules internally.
|
||||
{ source: '/execution/form', destination: '/workflows/deployment', permanent: true },
|
||||
{
|
||||
source: '/mcp/deploy-workflows',
|
||||
destination: '/workflows/deployment/mcp',
|
||||
permanent: true,
|
||||
},
|
||||
// building-agents section renamed to agents; mcp and skills folded into it
|
||||
{ source: '/building-agents', destination: '/agents', permanent: true },
|
||||
{ source: '/building-agents/:path*', destination: '/agents/:path*', permanent: true },
|
||||
{ source: '/mcp', destination: '/agents/mcp', permanent: true },
|
||||
{ source: '/skills', destination: '/agents/skills', permanent: true },
|
||||
// tools/ + triggers/<service> unified into per-service integrations/ pages.
|
||||
// Specific moves first (Next applies the first matching redirect):
|
||||
{
|
||||
source: '/tools/custom-tools',
|
||||
destination: '/agents/custom-tools',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/tools', destination: '/integrations', permanent: true },
|
||||
{ source: '/tools/:slug', destination: '/integrations/:slug', permanent: true },
|
||||
// Old blocks/triggers index pages were folded into the workflows overview.
|
||||
// Native trigger pages (/triggers/start|schedule|webhook|rss|table) still exist.
|
||||
{ source: '/blocks', destination: '/workflows#blocks', permanent: true },
|
||||
{ source: '/triggers', destination: '/workflows#triggers', permanent: true },
|
||||
// Integration trigger pages: provider slug differs from the block type for a few.
|
||||
{
|
||||
source: '/triggers/jsm',
|
||||
destination: '/integrations/jira_service_management',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/google-calendar',
|
||||
destination: '/integrations/google_calendar',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/google-drive',
|
||||
destination: '/integrations/google_drive',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/google-sheets',
|
||||
destination: '/integrations/google_sheets',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/microsoft-teams',
|
||||
destination: '/integrations/microsoft_teams',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source:
|
||||
'/triggers/:slug(airtable|ashby|attio|azure_devops|calcom|calendly|circleback|confluence|emailbison|fathom|fireflies|github|gmail|gong|google_forms|grain|greenhouse|hubspot|imap|intercom|jira|lemlist|linear|monday|notion|outlook|resend|salesforce|sendblue|servicenow|slack|stripe|telegram|twilio_voice|typeform|vercel|webflow|whatsapp|zoom)',
|
||||
destination: '/integrations/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
// URL structure now mirrors the sidebar: sections own their pages.
|
||||
{ source: '/blocks/:slug', destination: '/workflows/blocks/:slug', permanent: true },
|
||||
{
|
||||
source: '/triggers/:slug(start|schedule|webhook|rss|table|sim)',
|
||||
destination: '/workflows/triggers/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/deployment', destination: '/workflows/deployment', permanent: true },
|
||||
{
|
||||
source: '/deployment/:path*',
|
||||
destination: '/workflows/deployment/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/mailer', destination: '/chat/mailer', permanent: true },
|
||||
{ source: '/credentials', destination: '/platform/credentials', permanent: true },
|
||||
{
|
||||
source: '/credentials/:path*',
|
||||
destination: '/platform/credentials',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/permissions', destination: '/platform/permissions', permanent: true },
|
||||
{
|
||||
source: '/permissions/:path*',
|
||||
destination: '/platform/permissions',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/workspaces/fundamentals', destination: '/platform/workspaces', permanent: true },
|
||||
{
|
||||
source: '/workspaces/:slug(organization|permissions|credentials)',
|
||||
destination: '/platform/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/costs', destination: '/platform/costs', permanent: true },
|
||||
{ source: '/self-hosting', destination: '/platform/self-hosting', permanent: true },
|
||||
{
|
||||
source: '/self-hosting/:path*',
|
||||
destination: '/platform/self-hosting/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/enterprise', destination: '/platform/enterprise', permanent: true },
|
||||
{
|
||||
source: '/enterprise/:path*',
|
||||
destination: '/platform/enterprise/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Continuity for the `/api-reference/` operation pages retired when the docs
|
||||
* moved from the single v1 `openapi.json` to the seven code-first v2 specs.
|
||||
*
|
||||
* Every source below was a live, sitemap-submitted page whose slug no longer
|
||||
* exists in the generated set. Sources are unprefixed and have no locale
|
||||
* variants because generated pages mount only under the `en` base directory;
|
||||
* `scripts/openapi/docs-redirects.test.ts` documents and implements the slug
|
||||
* derivation.
|
||||
*
|
||||
* `permanent: true` (308) is reserved for a true 1:1 successor: same operation,
|
||||
* renamed. A 308 is cached indefinitely and is effectively unrecallable, so
|
||||
* anything that collapses two pages into one, changes the identifier model, or
|
||||
* lands on a merely adjacent operation uses `permanent: false` (307).
|
||||
*
|
||||
* `scripts/openapi/docs-redirects.test.ts` pins both halves of the invariant
|
||||
* against the specs: no source may shadow a live slug, and every destination
|
||||
* must resolve.
|
||||
*/
|
||||
// Pure operationId renames — same path and method, v1 -> v2.
|
||||
{
|
||||
source: '/api-reference/audit-logs/getAuditLogDetails',
|
||||
destination: '/api-reference/audit-logs/getAuditLog',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/knowledge-bases/listDocuments',
|
||||
destination: '/api-reference/knowledge-bases/listKnowledgeDocuments',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/knowledge-bases/getDocument',
|
||||
destination: '/api-reference/knowledge-bases/getKnowledgeDocument',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/knowledge-bases/deleteDocument',
|
||||
destination: '/api-reference/knowledge-bases/deleteKnowledgeDocument',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/knowledge-bases/uploadDocument',
|
||||
destination: '/api-reference/knowledge-bases/uploadKnowledgeDocument',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/knowledge-bases/searchKnowledgeBase',
|
||||
destination: '/api-reference/knowledge-bases/searchKnowledge',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/logs/queryLogs',
|
||||
destination: '/api-reference/logs/listLogs',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/listRows',
|
||||
destination: '/api-reference/tables/listTableRows',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/getRow',
|
||||
destination: '/api-reference/tables/getTableRow',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/insertRows',
|
||||
destination: '/api-reference/tables/createTableRows',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/updateRow',
|
||||
destination: '/api-reference/tables/updateTableRow',
|
||||
permanent: true,
|
||||
},
|
||||
// v1 `PUT /rows` and v2 `PATCH /rows` are both the filter-predicate bulk update.
|
||||
{
|
||||
source: '/api-reference/tables/updateRows',
|
||||
destination: '/api-reference/tables/updateTableRows',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/deleteRow',
|
||||
destination: '/api-reference/tables/deleteTableRow',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/deleteRows',
|
||||
destination: '/api-reference/tables/deleteTableRows',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/upsertRow',
|
||||
destination: '/api-reference/tables/upsertTableRow',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/addColumn',
|
||||
destination: '/api-reference/tables/addTableColumn',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/updateColumn',
|
||||
destination: '/api-reference/tables/updateTableColumn',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/tables/deleteColumn',
|
||||
destination: '/api-reference/tables/deleteTableColumn',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/workflows/executeWorkflow',
|
||||
destination: '/api-reference/workflows/executeWorkflowV2',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/workflows/getWorkflowExecution',
|
||||
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/workflows/cancelExecution',
|
||||
destination: '/api-reference/workflow-runs/cancelRunV2',
|
||||
permanent: true,
|
||||
},
|
||||
// Approximations — 307 so a better destination stays reachable later.
|
||||
// v1 took multipart up to 100MB; v2 `createFile` takes inline UTF-8/base64
|
||||
// and defers larger payloads to an upload session.
|
||||
{
|
||||
source: '/api-reference/files/uploadFile',
|
||||
destination: '/api-reference/files/createFile',
|
||||
permanent: false,
|
||||
},
|
||||
// The human-in-the-loop tag is retired: pause state is now a field on the
|
||||
// run resource rather than its own endpoint family.
|
||||
{
|
||||
source: '/api-reference/human-in-the-loop/listPausedExecutions',
|
||||
destination: '/api-reference/workflow-runs/listWorkflowRunsV2',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/human-in-the-loop/getPausedExecution',
|
||||
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/human-in-the-loop/getPausedExecutionByResumePath',
|
||||
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/human-in-the-loop/getPauseContext',
|
||||
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/human-in-the-loop/resumeExecution',
|
||||
destination: '/api-reference/workflow-runs/resumeWorkflowRunV2',
|
||||
permanent: false,
|
||||
},
|
||||
// Two v1 identifiers (log id, execution id) collapse onto one v2 runId.
|
||||
{
|
||||
source: '/api-reference/logs/getLogDetails',
|
||||
destination: '/api-reference/logs/getLog',
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/api-reference/logs/getExecutionDetails',
|
||||
destination: '/api-reference/logs/getLog',
|
||||
permanent: false,
|
||||
},
|
||||
// v1 updated a batch of rows by id; v2 has no by-id batch, only the predicate form.
|
||||
{
|
||||
source: '/api-reference/tables/batchUpdateRows',
|
||||
destination: '/api-reference/tables/updateTableRows',
|
||||
permanent: false,
|
||||
},
|
||||
// v1 reported rate limits alongside spend and storage; v2 billing status
|
||||
// carries plan, credit allowance, and storage quota but not rate limits.
|
||||
{
|
||||
source: '/api-reference/usage/getUsageLimits',
|
||||
destination: '/api-reference/billing/getBillingStatus',
|
||||
permanent: false,
|
||||
},
|
||||
// v1 polled a jobId; the v2 queue receipt returns a `statusUrl` pointing at
|
||||
// this run endpoint, which is the successor poll target.
|
||||
{
|
||||
source: '/api-reference/workflows/getJobStatus',
|
||||
destination: '/api-reference/workflow-runs/getWorkflowRunV2',
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
+2
-145
@@ -1,5 +1,6 @@
|
||||
import { createMDX } from 'fumadocs-mdx/next'
|
||||
import type { NextConfig } from 'next'
|
||||
import { DOCS_REDIRECTS } from './lib/redirects'
|
||||
|
||||
const withMDX = createMDX()
|
||||
|
||||
@@ -16,151 +17,7 @@ const config: NextConfig = {
|
||||
webpackBuildWorker: true,
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/introduction',
|
||||
permanent: true,
|
||||
},
|
||||
// building-agents/agents merged into the building-agents overview
|
||||
{ source: '/building-agents/agents', destination: '/agents', permanent: true },
|
||||
// form deployment removed
|
||||
{ source: '/deployment/form', destination: '/workflows/deployment', permanent: true },
|
||||
// copilot deprecated and removed
|
||||
{ source: '/copilot', destination: '/chat', permanent: true },
|
||||
{ source: '/copilot/:path*', destination: '/chat', permanent: true },
|
||||
// mothership/* renamed to chat/* — the surface is "Chat", the agent is "Sim"
|
||||
{ source: '/mothership', destination: '/chat', permanent: true },
|
||||
{ source: '/mothership/:path*', destination: '/chat/:path*', permanent: true },
|
||||
// connections/* and variables/* collapsed into single pages under workflows/
|
||||
{ source: '/connections', destination: '/workflows/connections', permanent: true },
|
||||
{ source: '/connections/:path*', destination: '/workflows/connections', permanent: true },
|
||||
{ source: '/variables', destination: '/workflows/variables', permanent: true },
|
||||
{ source: '/variables/:path*', destination: '/workflows/variables', permanent: true },
|
||||
// capabilities/* renamed to building-agents/*
|
||||
{ source: '/capabilities', destination: '/agents', permanent: true },
|
||||
{ source: '/capabilities/agents', destination: '/agents', permanent: true },
|
||||
{
|
||||
source: '/capabilities/choosing',
|
||||
destination: '/agents/choosing',
|
||||
permanent: true,
|
||||
},
|
||||
// execution/* was broken up; redirect old URLs to their new homes
|
||||
{ source: '/execution', destination: '/workflows', permanent: true },
|
||||
{ source: '/execution/index', destination: '/workflows', permanent: true },
|
||||
{ source: '/execution/basics', destination: '/workflows/how-it-runs', permanent: true },
|
||||
{ source: '/execution/files', destination: '/files/passing-files', permanent: true },
|
||||
{ source: '/execution/logging', destination: '/logs-debugging/logging', permanent: true },
|
||||
{ source: '/execution/costs', destination: '/platform/costs', permanent: true },
|
||||
{ source: '/execution/api', destination: '/api-reference/getting-started', permanent: true },
|
||||
{
|
||||
source: '/execution/api-deployment',
|
||||
destination: '/workflows/deployment/api',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/execution/chat', destination: '/workflows/deployment/chat', permanent: true },
|
||||
{ source: '/execution/form', destination: '/deployment/form', permanent: true },
|
||||
{
|
||||
source: '/mcp/deploy-workflows',
|
||||
destination: '/workflows/deployment/mcp',
|
||||
permanent: true,
|
||||
},
|
||||
// building-agents section renamed to agents; mcp and skills folded into it
|
||||
{ source: '/building-agents', destination: '/agents', permanent: true },
|
||||
{ source: '/building-agents/:path*', destination: '/agents/:path*', permanent: true },
|
||||
{ source: '/mcp', destination: '/agents/mcp', permanent: true },
|
||||
{ source: '/skills', destination: '/agents/skills', permanent: true },
|
||||
// tools/ + triggers/<service> unified into per-service integrations/ pages.
|
||||
// Specific moves first (Next applies the first matching redirect):
|
||||
{
|
||||
source: '/tools/custom-tools',
|
||||
destination: '/agents/custom-tools',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/tools', destination: '/integrations', permanent: true },
|
||||
{ source: '/tools/:slug', destination: '/integrations/:slug', permanent: true },
|
||||
// Old blocks/triggers index pages were folded into the workflows overview.
|
||||
// Native trigger pages (/triggers/start|schedule|webhook|rss|table) still exist.
|
||||
{ source: '/blocks', destination: '/workflows#blocks', permanent: true },
|
||||
{ source: '/triggers', destination: '/workflows#triggers', permanent: true },
|
||||
// Integration trigger pages: provider slug differs from the block type for a few.
|
||||
{
|
||||
source: '/triggers/jsm',
|
||||
destination: '/integrations/jira_service_management',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/google-calendar',
|
||||
destination: '/integrations/google_calendar',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/google-drive',
|
||||
destination: '/integrations/google_drive',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/google-sheets',
|
||||
destination: '/integrations/google_sheets',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/triggers/microsoft-teams',
|
||||
destination: '/integrations/microsoft_teams',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source:
|
||||
'/triggers/:slug(airtable|ashby|attio|azure_devops|calcom|calendly|circleback|confluence|emailbison|fathom|fireflies|github|gmail|gong|google_forms|grain|greenhouse|hubspot|imap|intercom|jira|lemlist|linear|monday|notion|outlook|resend|salesforce|sendblue|servicenow|slack|stripe|telegram|twilio_voice|typeform|vercel|webflow|whatsapp|zoom)',
|
||||
destination: '/integrations/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
// URL structure now mirrors the sidebar: sections own their pages.
|
||||
{ source: '/blocks/:slug', destination: '/workflows/blocks/:slug', permanent: true },
|
||||
{
|
||||
source: '/triggers/:slug(start|schedule|webhook|rss|table|sim)',
|
||||
destination: '/workflows/triggers/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/deployment', destination: '/workflows/deployment', permanent: true },
|
||||
{
|
||||
source: '/deployment/:path*',
|
||||
destination: '/workflows/deployment/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/mailer', destination: '/chat/mailer', permanent: true },
|
||||
{ source: '/credentials', destination: '/platform/credentials', permanent: true },
|
||||
{
|
||||
source: '/credentials/:path*',
|
||||
destination: '/platform/credentials',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/permissions', destination: '/platform/permissions', permanent: true },
|
||||
{
|
||||
source: '/permissions/:path*',
|
||||
destination: '/platform/permissions',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/workspaces/fundamentals', destination: '/platform/workspaces', permanent: true },
|
||||
{
|
||||
source: '/workspaces/:slug(organization|permissions|credentials)',
|
||||
destination: '/platform/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/costs', destination: '/platform/costs', permanent: true },
|
||||
{ source: '/self-hosting', destination: '/platform/self-hosting', permanent: true },
|
||||
{
|
||||
source: '/self-hosting/:path*',
|
||||
destination: '/platform/self-hosting/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
{ source: '/enterprise', destination: '/platform/enterprise', permanent: true },
|
||||
{
|
||||
source: '/enterprise/:path*',
|
||||
destination: '/platform/enterprise/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
return DOCS_REDIRECTS
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Pins the `/api-reference/` rules in `apps/docs/lib/redirects.ts` against the
|
||||
* generated operation pages.
|
||||
*
|
||||
* Page identity is derived from the specs at build time, so a spec regeneration
|
||||
* can silently invalidate the map in two directions: a redirect `source` can
|
||||
* start shadowing a page that now exists (the redirect wins and hides it), and a
|
||||
* `destination` can stop resolving (the redirect lands on a 404). Neither shows
|
||||
* up in a build, a type-check, or any other check — nothing else in the repo
|
||||
* reads docs URLs.
|
||||
*
|
||||
* Both directions use the same slug set, so a rule cannot shadow a hand-authored
|
||||
* page (`/api-reference/getting-started`) any more than a generated one.
|
||||
*
|
||||
* The slug derivation mirrors `fumadocs-openapi`'s auto preset: pages are
|
||||
* emitted at `<baseDir>/<slugify(tag)>/<operationId>.mdx`, `(generated)` is a
|
||||
* folder group stripped from the URL, and `hideLocale: 'default-locale'` drops
|
||||
* the `en` prefix — so the public URL is `/api-reference/<tag>/<operationId>`.
|
||||
*/
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OPENAPI_SPEC_FILES } from '../../apps/docs/lib/openapi-specs'
|
||||
import { DOCS_REDIRECTS } from '../../apps/docs/lib/redirects'
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '../..')
|
||||
const DOCS_DIR = path.join(ROOT, 'apps/docs')
|
||||
const STATIC_PAGES_DIR = path.join(DOCS_DIR, 'content/docs/en/api-reference')
|
||||
const PREFIX = '/api-reference/'
|
||||
|
||||
/**
|
||||
* `fumadocs-openapi`'s `methodKeys`, which is what decides whether an operation
|
||||
* becomes a page. It is deliberately narrower than the OpenAPI method set —
|
||||
* `options` and `trace` are not enumerated, so an operation declared under
|
||||
* either produces no page and must not count as a resolvable destination here.
|
||||
*/
|
||||
const HTTP_METHODS = ['get', 'post', 'patch', 'delete', 'head', 'put'] as const
|
||||
|
||||
interface OperationObject {
|
||||
operationId?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
/** `fumadocs-openapi`'s default `slugify` for tag folder names. */
|
||||
function slugify(value: string): string {
|
||||
return value.replace(/\s+/g, '-').toLowerCase()
|
||||
}
|
||||
|
||||
function generatedSlugs(): Set<string> {
|
||||
const slugs = new Set<string>()
|
||||
for (const file of OPENAPI_SPEC_FILES) {
|
||||
const spec = JSON.parse(readFileSync(path.join(DOCS_DIR, file), 'utf8')) as {
|
||||
paths?: Record<string, Record<string, OperationObject | undefined>>
|
||||
}
|
||||
for (const pathItem of Object.values(spec.paths ?? {})) {
|
||||
for (const method of HTTP_METHODS) {
|
||||
const operation = pathItem[method]
|
||||
if (!operation?.operationId) continue
|
||||
const tags = operation.tags?.length ? operation.tags : ['unknown']
|
||||
for (const tag of tags) {
|
||||
slugs.add(`${slugify(tag)}/${operation.operationId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return slugs
|
||||
}
|
||||
|
||||
/** Every slug a `/api-reference/` URL can resolve to: generated plus hand-authored. */
|
||||
function resolvableSlugs(): Set<string> {
|
||||
const slugs = generatedSlugs()
|
||||
for (const entry of readdirSync(STATIC_PAGES_DIR)) {
|
||||
if (entry.endsWith('.mdx')) slugs.add(path.basename(entry, '.mdx'))
|
||||
}
|
||||
return slugs
|
||||
}
|
||||
|
||||
const API_REFERENCE_RULES = DOCS_REDIRECTS.filter(
|
||||
(rule) => rule.source.startsWith(PREFIX) || rule.destination.startsWith(PREFIX)
|
||||
)
|
||||
|
||||
describe('api-reference redirects', () => {
|
||||
it('is a non-empty set of literal paths', () => {
|
||||
expect(API_REFERENCE_RULES.length).toBeGreaterThan(0)
|
||||
for (const rule of API_REFERENCE_RULES) {
|
||||
expect(
|
||||
`${rule.source} -> ${rule.destination}`,
|
||||
'path params would make the slug checks below vacuous'
|
||||
).not.toMatch(/[:*]/)
|
||||
}
|
||||
})
|
||||
|
||||
it('never shadows a page that exists', () => {
|
||||
const resolvable = resolvableSlugs()
|
||||
const shadowed = API_REFERENCE_RULES.map((rule) => rule.source)
|
||||
.filter((source) => source.startsWith(PREFIX))
|
||||
.filter((source) => resolvable.has(source.slice(PREFIX.length)))
|
||||
expect(shadowed).toEqual([])
|
||||
})
|
||||
|
||||
it('only points at pages that exist', () => {
|
||||
const resolvable = resolvableSlugs()
|
||||
const broken = API_REFERENCE_RULES.map((rule) => rule.destination)
|
||||
.filter((destination) => destination.startsWith(PREFIX))
|
||||
.filter((destination) => !resolvable.has(destination.slice(PREFIX.length)))
|
||||
expect(broken).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user