upt2
This commit is contained in:
@@ -186,7 +186,7 @@ Pokryté oblasti:
|
||||
|
||||
Aplikace poslouchá na `0.0.0.0` a portu `PORT` s výchozí hodnotou `3000`. `ROOT_PATH` se používá pro dokumentaci a testovací requesty za reverse proxy, například `/apps/sap-bo`.
|
||||
|
||||
`/docs` a `/openapi.json` dokumentují všechny obecně implementované endpointy a obsahují povinné `X-SAP-B1-*` hlavičky.
|
||||
`/docs` je Swagger UI napojené na `/openapi.json`. V Swaggeru použij `Authorize` pro vyplnění povinných `X-SAP-B1-*` hlaviček a potom `Try it out` u konkrétní operace. OpenAPI `servers` respektuje `ROOT_PATH`, takže za AppFactory proxy volá například `/apps/sap-bo/api/business-partners`, ne root doménu.
|
||||
|
||||
## TODO ověřit v konkrétní instalaci SAP Business One
|
||||
|
||||
|
||||
Generated
+44
@@ -11,11 +11,13 @@
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.18.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
@@ -354,6 +356,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@@ -515,6 +524,17 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/swagger-ui-express": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
|
||||
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
|
||||
@@ -2111,6 +2131,30 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.32.8",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz",
|
||||
"integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-express": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"swagger-ui-dist": ">=5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= v0.10.32"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.18.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
|
||||
+25
-145
@@ -1,4 +1,5 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import swaggerUi from "swagger-ui-express";
|
||||
import { SapBusinessOneServiceLayer } from "./SapBusinessOneServiceLayer";
|
||||
import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
|
||||
import { buildODataParams, extractNextLink } from "./client/odata";
|
||||
@@ -531,6 +532,14 @@ function openApiDocument(basePath = "") {
|
||||
"HTTP API and TypeScript connector for SAP Business One Service Layer. SAP credentials are supplied per request in X-SAP-B1-* headers."
|
||||
},
|
||||
servers: [{ url: basePath || "/" }],
|
||||
security: [
|
||||
{
|
||||
SapB1BaseUrl: [],
|
||||
SapB1CompanyDB: [],
|
||||
SapB1Username: [],
|
||||
SapB1Password: []
|
||||
}
|
||||
],
|
||||
tags: [
|
||||
{ name: "Service" },
|
||||
{ name: "Session" },
|
||||
@@ -572,143 +581,19 @@ function openApiDocument(basePath = "") {
|
||||
};
|
||||
}
|
||||
|
||||
function docsHtml(basePath = "") {
|
||||
const openApiUrl = `${basePath}/openapi.json`.replace("//", "/");
|
||||
const resources = resourceRoutes
|
||||
.map(
|
||||
(route) => `<tr>
|
||||
<td>${route.tag}</td>
|
||||
<td><code>GET ${basePath}/api/${route.slug}</code></td>
|
||||
<td><code>GET ${basePath}/api/${route.slug}/{id}</code></td>
|
||||
<td><code>POST ${basePath}/api/${route.slug}</code></td>
|
||||
<td><code>PATCH ${basePath}/api/${route.slug}/{id}</code></td>
|
||||
<td>${route.deleteSupported ? `<code>DELETE ${basePath}/api/${route.slug}/{id}</code>` : "not generally safe"}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>SAP Business One connector docs</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 2rem; line-height: 1.5; color: #1f2937; }
|
||||
code, pre { background: #f3f4f6; border-radius: 4px; padding: 0.2rem 0.35rem; }
|
||||
a, button { color: #0f766e; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
||||
th, td { border: 1px solid #d1d5db; padding: 0.5rem; text-align: left; vertical-align: top; }
|
||||
th { background: #f9fafb; }
|
||||
input, textarea, select, button { font: inherit; margin: 0.25rem 0; padding: 0.35rem; }
|
||||
textarea { width: min(760px, 100%); height: 9rem; display: block; }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: end; }
|
||||
.field { display: grid; gap: 0.2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SAP Business One connector</h1>
|
||||
<p>HTTP API over SAP Business One Service Layer. SAP credentials are supplied in X-SAP-B1-* request headers and are never logged or returned.</p>
|
||||
<p><a href="${openApiUrl}">OpenAPI JSON</a></p>
|
||||
|
||||
<h2>SAP credentials headers</h2>
|
||||
<div class="row">
|
||||
<label class="field">Base URL
|
||||
<input id="sap-base-url" size="36" placeholder="https://sap-host:50000">
|
||||
</label>
|
||||
<label class="field">Company DB
|
||||
<input id="sap-company-db" size="18" placeholder="SBODEMO">
|
||||
</label>
|
||||
<label class="field">Username
|
||||
<input id="sap-username" size="18">
|
||||
</label>
|
||||
<label class="field">Password
|
||||
<input id="sap-password" type="password" size="18">
|
||||
</label>
|
||||
<label class="field">Language
|
||||
<input id="sap-language" size="10">
|
||||
</label>
|
||||
<label class="field">Reject unauthorized
|
||||
<select id="sap-reject-unauthorized">
|
||||
<option value="true">true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">Timeout ms
|
||||
<input id="sap-timeout-ms" size="10" value="30000">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h2>Session</h2>
|
||||
<button type="button" data-method="POST" data-url="${basePath}/api/session/login">Login</button>
|
||||
<button type="button" data-method="POST" data-url="${basePath}/api/session/logout">Logout</button>
|
||||
<button type="button" data-method="GET" data-url="${basePath}/health">Health</button>
|
||||
|
||||
<h2>Resources</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Resource</th><th>List</th><th>Get</th><th>Create</th><th>Update</th><th>Delete</th></tr>
|
||||
</thead>
|
||||
<tbody>${resources}</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Try request</h2>
|
||||
<div class="row">
|
||||
<label class="field">Method
|
||||
<select id="method">
|
||||
<option>GET</option>
|
||||
<option>POST</option>
|
||||
<option>PATCH</option>
|
||||
<option>DELETE</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">Path
|
||||
<input id="path" size="48" value="${basePath}/api/business-partners?$top=10">
|
||||
</label>
|
||||
<button type="button" id="send">Send</button>
|
||||
</div>
|
||||
<label>JSON body</label>
|
||||
<textarea id="body">{}</textarea>
|
||||
<pre id="result" aria-live="polite"></pre>
|
||||
|
||||
<script>
|
||||
const result = document.getElementById("result");
|
||||
const method = document.getElementById("method");
|
||||
const path = document.getElementById("path");
|
||||
const body = document.getElementById("body");
|
||||
function sapHeaders() {
|
||||
const headers = {
|
||||
"X-SAP-B1-BaseUrl": document.getElementById("sap-base-url").value,
|
||||
"X-SAP-B1-CompanyDB": document.getElementById("sap-company-db").value,
|
||||
"X-SAP-B1-Username": document.getElementById("sap-username").value,
|
||||
"X-SAP-B1-Password": document.getElementById("sap-password").value,
|
||||
"X-SAP-B1-Reject-Unauthorized": document.getElementById("sap-reject-unauthorized").value,
|
||||
"X-SAP-B1-Timeout-Ms": document.getElementById("sap-timeout-ms").value
|
||||
};
|
||||
const language = document.getElementById("sap-language").value;
|
||||
if (language) {
|
||||
headers["X-SAP-B1-Language"] = language;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
async function sendRequest(requestMethod, requestUrl, requestBody) {
|
||||
const options = { method: requestMethod, headers: sapHeaders() };
|
||||
if (requestMethod !== "GET" && requestMethod !== "DELETE") {
|
||||
options.headers["content-type"] = "application/json";
|
||||
options.body = requestBody || "{}";
|
||||
}
|
||||
const response = await fetch(requestUrl, options);
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = response.status === 204 ? null : contentType.includes("application/json") ? await response.json() : await response.text();
|
||||
result.textContent = JSON.stringify({ status: response.status, body: payload }, null, 2);
|
||||
}
|
||||
document.querySelectorAll("[data-method][data-url]").forEach((button) => {
|
||||
button.addEventListener("click", () => sendRequest(button.dataset.method, button.dataset.url));
|
||||
});
|
||||
document.getElementById("send").addEventListener("click", () => sendRequest(method.value, path.value, body.value));
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
function swaggerOptions(basePath = "") {
|
||||
return {
|
||||
customSiteTitle: "SAP Business One connector API",
|
||||
swaggerOptions: {
|
||||
url: `${basePath}/openapi.json`.replace("//", "/"),
|
||||
displayRequestDuration: true,
|
||||
persistAuthorization: true,
|
||||
tryItOutEnabled: true,
|
||||
filter: true,
|
||||
tagsSorter: "alpha",
|
||||
operationsSorter: "method"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
app.get("/", (_req, res) => {
|
||||
@@ -723,12 +608,10 @@ app.get("/openapi.json", (_req, res) => {
|
||||
res.json(openApiDocument(rootPath));
|
||||
});
|
||||
|
||||
app.get("/docs", (_req, res) => {
|
||||
res.type("html").send(docsHtml(rootPath));
|
||||
});
|
||||
|
||||
addResourceEndpoints();
|
||||
|
||||
app.use("/docs", swaggerUi.serveFiles(undefined, swaggerOptions("")), swaggerUi.setup(undefined, swaggerOptions("")));
|
||||
|
||||
if (rootPath) {
|
||||
app.get(rootPath, (_req, res) => {
|
||||
res.json(serviceMetadata());
|
||||
@@ -742,11 +625,8 @@ if (rootPath) {
|
||||
res.json(openApiDocument(rootPath));
|
||||
});
|
||||
|
||||
app.get(rootPath + "/docs", (_req, res) => {
|
||||
res.type("html").send(docsHtml(rootPath));
|
||||
});
|
||||
|
||||
addResourceEndpoints(rootPath);
|
||||
app.use(rootPath + "/docs", swaggerUi.serveFiles(undefined, swaggerOptions(rootPath)), swaggerUi.setup(undefined, swaggerOptions(rootPath)));
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
||||
Reference in New Issue
Block a user