imposters 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Program/package.json +6 -0
- package/README.md +365 -0
- package/api/AdminApi/package.json +6 -0
- package/api/ApiErrors/package.json +6 -0
- package/api/ApiSchemas/package.json +6 -0
- package/api/Conversions/package.json +6 -0
- package/api/ImpostersGroup/package.json +6 -0
- package/api/ImpostersHandlers/package.json +6 -0
- package/api/SystemGroup/package.json +6 -0
- package/api/SystemHandlers/package.json +6 -0
- package/bin/imposters +47 -0
- package/cli/Commands/package.json +6 -0
- package/cli/ConfigLoader/package.json +6 -0
- package/client/HandlerHttpClient/package.json +6 -0
- package/client/ImpostersClient/package.json +6 -0
- package/client/testing/package.json +6 -0
- package/dist/cjs/Program.js +4 -0
- package/dist/cjs/Program.js.map +1 -0
- package/dist/cjs/api/AdminApi.js +11 -0
- package/dist/cjs/api/AdminApi.js.map +1 -0
- package/dist/cjs/api/ApiErrors.js +30 -0
- package/dist/cjs/api/ApiErrors.js.map +1 -0
- package/dist/cjs/api/ApiSchemas.js +36 -0
- package/dist/cjs/api/ApiSchemas.js.map +1 -0
- package/dist/cjs/api/Conversions.js +41 -0
- package/dist/cjs/api/Conversions.js.map +1 -0
- package/dist/cjs/api/ImpostersGroup.js +37 -0
- package/dist/cjs/api/ImpostersGroup.js.map +1 -0
- package/dist/cjs/api/ImpostersHandlers.js +361 -0
- package/dist/cjs/api/ImpostersHandlers.js.map +1 -0
- package/dist/cjs/api/SystemGroup.js +12 -0
- package/dist/cjs/api/SystemGroup.js.map +1 -0
- package/dist/cjs/api/SystemHandlers.js +74 -0
- package/dist/cjs/api/SystemHandlers.js.map +1 -0
- package/dist/cjs/cli/Commands.js +104 -0
- package/dist/cjs/cli/Commands.js.map +1 -0
- package/dist/cjs/cli/ConfigLoader.js +34 -0
- package/dist/cjs/cli/ConfigLoader.js.map +1 -0
- package/dist/cjs/client/HandlerHttpClient.js +50 -0
- package/dist/cjs/client/HandlerHttpClient.js.map +1 -0
- package/dist/cjs/client/ImpostersClient.js +20 -0
- package/dist/cjs/client/ImpostersClient.js.map +1 -0
- package/dist/cjs/client/index.js +57 -0
- package/dist/cjs/client/index.js.map +1 -0
- package/dist/cjs/client/testing.js +94 -0
- package/dist/cjs/client/testing.js.map +1 -0
- package/dist/cjs/domain/imposter.js +125 -0
- package/dist/cjs/domain/imposter.js.map +1 -0
- package/dist/cjs/domain/route.js +185 -0
- package/dist/cjs/domain/route.js.map +1 -0
- package/dist/cjs/index.js +106 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/layers/ApiLayer.js +18 -0
- package/dist/cjs/layers/ApiLayer.js.map +1 -0
- package/dist/cjs/layers/MainLayer.js +27 -0
- package/dist/cjs/layers/MainLayer.js.map +1 -0
- package/dist/cjs/matching/ExpressionEvaluator.js +103 -0
- package/dist/cjs/matching/ExpressionEvaluator.js.map +1 -0
- package/dist/cjs/matching/RequestMatcher.js +145 -0
- package/dist/cjs/matching/RequestMatcher.js.map +1 -0
- package/dist/cjs/matching/ResponseGenerator.js +80 -0
- package/dist/cjs/matching/ResponseGenerator.js.map +1 -0
- package/dist/cjs/matching/TemplateEngine.js +55 -0
- package/dist/cjs/matching/TemplateEngine.js.map +1 -0
- package/dist/cjs/repositories/ImposterRepository.js +118 -0
- package/dist/cjs/repositories/ImposterRepository.js.map +1 -0
- package/dist/cjs/schemas/ConfigFileSchema.js +44 -0
- package/dist/cjs/schemas/ConfigFileSchema.js.map +1 -0
- package/dist/cjs/schemas/ImposterSchema.js +202 -0
- package/dist/cjs/schemas/ImposterSchema.js.map +1 -0
- package/dist/cjs/schemas/RequestLogSchema.js +51 -0
- package/dist/cjs/schemas/RequestLogSchema.js.map +1 -0
- package/dist/cjs/schemas/StubSchema.js +84 -0
- package/dist/cjs/schemas/StubSchema.js.map +1 -0
- package/dist/cjs/schemas/common.js +67 -0
- package/dist/cjs/schemas/common.js.map +1 -0
- package/dist/cjs/server/AdminServer.js +36 -0
- package/dist/cjs/server/AdminServer.js.map +1 -0
- package/dist/cjs/server/BunServer.js +13 -0
- package/dist/cjs/server/BunServer.js.map +1 -0
- package/dist/cjs/server/FiberManager.js +21 -0
- package/dist/cjs/server/FiberManager.js.map +1 -0
- package/dist/cjs/server/ImposterServer.js +234 -0
- package/dist/cjs/server/ImposterServer.js.map +1 -0
- package/dist/cjs/services/AppConfig.js +18 -0
- package/dist/cjs/services/AppConfig.js.map +1 -0
- package/dist/cjs/services/MetricsService.js +113 -0
- package/dist/cjs/services/MetricsService.js.map +1 -0
- package/dist/cjs/services/PortAllocator.js +50 -0
- package/dist/cjs/services/PortAllocator.js.map +1 -0
- package/dist/cjs/services/ProxyService.js +109 -0
- package/dist/cjs/services/ProxyService.js.map +1 -0
- package/dist/cjs/services/RequestLogger.js +60 -0
- package/dist/cjs/services/RequestLogger.js.map +1 -0
- package/dist/cjs/services/Uuid.js +10 -0
- package/dist/cjs/services/Uuid.js.map +1 -0
- package/dist/cjs/services/UuidLive.js +16 -0
- package/dist/cjs/services/UuidLive.js.map +1 -0
- package/dist/cjs/ui/UiRouter.js +242 -0
- package/dist/cjs/ui/UiRouter.js.map +1 -0
- package/dist/cjs/ui/admin/AdminLayout.js +36 -0
- package/dist/cjs/ui/admin/AdminLayout.js.map +1 -0
- package/dist/cjs/ui/admin/AdminUiRouter.js +155 -0
- package/dist/cjs/ui/admin/AdminUiRouter.js.map +1 -0
- package/dist/cjs/ui/admin/pages/AdminDashboard.js +55 -0
- package/dist/cjs/ui/admin/pages/AdminDashboard.js.map +1 -0
- package/dist/cjs/ui/admin/partials.js +64 -0
- package/dist/cjs/ui/admin/partials.js.map +1 -0
- package/dist/cjs/ui/html.js +42 -0
- package/dist/cjs/ui/html.js.map +1 -0
- package/dist/cjs/ui/layout.js +39 -0
- package/dist/cjs/ui/layout.js.map +1 -0
- package/dist/cjs/ui/pages/dashboard.js +51 -0
- package/dist/cjs/ui/pages/dashboard.js.map +1 -0
- package/dist/cjs/ui/pages/request-detail.js +119 -0
- package/dist/cjs/ui/pages/request-detail.js.map +1 -0
- package/dist/cjs/ui/pages/requests.js +120 -0
- package/dist/cjs/ui/pages/requests.js.map +1 -0
- package/dist/cjs/ui/pages/stubs.js +46 -0
- package/dist/cjs/ui/pages/stubs.js.map +1 -0
- package/dist/cjs/ui/partials.js +104 -0
- package/dist/cjs/ui/partials.js.map +1 -0
- package/dist/dts/Program.d.ts +2 -0
- package/dist/dts/Program.d.ts.map +1 -0
- package/dist/dts/api/AdminApi.d.ts +490 -0
- package/dist/dts/api/AdminApi.d.ts.map +1 -0
- package/dist/dts/api/ApiErrors.d.ts +26 -0
- package/dist/dts/api/ApiErrors.d.ts.map +1 -0
- package/dist/dts/api/ApiSchemas.d.ts +36 -0
- package/dist/dts/api/ApiSchemas.d.ts.map +1 -0
- package/dist/dts/api/Conversions.d.ts +7 -0
- package/dist/dts/api/Conversions.d.ts.map +1 -0
- package/dist/dts/api/ImpostersGroup.d.ts +448 -0
- package/dist/dts/api/ImpostersGroup.d.ts.map +1 -0
- package/dist/dts/api/ImpostersHandlers.d.ts +9 -0
- package/dist/dts/api/ImpostersHandlers.d.ts.map +1 -0
- package/dist/dts/api/SystemGroup.d.ts +46 -0
- package/dist/dts/api/SystemGroup.d.ts.map +1 -0
- package/dist/dts/api/SystemHandlers.d.ts +4 -0
- package/dist/dts/api/SystemHandlers.d.ts.map +1 -0
- package/dist/dts/cli/Commands.d.ts +4 -0
- package/dist/dts/cli/Commands.d.ts.map +1 -0
- package/dist/dts/cli/ConfigLoader.d.ts +13 -0
- package/dist/dts/cli/ConfigLoader.d.ts.map +1 -0
- package/dist/dts/client/HandlerHttpClient.d.ts +5 -0
- package/dist/dts/client/HandlerHttpClient.d.ts.map +1 -0
- package/dist/dts/client/ImpostersClient.d.ts +1868 -0
- package/dist/dts/client/ImpostersClient.d.ts.map +1 -0
- package/dist/dts/client/index.d.ts +6 -0
- package/dist/dts/client/index.d.ts.map +1 -0
- package/dist/dts/client/testing.d.ts +35 -0
- package/dist/dts/client/testing.d.ts.map +1 -0
- package/dist/dts/domain/imposter.d.ts +123 -0
- package/dist/dts/domain/imposter.d.ts.map +1 -0
- package/dist/dts/domain/route.d.ts +128 -0
- package/dist/dts/domain/route.d.ts.map +1 -0
- package/dist/dts/index.d.ts +60 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/layers/ApiLayer.d.ts +3 -0
- package/dist/dts/layers/ApiLayer.d.ts.map +1 -0
- package/dist/dts/layers/MainLayer.d.ts +3 -0
- package/dist/dts/layers/MainLayer.d.ts.map +1 -0
- package/dist/dts/matching/ExpressionEvaluator.d.ts +11 -0
- package/dist/dts/matching/ExpressionEvaluator.d.ts.map +1 -0
- package/dist/dts/matching/RequestMatcher.d.ts +13 -0
- package/dist/dts/matching/RequestMatcher.d.ts.map +1 -0
- package/dist/dts/matching/ResponseGenerator.d.ts +9 -0
- package/dist/dts/matching/ResponseGenerator.d.ts.map +1 -0
- package/dist/dts/matching/TemplateEngine.d.ts +4 -0
- package/dist/dts/matching/TemplateEngine.d.ts.map +1 -0
- package/dist/dts/repositories/ImposterRepository.d.ts +33 -0
- package/dist/dts/repositories/ImposterRepository.d.ts.map +1 -0
- package/dist/dts/schemas/ConfigFileSchema.d.ts +142 -0
- package/dist/dts/schemas/ConfigFileSchema.d.ts.map +1 -0
- package/dist/dts/schemas/ImposterSchema.d.ts +368 -0
- package/dist/dts/schemas/ImposterSchema.d.ts.map +1 -0
- package/dist/dts/schemas/RequestLogSchema.d.ts +36 -0
- package/dist/dts/schemas/RequestLogSchema.d.ts.map +1 -0
- package/dist/dts/schemas/StubSchema.d.ts +112 -0
- package/dist/dts/schemas/StubSchema.d.ts.map +1 -0
- package/dist/dts/schemas/common.d.ts +56 -0
- package/dist/dts/schemas/common.d.ts.map +1 -0
- package/dist/dts/server/AdminServer.d.ts +11 -0
- package/dist/dts/server/AdminServer.d.ts.map +1 -0
- package/dist/dts/server/BunServer.d.ts +17 -0
- package/dist/dts/server/BunServer.d.ts.map +1 -0
- package/dist/dts/server/FiberManager.d.ts +12 -0
- package/dist/dts/server/FiberManager.d.ts.map +1 -0
- package/dist/dts/server/ImposterServer.d.ts +29 -0
- package/dist/dts/server/ImposterServer.d.ts.map +1 -0
- package/dist/dts/services/AppConfig.d.ts +14 -0
- package/dist/dts/services/AppConfig.d.ts.map +1 -0
- package/dist/dts/services/MetricsService.d.ts +26 -0
- package/dist/dts/services/MetricsService.d.ts.map +1 -0
- package/dist/dts/services/PortAllocator.d.ts +29 -0
- package/dist/dts/services/PortAllocator.d.ts.map +1 -0
- package/dist/dts/services/ProxyService.d.ts +24 -0
- package/dist/dts/services/ProxyService.d.ts.map +1 -0
- package/dist/dts/services/RequestLogger.d.ts +23 -0
- package/dist/dts/services/RequestLogger.d.ts.map +1 -0
- package/dist/dts/services/Uuid.d.ts +9 -0
- package/dist/dts/services/Uuid.d.ts.map +1 -0
- package/dist/dts/services/UuidLive.d.ts +4 -0
- package/dist/dts/services/UuidLive.d.ts.map +1 -0
- package/dist/dts/ui/UiRouter.d.ts +15 -0
- package/dist/dts/ui/UiRouter.d.ts.map +1 -0
- package/dist/dts/ui/admin/AdminLayout.d.ts +7 -0
- package/dist/dts/ui/admin/AdminLayout.d.ts.map +1 -0
- package/dist/dts/ui/admin/AdminUiRouter.d.ts +6 -0
- package/dist/dts/ui/admin/AdminUiRouter.d.ts.map +1 -0
- package/dist/dts/ui/admin/pages/AdminDashboard.d.ts +7 -0
- package/dist/dts/ui/admin/pages/AdminDashboard.d.ts.map +1 -0
- package/dist/dts/ui/admin/partials.d.ts +14 -0
- package/dist/dts/ui/admin/partials.d.ts.map +1 -0
- package/dist/dts/ui/html.d.ts +12 -0
- package/dist/dts/ui/html.d.ts.map +1 -0
- package/dist/dts/ui/layout.d.ts +9 -0
- package/dist/dts/ui/layout.d.ts.map +1 -0
- package/dist/dts/ui/pages/dashboard.d.ts +10 -0
- package/dist/dts/ui/pages/dashboard.d.ts.map +1 -0
- package/dist/dts/ui/pages/request-detail.d.ts +11 -0
- package/dist/dts/ui/pages/request-detail.d.ts.map +1 -0
- package/dist/dts/ui/pages/requests.d.ts +15 -0
- package/dist/dts/ui/pages/requests.d.ts.map +1 -0
- package/dist/dts/ui/pages/stubs.d.ts +8 -0
- package/dist/dts/ui/pages/stubs.d.ts.map +1 -0
- package/dist/dts/ui/partials.d.ts +13 -0
- package/dist/dts/ui/partials.d.ts.map +1 -0
- package/dist/esm/Program.js +2 -0
- package/dist/esm/Program.js.map +1 -0
- package/dist/esm/api/AdminApi.js +5 -0
- package/dist/esm/api/AdminApi.js.map +1 -0
- package/dist/esm/api/ApiErrors.js +20 -0
- package/dist/esm/api/ApiErrors.js.map +1 -0
- package/dist/esm/api/ApiSchemas.js +29 -0
- package/dist/esm/api/ApiSchemas.js.map +1 -0
- package/dist/esm/api/Conversions.js +32 -0
- package/dist/esm/api/Conversions.js.map +1 -0
- package/dist/esm/api/ImpostersGroup.js +30 -0
- package/dist/esm/api/ImpostersGroup.js.map +1 -0
- package/dist/esm/api/ImpostersHandlers.js +354 -0
- package/dist/esm/api/ImpostersHandlers.js.map +1 -0
- package/dist/esm/api/SystemGroup.js +6 -0
- package/dist/esm/api/SystemGroup.js.map +1 -0
- package/dist/esm/api/SystemHandlers.js +67 -0
- package/dist/esm/api/SystemHandlers.js.map +1 -0
- package/dist/esm/cli/Commands.js +98 -0
- package/dist/esm/cli/Commands.js.map +1 -0
- package/dist/esm/cli/ConfigLoader.js +25 -0
- package/dist/esm/cli/ConfigLoader.js.map +1 -0
- package/dist/esm/client/HandlerHttpClient.js +42 -0
- package/dist/esm/client/HandlerHttpClient.js.map +1 -0
- package/dist/esm/client/ImpostersClient.js +10 -0
- package/dist/esm/client/ImpostersClient.js.map +1 -0
- package/dist/esm/client/index.js +4 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/client/testing.js +86 -0
- package/dist/esm/client/testing.js.map +1 -0
- package/dist/esm/domain/imposter.js +103 -0
- package/dist/esm/domain/imposter.js.map +1 -0
- package/dist/esm/domain/route.js +164 -0
- package/dist/esm/domain/route.js.map +1 -0
- package/dist/esm/index.js +60 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/layers/ApiLayer.js +11 -0
- package/dist/esm/layers/ApiLayer.js.map +1 -0
- package/dist/esm/layers/MainLayer.js +20 -0
- package/dist/esm/layers/MainLayer.js.map +1 -0
- package/dist/esm/matching/ExpressionEvaluator.js +94 -0
- package/dist/esm/matching/ExpressionEvaluator.js.map +1 -0
- package/dist/esm/matching/RequestMatcher.js +135 -0
- package/dist/esm/matching/RequestMatcher.js.map +1 -0
- package/dist/esm/matching/ResponseGenerator.js +71 -0
- package/dist/esm/matching/ResponseGenerator.js.map +1 -0
- package/dist/esm/matching/TemplateEngine.js +47 -0
- package/dist/esm/matching/TemplateEngine.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/repositories/ImposterRepository.js +110 -0
- package/dist/esm/repositories/ImposterRepository.js.map +1 -0
- package/dist/esm/schemas/ConfigFileSchema.js +37 -0
- package/dist/esm/schemas/ConfigFileSchema.js.map +1 -0
- package/dist/esm/schemas/ImposterSchema.js +195 -0
- package/dist/esm/schemas/ImposterSchema.js.map +1 -0
- package/dist/esm/schemas/RequestLogSchema.js +44 -0
- package/dist/esm/schemas/RequestLogSchema.js.map +1 -0
- package/dist/esm/schemas/StubSchema.js +77 -0
- package/dist/esm/schemas/StubSchema.js.map +1 -0
- package/dist/esm/schemas/common.js +59 -0
- package/dist/esm/schemas/common.js.map +1 -0
- package/dist/esm/server/AdminServer.js +27 -0
- package/dist/esm/server/AdminServer.js.map +1 -0
- package/dist/esm/server/BunServer.js +6 -0
- package/dist/esm/server/BunServer.js.map +1 -0
- package/dist/esm/server/FiberManager.js +14 -0
- package/dist/esm/server/FiberManager.js.map +1 -0
- package/dist/esm/server/ImposterServer.js +225 -0
- package/dist/esm/server/ImposterServer.js.map +1 -0
- package/dist/esm/services/AppConfig.js +11 -0
- package/dist/esm/services/AppConfig.js.map +1 -0
- package/dist/esm/services/MetricsService.js +105 -0
- package/dist/esm/services/MetricsService.js.map +1 -0
- package/dist/esm/services/PortAllocator.js +41 -0
- package/dist/esm/services/PortAllocator.js.map +1 -0
- package/dist/esm/services/ProxyService.js +101 -0
- package/dist/esm/services/ProxyService.js.map +1 -0
- package/dist/esm/services/RequestLogger.js +53 -0
- package/dist/esm/services/RequestLogger.js.map +1 -0
- package/dist/esm/services/Uuid.js +3 -0
- package/dist/esm/services/Uuid.js.map +1 -0
- package/dist/esm/services/UuidLive.js +9 -0
- package/dist/esm/services/UuidLive.js.map +1 -0
- package/dist/esm/ui/UiRouter.js +235 -0
- package/dist/esm/ui/UiRouter.js.map +1 -0
- package/dist/esm/ui/admin/AdminLayout.js +29 -0
- package/dist/esm/ui/admin/AdminLayout.js.map +1 -0
- package/dist/esm/ui/admin/AdminUiRouter.js +148 -0
- package/dist/esm/ui/admin/AdminUiRouter.js.map +1 -0
- package/dist/esm/ui/admin/pages/AdminDashboard.js +48 -0
- package/dist/esm/ui/admin/pages/AdminDashboard.js.map +1 -0
- package/dist/esm/ui/admin/partials.js +54 -0
- package/dist/esm/ui/admin/partials.js.map +1 -0
- package/dist/esm/ui/html.js +32 -0
- package/dist/esm/ui/html.js.map +1 -0
- package/dist/esm/ui/layout.js +32 -0
- package/dist/esm/ui/layout.js.map +1 -0
- package/dist/esm/ui/pages/dashboard.js +44 -0
- package/dist/esm/ui/pages/dashboard.js.map +1 -0
- package/dist/esm/ui/pages/request-detail.js +112 -0
- package/dist/esm/ui/pages/request-detail.js.map +1 -0
- package/dist/esm/ui/pages/requests.js +112 -0
- package/dist/esm/ui/pages/requests.js.map +1 -0
- package/dist/esm/ui/pages/stubs.js +39 -0
- package/dist/esm/ui/pages/stubs.js.map +1 -0
- package/dist/esm/ui/partials.js +91 -0
- package/dist/esm/ui/partials.js.map +1 -0
- package/domain/imposter/package.json +6 -0
- package/domain/route/package.json +6 -0
- package/layers/ApiLayer/package.json +6 -0
- package/layers/MainLayer/package.json +6 -0
- package/matching/ExpressionEvaluator/package.json +6 -0
- package/matching/RequestMatcher/package.json +6 -0
- package/matching/ResponseGenerator/package.json +6 -0
- package/matching/TemplateEngine/package.json +6 -0
- package/package.json +435 -0
- package/repositories/ImposterRepository/package.json +6 -0
- package/schemas/ConfigFileSchema/package.json +6 -0
- package/schemas/ImposterSchema/package.json +6 -0
- package/schemas/RequestLogSchema/package.json +6 -0
- package/schemas/StubSchema/package.json +6 -0
- package/schemas/common/package.json +6 -0
- package/server/AdminServer/package.json +6 -0
- package/server/BunServer/package.json +6 -0
- package/server/FiberManager/package.json +6 -0
- package/server/ImposterServer/package.json +6 -0
- package/services/AppConfig/package.json +6 -0
- package/services/MetricsService/package.json +6 -0
- package/services/PortAllocator/package.json +6 -0
- package/services/ProxyService/package.json +6 -0
- package/services/RequestLogger/package.json +6 -0
- package/services/Uuid/package.json +6 -0
- package/services/UuidLive/package.json +6 -0
- package/src/Program.ts +1 -0
- package/src/api/AdminApi.ts +7 -0
- package/src/api/ApiErrors.ts +20 -0
- package/src/api/ApiSchemas.ts +36 -0
- package/src/api/Conversions.ts +34 -0
- package/src/api/ImpostersGroup.ts +103 -0
- package/src/api/ImpostersHandlers.ts +387 -0
- package/src/api/SystemGroup.ts +12 -0
- package/src/api/SystemHandlers.ts +76 -0
- package/src/cli/Commands.ts +119 -0
- package/src/cli/ConfigLoader.ts +41 -0
- package/src/client/HandlerHttpClient.ts +50 -0
- package/src/client/ImpostersClient.ts +21 -0
- package/src/client/index.ts +9 -0
- package/src/client/testing.ts +105 -0
- package/src/domain/imposter.ts +186 -0
- package/src/domain/route.ts +255 -0
- package/src/index.ts +153 -0
- package/src/layers/ApiLayer.ts +21 -0
- package/src/layers/MainLayer.ts +43 -0
- package/src/matching/ExpressionEvaluator.ts +102 -0
- package/src/matching/RequestMatcher.ts +162 -0
- package/src/matching/ResponseGenerator.ts +86 -0
- package/src/matching/TemplateEngine.ts +54 -0
- package/src/repositories/ImposterRepository.ts +145 -0
- package/src/schemas/ConfigFileSchema.ts +32 -0
- package/src/schemas/ImposterSchema.ts +232 -0
- package/src/schemas/RequestLogSchema.ts +38 -0
- package/src/schemas/StubSchema.ts +90 -0
- package/src/schemas/common.ts +95 -0
- package/src/server/AdminServer.ts +22 -0
- package/src/server/BunServer.ts +19 -0
- package/src/server/FiberManager.ts +25 -0
- package/src/server/ImposterServer.ts +244 -0
- package/src/services/AppConfig.ts +22 -0
- package/src/services/MetricsService.ts +157 -0
- package/src/services/PortAllocator.ts +68 -0
- package/src/services/ProxyService.ts +139 -0
- package/src/services/RequestLogger.ts +87 -0
- package/src/services/Uuid.ts +9 -0
- package/src/services/UuidLive.ts +9 -0
- package/src/types/bun.d.ts +6 -0
- package/src/ui/UiRouter.ts +278 -0
- package/src/ui/admin/AdminLayout.ts +36 -0
- package/src/ui/admin/AdminUiRouter.ts +170 -0
- package/src/ui/admin/pages/AdminDashboard.ts +54 -0
- package/src/ui/admin/partials.ts +83 -0
- package/src/ui/html.ts +37 -0
- package/src/ui/layout.ts +44 -0
- package/src/ui/pages/dashboard.ts +64 -0
- package/src/ui/pages/request-detail.ts +142 -0
- package/src/ui/pages/requests.ts +141 -0
- package/src/ui/pages/stubs.ts +52 -0
- package/src/ui/partials.ts +133 -0
- package/ui/UiRouter/package.json +6 -0
- package/ui/admin/AdminLayout/package.json +6 -0
- package/ui/admin/AdminUiRouter/package.json +6 -0
- package/ui/admin/pages/AdminDashboard/package.json +6 -0
- package/ui/admin/partials/package.json +6 -0
- package/ui/html/package.json +6 -0
- package/ui/layout/package.json +6 -0
- package/ui/pages/dashboard/package.json +6 -0
- package/ui/pages/requests/package.json +6 -0
- package/ui/pages/stubs/package.json +6 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Queue, Scope } from "effect"
|
|
2
|
+
import { Context, Effect, HashMap, Layer, PubSub, Ref } from "effect"
|
|
3
|
+
import type { RequestLogEntry } from "../schemas/RequestLogSchema"
|
|
4
|
+
|
|
5
|
+
const MAX_ENTRIES = 100
|
|
6
|
+
|
|
7
|
+
export interface RequestLoggerShape {
|
|
8
|
+
readonly log: (entry: RequestLogEntry) => Effect.Effect<void>
|
|
9
|
+
readonly getEntries: (
|
|
10
|
+
imposterId: string,
|
|
11
|
+
opts?: { limit?: number; method?: string; path?: string; status?: number }
|
|
12
|
+
) => Effect.Effect<ReadonlyArray<RequestLogEntry>>
|
|
13
|
+
readonly getCount: (imposterId: string) => Effect.Effect<number>
|
|
14
|
+
readonly clear: (imposterId: string) => Effect.Effect<void>
|
|
15
|
+
readonly subscribe: Effect.Effect<Queue.Dequeue<RequestLogEntry>, never, Scope.Scope>
|
|
16
|
+
readonly getEntryById: (imposterId: string, entryId: string) => Effect.Effect<RequestLogEntry | null>
|
|
17
|
+
readonly removeImposter: (imposterId: string) => Effect.Effect<void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class RequestLogger extends Context.Tag("RequestLogger")<RequestLogger, RequestLoggerShape>() {}
|
|
21
|
+
|
|
22
|
+
export const RequestLoggerLive = Layer.scoped(
|
|
23
|
+
RequestLogger,
|
|
24
|
+
Effect.gen(function*() {
|
|
25
|
+
const storeRef = yield* Ref.make(HashMap.empty<string, Array<RequestLogEntry>>())
|
|
26
|
+
const pubsub = yield* PubSub.sliding<RequestLogEntry>(256)
|
|
27
|
+
|
|
28
|
+
const log = (entry: RequestLogEntry): Effect.Effect<void> =>
|
|
29
|
+
Effect.gen(function*() {
|
|
30
|
+
yield* Ref.update(storeRef, (store) => {
|
|
31
|
+
const existing = HashMap.get(store, entry.imposterId)
|
|
32
|
+
const entries = existing._tag === "Some" ? existing.value : []
|
|
33
|
+
const updated = [...entries, entry].slice(-MAX_ENTRIES)
|
|
34
|
+
return HashMap.set(store, entry.imposterId, updated)
|
|
35
|
+
})
|
|
36
|
+
yield* PubSub.publish(pubsub, entry)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const getEntries = (
|
|
40
|
+
imposterId: string,
|
|
41
|
+
opts?: { limit?: number; method?: string; path?: string; status?: number }
|
|
42
|
+
): Effect.Effect<ReadonlyArray<RequestLogEntry>> =>
|
|
43
|
+
Ref.get(storeRef).pipe(
|
|
44
|
+
Effect.map((store) => {
|
|
45
|
+
const existing = HashMap.get(store, imposterId)
|
|
46
|
+
let entries = existing._tag === "Some" ? existing.value : []
|
|
47
|
+
if (opts?.method !== undefined) {
|
|
48
|
+
entries = entries.filter((e) => e.request.method.toUpperCase() === opts.method!.toUpperCase())
|
|
49
|
+
}
|
|
50
|
+
if (opts?.path !== undefined) {
|
|
51
|
+
entries = entries.filter((e) => e.request.path === opts.path)
|
|
52
|
+
}
|
|
53
|
+
if (opts?.status !== undefined) {
|
|
54
|
+
entries = entries.filter((e) => e.response.status === opts.status)
|
|
55
|
+
}
|
|
56
|
+
const limit = opts?.limit ?? 50
|
|
57
|
+
return entries.slice(-limit)
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const getCount = (imposterId: string): Effect.Effect<number> =>
|
|
62
|
+
Ref.get(storeRef).pipe(
|
|
63
|
+
Effect.map((store) => {
|
|
64
|
+
const existing = HashMap.get(store, imposterId)
|
|
65
|
+
return existing._tag === "Some" ? existing.value.length : 0
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const clear = (imposterId: string): Effect.Effect<void> =>
|
|
70
|
+
Ref.update(storeRef, (store) => HashMap.set(store, imposterId, []))
|
|
71
|
+
|
|
72
|
+
const subscribe: Effect.Effect<Queue.Dequeue<RequestLogEntry>, never, Scope.Scope> = PubSub.subscribe(pubsub)
|
|
73
|
+
|
|
74
|
+
const getEntryById = (imposterId: string, entryId: string): Effect.Effect<RequestLogEntry | null> =>
|
|
75
|
+
Ref.get(storeRef).pipe(
|
|
76
|
+
Effect.map((store) => {
|
|
77
|
+
const existing = HashMap.get(store, imposterId)
|
|
78
|
+
if (existing._tag === "None") return null
|
|
79
|
+
return existing.value.find((e) => e.id === entryId) ?? null
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const removeImposter = (imposterId: string): Effect.Effect<void> => Ref.update(storeRef, HashMap.remove(imposterId))
|
|
84
|
+
|
|
85
|
+
return { log, getEntries, getCount, clear, subscribe, getEntryById, removeImposter } satisfies RequestLoggerShape
|
|
86
|
+
})
|
|
87
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Layer from "effect/Layer"
|
|
3
|
+
import { v4 } from "uuid"
|
|
4
|
+
import { Uuid } from "./Uuid"
|
|
5
|
+
|
|
6
|
+
export const UuidLive = Layer.succeed(Uuid, {
|
|
7
|
+
generate: Effect.sync(() => v4()),
|
|
8
|
+
generateShort: Effect.sync(() => v4().substring(0, 8))
|
|
9
|
+
})
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { Effect, Ref } from "effect"
|
|
2
|
+
import type { ImposterConfig } from "../domain/imposter"
|
|
3
|
+
import type { ImposterRepositoryShape } from "../repositories/ImposterRepository"
|
|
4
|
+
import { NonEmptyString } from "../schemas/common"
|
|
5
|
+
import type { Stub } from "../schemas/StubSchema"
|
|
6
|
+
import type { RequestLoggerShape } from "../services/RequestLogger"
|
|
7
|
+
import { dashboardPage } from "./pages/dashboard"
|
|
8
|
+
import { requestDetailPage } from "./pages/request-detail"
|
|
9
|
+
import { requestsPage, testResultPartial } from "./pages/requests"
|
|
10
|
+
import { stubsPage } from "./pages/stubs"
|
|
11
|
+
import { errorPartial, requestTablePartial, stubListPartial } from "./partials"
|
|
12
|
+
|
|
13
|
+
export interface UiDeps {
|
|
14
|
+
readonly id: string
|
|
15
|
+
readonly config: ImposterConfig
|
|
16
|
+
readonly stubsRef: Ref.Ref<ReadonlyArray<Stub>>
|
|
17
|
+
readonly repo: ImposterRepositoryShape
|
|
18
|
+
readonly requestLogger: RequestLoggerShape
|
|
19
|
+
readonly runPromise: <A>(effect: Effect.Effect<A>) => Promise<A>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const htmlResponse = (body: string, status = 200): Response =>
|
|
23
|
+
new Response(body, {
|
|
24
|
+
status,
|
|
25
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const parseStubIdFromPath = (path: string): string | null => {
|
|
29
|
+
const match = path.match(/^\/stubs\/(.+)$/)
|
|
30
|
+
return match ? match[1]! : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const makeUiRouter = (deps: UiDeps) => async (request: Request): Promise<Response | null> => {
|
|
34
|
+
const url = new URL(request.url)
|
|
35
|
+
if (!url.pathname.startsWith("/_admin")) return null
|
|
36
|
+
|
|
37
|
+
const path = url.pathname.slice("/_admin".length) || "/"
|
|
38
|
+
const method = request.method.toUpperCase()
|
|
39
|
+
|
|
40
|
+
// GET / — dashboard
|
|
41
|
+
if (method === "GET" && (path === "/" || path === "")) {
|
|
42
|
+
return deps.runPromise(
|
|
43
|
+
Effect.gen(function*() {
|
|
44
|
+
const stubs = yield* Ref.get(deps.stubsRef)
|
|
45
|
+
const requestCount = yield* deps.requestLogger.getCount(deps.id)
|
|
46
|
+
const recentRequests = yield* deps.requestLogger.getEntries(deps.id, { limit: 10 })
|
|
47
|
+
return htmlResponse(
|
|
48
|
+
dashboardPage({
|
|
49
|
+
config: deps.config,
|
|
50
|
+
stubCount: stubs.length,
|
|
51
|
+
requestCount,
|
|
52
|
+
recentRequests: recentRequests.slice().reverse()
|
|
53
|
+
}).value
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// GET /stubs — stubs page
|
|
60
|
+
if (method === "GET" && path === "/stubs") {
|
|
61
|
+
return deps.runPromise(
|
|
62
|
+
Effect.gen(function*() {
|
|
63
|
+
const stubs = yield* Ref.get(deps.stubsRef)
|
|
64
|
+
return htmlResponse(stubsPage({ config: deps.config, stubs }).value)
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// POST /stubs — add stub
|
|
70
|
+
if (method === "POST" && path === "/stubs") {
|
|
71
|
+
try {
|
|
72
|
+
const formData = await request.formData()
|
|
73
|
+
const predicatesRaw = formData.get("predicates") as string | null
|
|
74
|
+
const responsesRaw = formData.get("responses") as string | null
|
|
75
|
+
const responseMode = (formData.get("responseMode") as string | null) || "sequential"
|
|
76
|
+
|
|
77
|
+
if (!responsesRaw) {
|
|
78
|
+
return htmlResponse(errorPartial("Responses field is required.").value, 200)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let predicates: unknown
|
|
82
|
+
let responses: unknown
|
|
83
|
+
try {
|
|
84
|
+
predicates = JSON.parse(predicatesRaw || "[]")
|
|
85
|
+
responses = JSON.parse(responsesRaw)
|
|
86
|
+
} catch {
|
|
87
|
+
return htmlResponse(errorPartial("Invalid JSON in predicates or responses.").value, 200)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!Array.isArray(responses) || responses.length === 0) {
|
|
91
|
+
return htmlResponse(errorPartial("Responses must be a non-empty array.").value, 200)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const stubId = NonEmptyString.make(crypto.randomUUID().slice(0, 8))
|
|
95
|
+
const stub: Stub = {
|
|
96
|
+
id: stubId,
|
|
97
|
+
predicates: predicates as Stub["predicates"],
|
|
98
|
+
responses: responses as unknown as Stub["responses"],
|
|
99
|
+
responseMode: responseMode as Stub["responseMode"]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return await deps.runPromise(
|
|
103
|
+
Effect.gen(function*() {
|
|
104
|
+
yield* deps.repo.addStub(deps.id, stub).pipe(
|
|
105
|
+
Effect.catchAll(() => Effect.void)
|
|
106
|
+
)
|
|
107
|
+
const updated = yield* deps.repo.getStubs(deps.id).pipe(
|
|
108
|
+
Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
|
|
109
|
+
)
|
|
110
|
+
yield* Ref.set(deps.stubsRef, updated)
|
|
111
|
+
return htmlResponse(stubListPartial(updated).value)
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
} catch {
|
|
115
|
+
return htmlResponse(errorPartial("Failed to parse form data.").value, 200)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// DELETE /stubs/:id — delete stub
|
|
120
|
+
const deleteStubId = method === "DELETE" ? parseStubIdFromPath(path) : null
|
|
121
|
+
if (deleteStubId !== null) {
|
|
122
|
+
return deps.runPromise(
|
|
123
|
+
Effect.gen(function*() {
|
|
124
|
+
yield* deps.repo.removeStub(deps.id, deleteStubId).pipe(
|
|
125
|
+
Effect.catchAll(() => Effect.void)
|
|
126
|
+
)
|
|
127
|
+
const updated = yield* deps.repo.getStubs(deps.id).pipe(
|
|
128
|
+
Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
|
|
129
|
+
)
|
|
130
|
+
yield* Ref.set(deps.stubsRef, updated)
|
|
131
|
+
return htmlResponse(stubListPartial(updated).value)
|
|
132
|
+
})
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// PUT /stubs/:id — update stub
|
|
137
|
+
const putStubId = method === "PUT" ? parseStubIdFromPath(path) : null
|
|
138
|
+
if (putStubId !== null) {
|
|
139
|
+
try {
|
|
140
|
+
const formData = await request.formData()
|
|
141
|
+
const predicatesRaw = formData.get("predicates") as string | null
|
|
142
|
+
const responsesRaw = formData.get("responses") as string | null
|
|
143
|
+
const responseMode = formData.get("responseMode") as string | null
|
|
144
|
+
|
|
145
|
+
return await deps.runPromise(
|
|
146
|
+
Effect.gen(function*() {
|
|
147
|
+
yield* deps.repo.updateStub(deps.id, putStubId, (existing) => ({
|
|
148
|
+
...existing,
|
|
149
|
+
...(predicatesRaw ? { predicates: JSON.parse(predicatesRaw) } : {}),
|
|
150
|
+
...(responsesRaw ? { responses: JSON.parse(responsesRaw) } : {}),
|
|
151
|
+
...(responseMode ? { responseMode: responseMode as Stub["responseMode"] } : {})
|
|
152
|
+
})).pipe(Effect.catchAll(() => Effect.void))
|
|
153
|
+
const updated = yield* deps.repo.getStubs(deps.id).pipe(
|
|
154
|
+
Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
|
|
155
|
+
)
|
|
156
|
+
yield* Ref.set(deps.stubsRef, updated)
|
|
157
|
+
return htmlResponse(stubListPartial(updated).value)
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
} catch {
|
|
161
|
+
return htmlResponse(errorPartial("Failed to parse form data.").value, 200)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// GET /requests — full page
|
|
166
|
+
if (method === "GET" && path === "/requests") {
|
|
167
|
+
return deps.runPromise(
|
|
168
|
+
Effect.gen(function*() {
|
|
169
|
+
const entries = yield* deps.requestLogger.getEntries(deps.id, { limit: 100 })
|
|
170
|
+
return htmlResponse(requestsPage({ config: deps.config, entries }).value)
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// GET /requests/list — HTMX partial (filtered table body)
|
|
176
|
+
if (method === "GET" && path === "/requests/list") {
|
|
177
|
+
const params = url.searchParams
|
|
178
|
+
return deps.runPromise(
|
|
179
|
+
Effect.gen(function*() {
|
|
180
|
+
const opts: { limit?: number; method?: string; path?: string; status?: number } = { limit: 100 }
|
|
181
|
+
const methodFilter = params.get("method")
|
|
182
|
+
if (methodFilter) opts.method = methodFilter
|
|
183
|
+
const pathFilter = params.get("path")
|
|
184
|
+
if (pathFilter) opts.path = pathFilter
|
|
185
|
+
const statusFilter = params.get("status")
|
|
186
|
+
if (statusFilter) opts.status = Number(statusFilter)
|
|
187
|
+
const entries = yield* deps.requestLogger.getEntries(deps.id, opts)
|
|
188
|
+
return htmlResponse(requestTablePartial(entries.slice().reverse()).value)
|
|
189
|
+
})
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// POST /requests/test — send test request to own port
|
|
194
|
+
if (method === "POST" && path === "/requests/test") {
|
|
195
|
+
try {
|
|
196
|
+
const formData = await request.formData()
|
|
197
|
+
const testMethod = (formData.get("method") as string) || "GET"
|
|
198
|
+
const testPath = (formData.get("path") as string) || "/"
|
|
199
|
+
const testBody = formData.get("body") as string | null
|
|
200
|
+
const testContentType = (formData.get("contentType") as string) || "application/json"
|
|
201
|
+
const testHeadersRaw = (formData.get("headers") as string) || ""
|
|
202
|
+
|
|
203
|
+
const headers: Record<string, string> = {}
|
|
204
|
+
if (testContentType && testBody) {
|
|
205
|
+
headers["content-type"] = testContentType
|
|
206
|
+
}
|
|
207
|
+
for (const line of testHeadersRaw.split("\n")) {
|
|
208
|
+
const colonIdx = line.indexOf(":")
|
|
209
|
+
if (colonIdx > 0) {
|
|
210
|
+
headers[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim()
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const startTime = Date.now()
|
|
215
|
+
const testResp = await fetch(`http://localhost:${deps.config.port}${testPath}`, {
|
|
216
|
+
method: testMethod,
|
|
217
|
+
headers,
|
|
218
|
+
...(testBody && testMethod !== "GET" && testMethod !== "HEAD" ? { body: testBody } : {})
|
|
219
|
+
})
|
|
220
|
+
const duration = Date.now() - startTime
|
|
221
|
+
|
|
222
|
+
const respBody = await testResp.text()
|
|
223
|
+
const respHeaders: Record<string, string> = {}
|
|
224
|
+
testResp.headers.forEach((val, key) => {
|
|
225
|
+
respHeaders[key] = val
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return htmlResponse(
|
|
229
|
+
testResultPartial({ status: testResp.status, headers: respHeaders, body: respBody, duration }).value
|
|
230
|
+
)
|
|
231
|
+
} catch (err) {
|
|
232
|
+
return htmlResponse(
|
|
233
|
+
`<div class="bg-red-50 border border-red-200 text-red-700 rounded p-3">Request failed: ${String(err)}</div>`
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// DELETE /requests — clear log
|
|
239
|
+
if (method === "DELETE" && path === "/requests") {
|
|
240
|
+
return deps.runPromise(
|
|
241
|
+
Effect.gen(function*() {
|
|
242
|
+
yield* deps.requestLogger.clear(deps.id)
|
|
243
|
+
return htmlResponse(requestTablePartial([]).value)
|
|
244
|
+
})
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// GET /requests/:id — detail page
|
|
249
|
+
if (method === "GET" && path.startsWith("/requests/")) {
|
|
250
|
+
const entryId = path.slice("/requests/".length)
|
|
251
|
+
if (entryId && !entryId.includes("/")) {
|
|
252
|
+
return deps.runPromise(
|
|
253
|
+
Effect.gen(function*() {
|
|
254
|
+
const entry = yield* deps.requestLogger.getEntryById(deps.id, entryId)
|
|
255
|
+
if (entry === null) {
|
|
256
|
+
return htmlResponse("<h1>Request not found</h1>", 404)
|
|
257
|
+
}
|
|
258
|
+
// Try to find matched stub
|
|
259
|
+
let matchedStub: Stub | null = null
|
|
260
|
+
if (entry.response.matchedStubId) {
|
|
261
|
+
try {
|
|
262
|
+
const stubs = yield* deps.repo.getStubs(deps.id).pipe(
|
|
263
|
+
Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
|
|
264
|
+
)
|
|
265
|
+
matchedStub = stubs.find((s) => s.id === entry.response.matchedStubId) ?? null
|
|
266
|
+
} catch {
|
|
267
|
+
// ignore
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return htmlResponse(requestDetailPage({ config: deps.config, entry, matchedStub }).value)
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Fallback: 404 within /_admin
|
|
277
|
+
return htmlResponse("<h1>Not Found</h1>", 404)
|
|
278
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { html, raw } from "../html"
|
|
2
|
+
import type { SafeHtml } from "../html"
|
|
3
|
+
|
|
4
|
+
export interface AdminLayoutOpts {
|
|
5
|
+
readonly title: string
|
|
6
|
+
readonly activeTab?: "dashboard"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const adminLayout = (opts: AdminLayoutOpts, content: SafeHtml): SafeHtml =>
|
|
10
|
+
html`<!DOCTYPE html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="utf-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
15
|
+
<title>${opts.title}</title>
|
|
16
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
17
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
18
|
+
</head>
|
|
19
|
+
<body class="bg-gray-50 min-h-screen">
|
|
20
|
+
<nav class="bg-gray-800 text-white shadow-md">
|
|
21
|
+
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
22
|
+
<div class="flex items-center gap-3">
|
|
23
|
+
<span class="text-lg font-bold">Imposters</span>
|
|
24
|
+
<span class="text-gray-400 text-sm">Admin</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="flex gap-3 items-center">
|
|
27
|
+
<a href="/_ui" class="px-3 py-1.5 bg-gray-700 text-white rounded text-sm font-medium hover:bg-gray-600">Dashboard</a>
|
|
28
|
+
<a href="/docs" class="text-gray-400 hover:text-white text-sm">API Docs</a>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</nav>
|
|
32
|
+
<main class="max-w-6xl mx-auto px-4 py-6">
|
|
33
|
+
${raw(content.value)}
|
|
34
|
+
</main>
|
|
35
|
+
</body>
|
|
36
|
+
</html>`
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { adminDashboardPage } from "./pages/AdminDashboard"
|
|
2
|
+
import type { AdminImposterData } from "./partials"
|
|
3
|
+
import { adminErrorPartial, imposterListPartial, imposterRowPartial } from "./partials"
|
|
4
|
+
|
|
5
|
+
export interface AdminUiDeps {
|
|
6
|
+
readonly apiHandler: (request: Request) => Promise<Response>
|
|
7
|
+
readonly adminPort: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const htmlResponse = (body: string, status = 200): Response =>
|
|
11
|
+
new Response(body, {
|
|
12
|
+
status,
|
|
13
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const toAdminData = (imp: any): AdminImposterData => ({
|
|
17
|
+
id: imp.id,
|
|
18
|
+
name: imp.name ?? `imposter-${imp.id}`,
|
|
19
|
+
port: imp.port,
|
|
20
|
+
status: imp.status,
|
|
21
|
+
protocol: imp.protocol ?? "HTTP",
|
|
22
|
+
stubCount: imp.stubs?.length ?? imp.endpointCount ?? imp.stubCount ?? 0
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const fetchImposters = async (apiHandler: (r: Request) => Promise<Response>): Promise<Array<AdminImposterData>> => {
|
|
26
|
+
const resp = await apiHandler(new Request("http://localhost/imposters?limit=50", { method: "GET" }))
|
|
27
|
+
if (!resp.ok) return []
|
|
28
|
+
const data = await resp.json()
|
|
29
|
+
const items = Array.isArray(data) ? data : (data.imposters ?? data.items ?? [])
|
|
30
|
+
return items.map(toAdminData)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fetchImposter = async (
|
|
34
|
+
apiHandler: (r: Request) => Promise<Response>,
|
|
35
|
+
id: string
|
|
36
|
+
): Promise<AdminImposterData | null> => {
|
|
37
|
+
const resp = await apiHandler(new Request(`http://localhost/imposters/${id}`, { method: "GET" }))
|
|
38
|
+
if (!resp.ok) return null
|
|
39
|
+
const imp = await resp.json()
|
|
40
|
+
return toAdminData(imp)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const makeAdminUiRouter = (deps: AdminUiDeps) => async (request: Request): Promise<Response | null> => {
|
|
44
|
+
const url = new URL(request.url)
|
|
45
|
+
if (!url.pathname.startsWith("/_ui")) return null
|
|
46
|
+
|
|
47
|
+
const path = url.pathname.slice("/_ui".length) || "/"
|
|
48
|
+
const method = request.method.toUpperCase()
|
|
49
|
+
|
|
50
|
+
// GET / — dashboard
|
|
51
|
+
if (method === "GET" && (path === "/" || path === "")) {
|
|
52
|
+
const imposters = await fetchImposters(deps.apiHandler)
|
|
53
|
+
return htmlResponse(adminDashboardPage({ imposters }).value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// GET /imposters — HTMX partial (imposter list)
|
|
57
|
+
if (method === "GET" && path === "/imposters") {
|
|
58
|
+
const imposters = await fetchImposters(deps.apiHandler)
|
|
59
|
+
return htmlResponse(imposterListPartial(imposters).value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// POST /imposters — create imposter from form
|
|
63
|
+
if (method === "POST" && path === "/imposters") {
|
|
64
|
+
try {
|
|
65
|
+
const formData = await request.formData()
|
|
66
|
+
const name = formData.get("name") as string | null
|
|
67
|
+
const portStr = formData.get("port") as string | null
|
|
68
|
+
const autoStart = formData.get("autoStart") === "on"
|
|
69
|
+
|
|
70
|
+
const payload: Record<string, unknown> = {
|
|
71
|
+
protocol: "HTTP"
|
|
72
|
+
}
|
|
73
|
+
if (name && name.trim()) payload.name = name.trim()
|
|
74
|
+
if (portStr && portStr.trim()) payload.port = Number(portStr.trim())
|
|
75
|
+
|
|
76
|
+
const createResp = await deps.apiHandler(
|
|
77
|
+
new Request("http://localhost/imposters", {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
body: JSON.stringify(payload)
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (!createResp.ok) {
|
|
85
|
+
const errBody = await createResp.text()
|
|
86
|
+
const imposters = await fetchImposters(deps.apiHandler)
|
|
87
|
+
return htmlResponse(
|
|
88
|
+
adminErrorPartial(`Failed to create imposter: ${errBody}`).value +
|
|
89
|
+
imposterListPartial(imposters).value
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const created = await createResp.json()
|
|
94
|
+
|
|
95
|
+
// Auto-start if requested
|
|
96
|
+
if (autoStart) {
|
|
97
|
+
await deps.apiHandler(
|
|
98
|
+
new Request(`http://localhost/imposters/${created.id}`, {
|
|
99
|
+
method: "PATCH",
|
|
100
|
+
headers: { "content-type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ status: "running" })
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const imposters = await fetchImposters(deps.apiHandler)
|
|
107
|
+
return htmlResponse(imposterListPartial(imposters).value)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const imposters = await fetchImposters(deps.apiHandler)
|
|
110
|
+
return htmlResponse(
|
|
111
|
+
adminErrorPartial(`Error: ${String(err)}`).value +
|
|
112
|
+
imposterListPartial(imposters).value
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// POST /imposters/:id/start
|
|
118
|
+
const startMatch = path.match(/^\/imposters\/([^/]+)\/start$/)
|
|
119
|
+
if (method === "POST" && startMatch) {
|
|
120
|
+
const id = startMatch[1]!
|
|
121
|
+
await deps.apiHandler(
|
|
122
|
+
new Request(`http://localhost/imposters/${id}`, {
|
|
123
|
+
method: "PATCH",
|
|
124
|
+
headers: { "content-type": "application/json" },
|
|
125
|
+
body: JSON.stringify({ status: "running" })
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
// Small delay for the server to start
|
|
129
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
130
|
+
const imp = await fetchImposter(deps.apiHandler, id)
|
|
131
|
+
if (!imp) {
|
|
132
|
+
return htmlResponse(adminErrorPartial("Imposter not found").value, 404)
|
|
133
|
+
}
|
|
134
|
+
return htmlResponse(imposterRowPartial(imp).value)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// POST /imposters/:id/stop
|
|
138
|
+
const stopMatch = path.match(/^\/imposters\/([^/]+)\/stop$/)
|
|
139
|
+
if (method === "POST" && stopMatch) {
|
|
140
|
+
const id = stopMatch[1]!
|
|
141
|
+
await deps.apiHandler(
|
|
142
|
+
new Request(`http://localhost/imposters/${id}`, {
|
|
143
|
+
method: "PATCH",
|
|
144
|
+
headers: { "content-type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ status: "stopped" })
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
149
|
+
const imp = await fetchImposter(deps.apiHandler, id)
|
|
150
|
+
if (!imp) {
|
|
151
|
+
return htmlResponse(adminErrorPartial("Imposter not found").value, 404)
|
|
152
|
+
}
|
|
153
|
+
return htmlResponse(imposterRowPartial(imp).value)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// DELETE /imposters/:id
|
|
157
|
+
const deleteMatch = path.match(/^\/imposters\/([^/]+)$/)
|
|
158
|
+
if (method === "DELETE" && deleteMatch) {
|
|
159
|
+
const id = deleteMatch[1]!
|
|
160
|
+
await deps.apiHandler(
|
|
161
|
+
new Request(`http://localhost/imposters/${id}?force=true`, {
|
|
162
|
+
method: "DELETE"
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
const imposters = await fetchImposters(deps.apiHandler)
|
|
166
|
+
return htmlResponse(imposterListPartial(imposters).value)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { html, raw } from "../../html"
|
|
2
|
+
import type { SafeHtml } from "../../html"
|
|
3
|
+
import { adminLayout } from "../AdminLayout"
|
|
4
|
+
import type { AdminImposterData } from "../partials"
|
|
5
|
+
import { createFormPartial, imposterListPartial } from "../partials"
|
|
6
|
+
|
|
7
|
+
export interface AdminDashboardData {
|
|
8
|
+
readonly imposters: ReadonlyArray<AdminImposterData>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const summaryBar = (data: AdminDashboardData): SafeHtml => {
|
|
12
|
+
const total = data.imposters.length
|
|
13
|
+
const running = data.imposters.filter((i) => i.status === "running").length
|
|
14
|
+
const stopped = total - running
|
|
15
|
+
return html`<div class="grid grid-cols-3 gap-4 mb-6">
|
|
16
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
17
|
+
<div class="text-sm text-gray-500">Total Imposters</div>
|
|
18
|
+
<div class="text-2xl font-bold text-gray-800">${String(total)}</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
21
|
+
<div class="text-sm text-gray-500">Running</div>
|
|
22
|
+
<div class="text-2xl font-bold text-green-600">${String(running)}</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
25
|
+
<div class="text-sm text-gray-500">Stopped</div>
|
|
26
|
+
<div class="text-2xl font-bold text-gray-500">${String(stopped)}</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const adminDashboardPage = (data: AdminDashboardData): SafeHtml => {
|
|
32
|
+
const content = html`
|
|
33
|
+
${summaryBar(data)}
|
|
34
|
+
${createFormPartial()}
|
|
35
|
+
<div class="bg-white rounded-lg shadow overflow-x-auto">
|
|
36
|
+
<table class="w-full text-left">
|
|
37
|
+
<thead>
|
|
38
|
+
<tr class="text-xs text-gray-500 uppercase border-b">
|
|
39
|
+
<th class="py-3 px-4">Name</th>
|
|
40
|
+
<th class="py-3 px-4">Port</th>
|
|
41
|
+
<th class="py-3 px-4">Status</th>
|
|
42
|
+
<th class="py-3 px-4">Protocol</th>
|
|
43
|
+
<th class="py-3 px-4">Stubs</th>
|
|
44
|
+
<th class="py-3 px-4">Actions</th>
|
|
45
|
+
</tr>
|
|
46
|
+
</thead>
|
|
47
|
+
<tbody id="imposter-list">
|
|
48
|
+
${raw(imposterListPartial(data.imposters).value)}
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>`
|
|
52
|
+
|
|
53
|
+
return adminLayout({ title: "Imposters — Admin Dashboard" }, content)
|
|
54
|
+
}
|