diff --git a/README.md b/README.md index 2050a0e..5c1564b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index f898471..d8576c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bb563da..1863a41 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/index.ts b/src/index.ts index 83ab4eb..d98c8b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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) => ` - ${route.tag} - GET ${basePath}/api/${route.slug} - GET ${basePath}/api/${route.slug}/{id} - POST ${basePath}/api/${route.slug} - PATCH ${basePath}/api/${route.slug}/{id} - ${route.deleteSupported ? `DELETE ${basePath}/api/${route.slug}/{id}` : "not generally safe"} - ` - ) - .join(""); - - return ` - - - - - SAP Business One connector docs - - - -

SAP Business One connector

-

HTTP API over SAP Business One Service Layer. SAP credentials are supplied in X-SAP-B1-* request headers and are never logged or returned.

-

OpenAPI JSON

- -

SAP credentials headers

-
- - - - - - - -
- -

Session

- - - - -

Resources

- - - - - ${resources} -
ResourceListGetCreateUpdateDelete
- -

Try request

-
- - - -
- - -

-
-    
-  
-`;
+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) {