<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="description" content="SwaggerUI"/> <title>Grist API Console</title> <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.1.0/swagger-ui.css"/> <style> body { margin: 0; } </style> </head> <body> <div id="root"/> <script src="https://unpkg.com/swagger-ui-dist@5.1.0/swagger-ui-bundle.js" crossorigin></script> <script> // Start by initiating various fetches early, we'll use the promise results later. // The API calls are authorized by cookies. // We only fetch the API key to pass to `preauthorizeApiKey` which makes it show // in the example `curl` commands (which isn't unambiguously good, since // it makes screen-sharing more risky). const apiKey = fetch('/api/profile/apiKey').then(r => r.text()); // These are used to set the examples for orgs, workspaces, and docs. const orgsPromise = fetch('/api/orgs').then(r => r.json()); // We make a request for each org - hopefully there aren't too many. // Currently I only see rate limiting in DocApi, which shouldn't be a problem here. // Fortunately we don't need a request for each workspace, // since listing workspaces in an org also lists the docs in each workspace. const workspacesPromise = orgsPromise.then(orgs => Promise.all(orgs.map(org => fetch(`/api/orgs/${org.id}/workspaces`).then(r => r.json()).then(workspaces => ({org, workspaces})) ))); function GristPlugin(system) { return { statePlugins: { spec: { wrapActions: { // Customize what happens when a parameter is changed, e.g. selected from a dropdown. changeParamByIdentity: (oriAction) => (...args) => { const [keyPath, param, value, _isXml, noWrap] = args; if (noWrap || !value) { // `noWrap` is our own flag to avoid infinite recursion. // It's set when calling this action inside `setParamValue` below. // `value` is falsy when choosing our default "Select..." option from a dropdown. return oriAction(...args); } // These are the path parameters that we handle specially and provide examples for. // When a value is selected in one endpoint, set the same value in all other endpoints. // This makes a bit more convenient to do multiple different operations on the same object. // But maybe it'll cause confusion/mistakes when operating on different objects? if (["orgId", "workspaceId", "docId", "tableId", "colId"].includes(param.get("name"))) { setParamValue(param, value); } // When a docId is selected, fetch the list of that doc's tables and set examples for tableId. // This is a significant convenience, but it causes some UI jankiness. // Updating the spec with these examples takes some CPU and the UI freezes for a moment. // Then things jump around a bit as stuff is re-rendered, although it ends up in the right place // so it shouldn't be too disruptive. // All this happens after a short delay while the tables are being fetched. // It *might* be possible to set these example values more efficiently/lazily but I'm not sure, // and it'll probably significantly more difficult. if (param.get("name") === "docId") { fetch(`/api/docs/${value}/tables`).then(r => r.json()).then(({tables}) => { const examples = tables.map(table => ({value: table.id})); setExamples(examples, "tableId", true); }); } // When a tableId is selected, fetch the list of columns and set examples for colId. // This causes similar UI jankiness as above, but I think less severely since fewer endpoints // have a colId parameter. In fact, there's currently only one: `DELETE /columns`. // We *could* only do this when setting tableId within that endpoint, // but then the dropdown will be missing if you set the tableId elsewhere and then open this endpoint. // Alternatively, `GET /tables` could be modified to return column metadata for each table. if (param.get("name") === "tableId") { // When getting tables after setting docId, `value` is the docId so we have all the info. // Here `value` is the tableId and we need to get the docId separately. const parameters = system.getState().getIn(["spec", "meta", "paths", ...keyPath, "parameters"]); const docId = parameters.find((_value, key) => key.startsWith("path.docId"))?.get("value"); if (docId) { // `?hidden=1` includes hidden columns like gristHelper_Display and manualSort. fetch(`/api/docs/${docId}/tables/${value}/columns?hidden=1`).then(r => r.json()).then(({columns}) => { const examples = columns.map(col => ({value: col.id, summary: col.fields.label})); setExamples(examples, "colId"); }); } } return oriAction(...args); }, } } } } } function applySpecActions(cb) { // Don't call actions directly within `wrapActions`, react/redux doesn't like it. setTimeout(() => { const system = window.ui.getSystem(); const jsonSpec = system.getState().getIn(["spec", "json"]); cb(system.specActions, jsonSpec); }, 0); } function updateSpec(cb) { applySpecActions((specActions, jsonSpec) => { // `jsonSpec` is a special immutable object with methods like `getIn/setIn`. // `updateJsonSpec` expects a plain JS object, so we need to convert it. specActions.updateJsonSpec(cb(jsonSpec).toJSON()); }); } function setExamples(examplesArr, paramName, startBlank) { if (startBlank) { // When opening an endpoint, parameters with examples are immediately set to the first example. // For documents and tables, this would immediately call our custom code, // fetching lists of tables/columns. This is especially bad for documents, // as the document may have to be loaded from scratch in the doc worker. // So the dropdown has to start with an empty value in those cases. // You'd think this would run into the check for `!value` in `changeParamByIdentity`, // but apparently swagger has its own special handing for empty values before then. // // Somehow, when using this for workspace examples, this blank option becomes the last option in the dropdown. // That looks silly, so `startBlank` is only set for parameters that need it as mentioned above. examplesArr = [ {value: "", summary: "Select..."}, ...examplesArr.sort((a, b) => (a.summary || a.value).localeCompare(b.summary || b.value)) ]; } // Swagger expects `examples` to be an object, not an array. const examples = Object.fromEntries(examplesArr.map((ex) => [ex.value, ex])); updateSpec(spec => { return spec.setIn(["components", "parameters", `${paramName}PathParam`, "examples"], examples); }); } // Set the value of a parameter in all endpoints. function setParamValue(resolvedParam, value) { applySpecActions((specActions, spec) => { // This will be something like: // "https://url-to-grist.yml#/components/parameters/orgIdPathParam" // Note that we're assuming that the endpoint always uses `$ref` to define the parameter, // rather than defining it inline. // https://github.com/gristlabs/grist-help/pull/293 ensures this, // but future changes to the spec must remember to do the same. const ref = resolvedParam.get("$$ref"); // For every endpoint in the spec... for (const [pathKey, path] of spec.get("paths").entries()) { for (const [method, operation] of path.entries()) { const parameters = operation.get("parameters"); if (!parameters) continue; for (const param of parameters.values()) { // If this is the same parameter... if (ref.endsWith(param.get("$ref"))) { // Set the value. The final `true` is `noWrap` to prevent infinite recursion. specActions.changeParamByIdentity([pathKey, method], resolvedParam, value, false, true); } } } } }); } // Called after the spec is downloaded and parsed. function onComplete() { // The actual spec sets the server to `https://{subdomain}.getgrist.com/api`, // where {subdomain} is a variable that defaults to `docs`. // We want to use the same server as the page is loaded from. // This simplifies the UI and makes it work e.g. on localhost. updateSpec(spec => spec.set("servers", [{url: window.origin + "/api"}])); // See the comment where `apiKey` is defined. apiKey.then(key => window.ui.preauthorizeApiKey('ApiKey', key)); // Set examples for orgs, workspaces, and docs. orgsPromise.then(orgs => { const examples = orgs.map(org => ({ value: org.domain, summary: org.name, })); setExamples(examples, "orgId"); }); workspacesPromise.then(orgs => { const workSpaceExamples = orgs.flatMap(({org, workspaces}) => workspaces.map(ws => ({ value: ws.id, summary: `${org.name} » ${ws.name}` }))); setExamples(workSpaceExamples, "workspaceId"); const docExamples = orgs.flatMap(({org, workspaces}) => workspaces.flatMap(ws => ws.docs.map(doc => ({ value: doc.id, summary: `${org.name} » ${ws.name} » ${doc.name}` })))); setExamples(docExamples, "docId", true); }) } window.onload = () => { window.ui = SwaggerUIBundle({ plugins: [ GristPlugin, ], url: 'https://raw.githubusercontent.com/gristlabs/grist-help/master/api/grist.yml', dom_id: '#root', onComplete, }); }; </script> </body> </html>