fix
This commit is contained in:
@@ -186,12 +186,12 @@ 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` je Swagger UI, které načítá OpenAPI dokument přes relativní endpoint
|
`/docs` je Swagger UI servírované přímo jako HTML (stejně jako sousední `google-service`),
|
||||||
`openapi.json` (servírovaný jako `/docs/openapi.json`, alias `/openapi.json`). Díky tomu
|
bez statického middleware – proto nevzniká redirect `/docs` → `/docs/`, který by za proxy
|
||||||
se dokument načte lokálně i za AppFactory proxy bez hardcodování prefixu. V Swaggeru použij
|
zahodil prefix. Assety se načítají z CDN, spec URL je `ROOT_PATH + /openapi.json`. V Swaggeru
|
||||||
`Authorize` pro vyplnění povinných `X-SAP-B1-*` hlaviček a potom `Try it out` u konkrétní
|
použij `Authorize` pro vyplnění povinných `X-SAP-B1-*` hlaviček a potom `Try it out` u
|
||||||
operace. OpenAPI `servers` se nastaví z `ROOT_PATH` (fallback `X-Forwarded-Prefix`, jinak
|
konkrétní operace. OpenAPI `servers` se nastaví z `ROOT_PATH` (jinak `/`), takže za proxy
|
||||||
`/`), takže za proxy volá například `/apps/sap-bo/api/business-partners`, ne root doménu.
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -50,19 +50,21 @@ HTTP API hlavičky:
|
|||||||
|
|
||||||
Aplikace běží za AppFactory proxy na `/apps/<app-id>` (Caddy `handle_path` prefix před
|
Aplikace běží za AppFactory proxy na `/apps/<app-id>` (Caddy `handle_path` prefix před
|
||||||
předáním do containeru odstraní, takže container vidí routy bez prefixu a z requestu
|
předáním do containeru odstraní, takže container vidí routy bez prefixu a z requestu
|
||||||
veřejnou cestu nelze odvodit). Stejný přístup jako sousední služba `idoklad`:
|
veřejnou cestu nelze odvodit). Stejný přístup jako sousední služba `google-service`:
|
||||||
|
|
||||||
- OpenAPI dokument se servíruje **pod stejným `/docs` prefixem** jako UI na
|
- `/docs` se servíruje **přímo jako HTML** (`GET /docs`), bez `swagger-ui-express` a bez
|
||||||
`GET /docs/openapi.json` (plus alias `GET /openapi.json` pro přímý přístup).
|
statického middleware – proto **nevzniká žádný redirect `/docs` → `/docs/`**, který by za
|
||||||
- Swagger UI načítá dokument přes **relativní** endpoint `openapi.json`, takže se
|
proxy zahodil prefix `/apps/<app-id>` (to byla příčina bílé stránky).
|
||||||
v prohlížeči vyhodnotí jako `{prefix}/docs/openapi.json` lokálně i za proxy, bez
|
- Swagger UI assety se načítají z CDN (`unpkg.com/swagger-ui-dist@5`).
|
||||||
hardcodování `/apps/<app-id>`.
|
- Spec URL je `ROOT_PATH + /openapi.json`, takže odkazuje na veřejné
|
||||||
- `servers[0].url` se nastaví z `ROOT_PATH` (fallback hlavička `X-Forwarded-Prefix`,
|
`/apps/<app-id>/openapi.json`.
|
||||||
nakonec `/`), takže Swagger `Try it out` volá `…/apps/<app-id>/api/<resource>`.
|
- `servers[0].url` se nastaví z `ROOT_PATH` (jinak `/`), takže Swagger `Try it out` volá
|
||||||
|
`…/apps/<app-id>/api/<resource>`.
|
||||||
|
|
||||||
| Kontrola | Lokálně (bez ROOT_PATH) | S `ROOT_PATH=/apps/sap-bo` |
|
| Kontrola | Lokálně (bez ROOT_PATH) | S `ROOT_PATH=/apps/sap-bo` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GET /health` | 200 | 200 |
|
| `GET /health` | 200 | 200 |
|
||||||
| `GET /docs` (Swagger UI) | 200 | 200 |
|
| `GET /docs` (Swagger UI) | 200, žádný redirect | 200, žádný redirect |
|
||||||
| `GET /docs/openapi.json` | 200 | 200 |
|
| `GET /openapi.json` | 200 | 200 |
|
||||||
|
| spec URL v `/docs` | `/openapi.json` | `/apps/sap-bo/openapi.json` |
|
||||||
| `servers[0].url` v OpenAPI | `/` | `/apps/sap-bo` |
|
| `servers[0].url` v OpenAPI | `/` | `/apps/sap-bo` |
|
||||||
|
|||||||
Generated
-44
@@ -11,13 +11,11 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -356,13 +354,6 @@
|
|||||||
"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",
|
||||||
@@ -524,17 +515,6 @@
|
|||||||
"@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",
|
||||||
@@ -2131,30 +2111,6 @@
|
|||||||
"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,13 +14,11 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-45
@@ -1,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -38,11 +37,25 @@ interface ResourceRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
app.set("trust proxy", true);
|
||||||
const port = Number(process.env.PORT || 3000);
|
const port = Number(process.env.PORT || 3000);
|
||||||
const rootPath = process.env.ROOT_PATH || "";
|
const rootPath = normalizeRootPath(process.env.ROOT_PATH || "");
|
||||||
const serviceName = "SAP Business One";
|
const serviceName = "SAP Business One";
|
||||||
const serviceId = "sap-bo";
|
const serviceId = "sap-bo";
|
||||||
|
|
||||||
|
function normalizeRootPath(value: string): string {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const withLeadingSlash = value.startsWith("/") ? value : "/" + value;
|
||||||
|
return withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withRootPath(path: string): string {
|
||||||
|
return rootPath + path;
|
||||||
|
}
|
||||||
|
|
||||||
const resourceRoutes: ResourceRoute[] = [
|
const resourceRoutes: ResourceRoute[] = [
|
||||||
{
|
{
|
||||||
slug: "business-partners",
|
slug: "business-partners",
|
||||||
@@ -581,31 +594,42 @@ function openApiDocument(basePath = "") {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBasePath(req: Request): string {
|
function renderDocsHtml(): string {
|
||||||
// Behind the AppFactory reverse proxy the public prefix is /apps/<app-id>, but the
|
// Same approach as the sibling google-service: serve the Swagger UI page directly at /docs
|
||||||
// handle_path route strips it before the request reaches this container, so it cannot be
|
// (no static-file middleware, so no /docs -> /docs/ redirect that would drop the proxy prefix),
|
||||||
// read from the request path. ROOT_PATH is the sanctioned source; X-Forwarded-Prefix is a
|
// load the UI assets from CDN, and point the spec URL at ROOT_PATH + /openapi.json so it resolves
|
||||||
// fallback for proxies that forward it. Empty when running without a proxy.
|
// to the public /apps/<app-id>/openapi.json behind the AppFactory reverse proxy.
|
||||||
const forwardedPrefix = getHeader(req, "x-forwarded-prefix");
|
const specUrl = withRootPath("/openapi.json");
|
||||||
return (rootPath || forwardedPrefix || "").replace(/\/+$/, "");
|
return `<!doctype html>
|
||||||
}
|
<html lang="en">
|
||||||
|
<head>
|
||||||
function swaggerUiOptions() {
|
<meta charset="utf-8">
|
||||||
return {
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
customSiteTitle: "SAP Business One connector API",
|
<title>SAP Business One connector API</title>
|
||||||
swaggerOptions: {
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||||||
// Relative endpoint (same approach as the sibling idoklad service): the browser resolves
|
<style>
|
||||||
// it against the docs page, i.e. {prefix}/docs/openapi.json, so the spec loads locally and
|
body { margin: 0; background: #f7f7f7; }
|
||||||
// behind the AppFactory proxy without hardcoding the /apps/<app-id> prefix.
|
.topbar { display: none; }
|
||||||
url: "openapi.json",
|
</style>
|
||||||
displayRequestDuration: true,
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||||
|
<script>
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
url: ${JSON.stringify(specUrl)},
|
||||||
|
dom_id: "#swagger-ui",
|
||||||
|
deepLinking: true,
|
||||||
persistAuthorization: true,
|
persistAuthorization: true,
|
||||||
tryItOutEnabled: true,
|
tryItOutEnabled: true,
|
||||||
filter: true,
|
filter: true,
|
||||||
|
displayRequestDuration: true,
|
||||||
tagsSorter: "alpha",
|
tagsSorter: "alpha",
|
||||||
operationsSorter: "method"
|
operationsSorter: "method"
|
||||||
}
|
});
|
||||||
};
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/", (_req, res) => {
|
app.get("/", (_req, res) => {
|
||||||
@@ -616,33 +640,16 @@ app.get("/health", (_req, res) => {
|
|||||||
res.json({ status: "ok" });
|
res.json({ status: "ok" });
|
||||||
});
|
});
|
||||||
|
|
||||||
// OpenAPI document served under the same /docs prefix as the UI so the relative UI endpoint
|
app.get("/docs", (_req, res) => {
|
||||||
// resolves correctly locally and behind the reverse-proxy prefix. servers[0].url advertises the
|
res.type("html").send(renderDocsHtml());
|
||||||
// public prefix (ROOT_PATH) so Swagger UI "Try it out" targets {prefix}/api/..., not the host root.
|
|
||||||
app.get("/docs/openapi.json", (req, res) => {
|
|
||||||
res.json(openApiDocument(resolveBasePath(req)));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Documented root alias for direct access to the OpenAPI document.
|
// servers[0].url advertises the public prefix (ROOT_PATH) so Swagger UI "Try it out" targets
|
||||||
app.get("/openapi.json", (req, res) => {
|
// {prefix}/api/..., not the host root.
|
||||||
res.json(openApiDocument(resolveBasePath(req)));
|
app.get("/openapi.json", (_req, res) => {
|
||||||
|
res.json(openApiDocument(rootPath));
|
||||||
});
|
});
|
||||||
|
|
||||||
// swagger-ui-express's static handler redirects /docs -> absolute "/docs/", which behind the
|
|
||||||
// AppFactory handle_path proxy drops the /apps/<app-id> prefix and breaks the page. Intercept
|
|
||||||
// the no-slash form first and redirect to the ROOT_PATH-prefixed URL (mirrors idoklad's
|
|
||||||
// UsePathBase), so the trailing-slash form keeps the public prefix.
|
|
||||||
app.get("/docs", (req, res, next) => {
|
|
||||||
// Express non-strict routing matches both /docs and /docs/ here; only the no-slash form
|
|
||||||
// needs the redirect (otherwise /docs/ would redirect to itself in a loop).
|
|
||||||
if (req.path.endsWith("/")) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
res.redirect(301, `${resolveBasePath(req)}/docs/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use("/docs", swaggerUi.serve, swaggerUi.setup(undefined, swaggerUiOptions()));
|
|
||||||
|
|
||||||
addResourceEndpoints();
|
addResourceEndpoints();
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|||||||
Reference in New Issue
Block a user