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`.
|
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
|
## TODO ověřit v konkrétní instalaci SAP Business One
|
||||||
|
|
||||||
|
|||||||
Generated
+44
@@ -11,11 +11,13 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"typescript": "^5.4.0",
|
"typescript": "^5.4.0",
|
||||||
"vitest": "^4.1.9"
|
"vitest": "^4.1.9"
|
||||||
}
|
}
|
||||||
@@ -354,6 +356,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
@@ -515,6 +524,17 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
|
||||||
@@ -2111,6 +2131,30 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"typescript": "^5.4.0",
|
"typescript": "^5.4.0",
|
||||||
"vitest": "^4.1.9"
|
"vitest": "^4.1.9"
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-145
@@ -1,4 +1,5 @@
|
|||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
|
import swaggerUi from "swagger-ui-express";
|
||||||
import { SapBusinessOneServiceLayer } from "./SapBusinessOneServiceLayer";
|
import { SapBusinessOneServiceLayer } from "./SapBusinessOneServiceLayer";
|
||||||
import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
|
import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
|
||||||
import { buildODataParams, extractNextLink } from "./client/odata";
|
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."
|
"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 || "/" }],
|
servers: [{ url: basePath || "/" }],
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
SapB1BaseUrl: [],
|
||||||
|
SapB1CompanyDB: [],
|
||||||
|
SapB1Username: [],
|
||||||
|
SapB1Password: []
|
||||||
|
}
|
||||||
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: "Service" },
|
{ name: "Service" },
|
||||||
{ name: "Session" },
|
{ name: "Session" },
|
||||||
@@ -572,143 +581,19 @@ function openApiDocument(basePath = "") {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function docsHtml(basePath = "") {
|
function swaggerOptions(basePath = "") {
|
||||||
const openApiUrl = `${basePath}/openapi.json`.replace("//", "/");
|
return {
|
||||||
const resources = resourceRoutes
|
customSiteTitle: "SAP Business One connector API",
|
||||||
.map(
|
swaggerOptions: {
|
||||||
(route) => `<tr>
|
url: `${basePath}/openapi.json`.replace("//", "/"),
|
||||||
<td>${route.tag}</td>
|
displayRequestDuration: true,
|
||||||
<td><code>GET ${basePath}/api/${route.slug}</code></td>
|
persistAuthorization: true,
|
||||||
<td><code>GET ${basePath}/api/${route.slug}/{id}</code></td>
|
tryItOutEnabled: true,
|
||||||
<td><code>POST ${basePath}/api/${route.slug}</code></td>
|
filter: true,
|
||||||
<td><code>PATCH ${basePath}/api/${route.slug}/{id}</code></td>
|
tagsSorter: "alpha",
|
||||||
<td>${route.deleteSupported ? `<code>DELETE ${basePath}/api/${route.slug}/{id}</code>` : "not generally safe"}</td>
|
operationsSorter: "method"
|
||||||
</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>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/", (_req, res) => {
|
app.get("/", (_req, res) => {
|
||||||
@@ -723,12 +608,10 @@ app.get("/openapi.json", (_req, res) => {
|
|||||||
res.json(openApiDocument(rootPath));
|
res.json(openApiDocument(rootPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/docs", (_req, res) => {
|
|
||||||
res.type("html").send(docsHtml(rootPath));
|
|
||||||
});
|
|
||||||
|
|
||||||
addResourceEndpoints();
|
addResourceEndpoints();
|
||||||
|
|
||||||
|
app.use("/docs", swaggerUi.serveFiles(undefined, swaggerOptions("")), swaggerUi.setup(undefined, swaggerOptions("")));
|
||||||
|
|
||||||
if (rootPath) {
|
if (rootPath) {
|
||||||
app.get(rootPath, (_req, res) => {
|
app.get(rootPath, (_req, res) => {
|
||||||
res.json(serviceMetadata());
|
res.json(serviceMetadata());
|
||||||
@@ -742,11 +625,8 @@ if (rootPath) {
|
|||||||
res.json(openApiDocument(rootPath));
|
res.json(openApiDocument(rootPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(rootPath + "/docs", (_req, res) => {
|
|
||||||
res.type("html").send(docsHtml(rootPath));
|
|
||||||
});
|
|
||||||
|
|
||||||
addResourceEndpoints(rootPath);
|
addResourceEndpoints(rootPath);
|
||||||
|
app.use(rootPath + "/docs", swaggerUi.serveFiles(undefined, swaggerOptions(rootPath)), swaggerUi.setup(undefined, swaggerOptions(rootPath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|||||||
Reference in New Issue
Block a user