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,387 @@
|
|
|
1
|
+
import { HttpApiBuilder } from "@effect/platform"
|
|
2
|
+
import * as Clock from "effect/Clock"
|
|
3
|
+
import * as DateTime from "effect/DateTime"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import { ImposterConfig, type ProxyConfigDomain } from "../domain/imposter"
|
|
6
|
+
import { ImposterRepository } from "../repositories/ImposterRepository"
|
|
7
|
+
import { NonEmptyString } from "../schemas/common"
|
|
8
|
+
import { ImposterServer } from "../server/ImposterServer"
|
|
9
|
+
import { AppConfig } from "../services/AppConfig"
|
|
10
|
+
import { MetricsService } from "../services/MetricsService"
|
|
11
|
+
import { PortAllocator } from "../services/PortAllocator"
|
|
12
|
+
import { RequestLogger } from "../services/RequestLogger"
|
|
13
|
+
import { Uuid } from "../services/Uuid"
|
|
14
|
+
import { AdminApi } from "./AdminApi"
|
|
15
|
+
import { ApiConflictError, ApiNotFoundError, ApiServiceError } from "./ApiErrors"
|
|
16
|
+
import { buildPaginationMeta, toImposterResponse } from "./Conversions"
|
|
17
|
+
|
|
18
|
+
export const ImpostersHandlersLive = HttpApiBuilder.group(AdminApi, "imposters", (handlers) =>
|
|
19
|
+
handlers
|
|
20
|
+
.handle("createImposter", ({ payload }) =>
|
|
21
|
+
Effect.gen(function*() {
|
|
22
|
+
const repo = yield* ImposterRepository
|
|
23
|
+
const uuid = yield* Uuid
|
|
24
|
+
const allocator = yield* PortAllocator
|
|
25
|
+
const config = yield* AppConfig
|
|
26
|
+
|
|
27
|
+
const all = yield* repo.getAll
|
|
28
|
+
if (all.length >= config.maxImposters) {
|
|
29
|
+
return yield* Effect.fail(
|
|
30
|
+
new ApiServiceError({ message: `Maximum number of imposters (${config.maxImposters}) reached` })
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const id = yield* uuid.generateShort
|
|
35
|
+
const name = payload.name ?? NonEmptyString.make(id)
|
|
36
|
+
|
|
37
|
+
const port = yield* allocator.allocate(payload.port).pipe(
|
|
38
|
+
Effect.catchTags({
|
|
39
|
+
PortAllocatorError: (e) => Effect.fail(new ApiConflictError({ message: e.reason })),
|
|
40
|
+
PortExhaustedError: (e) =>
|
|
41
|
+
Effect.fail(new ApiServiceError({ message: `No available ports in range ${e.rangeMin}-${e.rangeMax}` }))
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const imposterConfig = ImposterConfig({
|
|
46
|
+
id,
|
|
47
|
+
name,
|
|
48
|
+
port,
|
|
49
|
+
status: "stopped",
|
|
50
|
+
createdAt: DateTime.unsafeNow(),
|
|
51
|
+
...(payload.proxy !== undefined ? { proxy: payload.proxy } : {})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const record = yield* repo.create(imposterConfig)
|
|
55
|
+
return yield* toImposterResponse(record)
|
|
56
|
+
}))
|
|
57
|
+
.handle("listImposters", ({ urlParams }) =>
|
|
58
|
+
Effect.gen(function*() {
|
|
59
|
+
const repo = yield* ImposterRepository
|
|
60
|
+
const all = yield* repo.getAll
|
|
61
|
+
|
|
62
|
+
// All imposters are HTTP in Phase 2; protocol filter is for forward compatibility
|
|
63
|
+
const filtered = all
|
|
64
|
+
.filter((r) => urlParams.status === undefined || r.config.status === urlParams.status)
|
|
65
|
+
.filter(() => urlParams.protocol === undefined || urlParams.protocol === "HTTP")
|
|
66
|
+
|
|
67
|
+
filtered.sort((a, b) => DateTime.toEpochMillis(a.config.createdAt) - DateTime.toEpochMillis(b.config.createdAt))
|
|
68
|
+
|
|
69
|
+
const total = filtered.length
|
|
70
|
+
const paged = filtered.slice(urlParams.offset, urlParams.offset + urlParams.limit)
|
|
71
|
+
const imposters = yield* Effect.all(paged.map(toImposterResponse))
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
imposters,
|
|
75
|
+
pagination: buildPaginationMeta(total, urlParams.limit, urlParams.offset)
|
|
76
|
+
}
|
|
77
|
+
}))
|
|
78
|
+
.handle("getImposter", ({ path }) =>
|
|
79
|
+
Effect.gen(function*() {
|
|
80
|
+
const repo = yield* ImposterRepository
|
|
81
|
+
const record = yield* repo.get(path.id).pipe(
|
|
82
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
83
|
+
Effect.fail(
|
|
84
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
85
|
+
))
|
|
86
|
+
)
|
|
87
|
+
return yield* toImposterResponse(record)
|
|
88
|
+
}))
|
|
89
|
+
.handle("updateImposter", ({ path, payload }) =>
|
|
90
|
+
Effect.gen(function*() {
|
|
91
|
+
const repo = yield* ImposterRepository
|
|
92
|
+
const allocator = yield* PortAllocator
|
|
93
|
+
const imposterServer = yield* ImposterServer
|
|
94
|
+
|
|
95
|
+
const existing = yield* repo.get(path.id).pipe(
|
|
96
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
97
|
+
Effect.fail(
|
|
98
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
99
|
+
))
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const wasRunning = yield* imposterServer.isRunning(path.id)
|
|
103
|
+
const wantsRunning = payload.status === "running"
|
|
104
|
+
const wantsStopped = payload.status === "stopped"
|
|
105
|
+
const portChanging = payload.port !== undefined && payload.port !== existing.config.port
|
|
106
|
+
|
|
107
|
+
// If port is changing while running, stop first
|
|
108
|
+
if (portChanging && wasRunning) {
|
|
109
|
+
yield* imposterServer.stop(path.id)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let newPort: number | undefined
|
|
113
|
+
if (portChanging) {
|
|
114
|
+
newPort = yield* allocator.allocate(payload.port).pipe(
|
|
115
|
+
Effect.catchTags({
|
|
116
|
+
PortAllocatorError: (e) => Effect.fail(new ApiConflictError({ message: e.reason })),
|
|
117
|
+
PortExhaustedError: (e) =>
|
|
118
|
+
Effect.fail(new ApiServiceError({ message: `No available ports in range ${e.rangeMin}-${e.rangeMax}` }))
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Compute proxy update: undefined = no change, null = remove, object = set
|
|
124
|
+
const proxyUpdate: { proxy?: ProxyConfigDomain | undefined } = payload.proxy === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: payload.proxy === null
|
|
127
|
+
? { proxy: undefined }
|
|
128
|
+
: { proxy: payload.proxy }
|
|
129
|
+
|
|
130
|
+
yield* repo.update(path.id, (r) => ({
|
|
131
|
+
...r,
|
|
132
|
+
config: ImposterConfig({
|
|
133
|
+
...r.config,
|
|
134
|
+
...(payload.name !== undefined ? { name: payload.name as string } : {}),
|
|
135
|
+
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
136
|
+
...(newPort !== undefined ? { port: newPort } : {}),
|
|
137
|
+
...proxyUpdate
|
|
138
|
+
})
|
|
139
|
+
})).pipe(
|
|
140
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
141
|
+
Effect.fail(
|
|
142
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
143
|
+
)),
|
|
144
|
+
Effect.tapError(() => newPort !== undefined ? allocator.release(newPort) : Effect.void)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (newPort !== undefined) {
|
|
148
|
+
yield* allocator.release(existing.config.port)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Hot-reload proxy config if it changed
|
|
152
|
+
if (payload.proxy !== undefined) {
|
|
153
|
+
yield* imposterServer.updateProxyConfig(path.id)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle start/stop transitions
|
|
157
|
+
if (wantsRunning && !wasRunning) {
|
|
158
|
+
yield* imposterServer.start(path.id).pipe(
|
|
159
|
+
Effect.catchTag("ImposterServerError", (e) => Effect.fail(new ApiServiceError({ message: e.reason }))),
|
|
160
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
161
|
+
Effect.fail(
|
|
162
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
163
|
+
))
|
|
164
|
+
)
|
|
165
|
+
} else if (wantsStopped && wasRunning && !portChanging) {
|
|
166
|
+
yield* imposterServer.stop(path.id)
|
|
167
|
+
} else if (portChanging && wasRunning) {
|
|
168
|
+
// Port changed while running — restart
|
|
169
|
+
yield* imposterServer.start(path.id).pipe(
|
|
170
|
+
Effect.catchTag("ImposterServerError", (e) => Effect.fail(new ApiServiceError({ message: e.reason }))),
|
|
171
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
172
|
+
Effect.fail(
|
|
173
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
174
|
+
))
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Re-read to get final status
|
|
179
|
+
const final = yield* repo.get(path.id).pipe(
|
|
180
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
181
|
+
Effect.fail(
|
|
182
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
183
|
+
))
|
|
184
|
+
)
|
|
185
|
+
return yield* toImposterResponse(final)
|
|
186
|
+
}))
|
|
187
|
+
.handle("deleteImposter", ({ path, urlParams }) =>
|
|
188
|
+
Effect.gen(function*() {
|
|
189
|
+
const repo = yield* ImposterRepository
|
|
190
|
+
const allocator = yield* PortAllocator
|
|
191
|
+
const imposterServer = yield* ImposterServer
|
|
192
|
+
const metricsService = yield* MetricsService
|
|
193
|
+
|
|
194
|
+
const existing = yield* repo.get(path.id).pipe(
|
|
195
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
196
|
+
Effect.fail(
|
|
197
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
198
|
+
))
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if (!urlParams.force && existing.config.status !== "stopped") {
|
|
202
|
+
return yield* Effect.fail(
|
|
203
|
+
new ApiConflictError({
|
|
204
|
+
message: `Imposter is ${existing.config.status}, use force=true to delete`
|
|
205
|
+
})
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Stop if running
|
|
210
|
+
const running = yield* imposterServer.isRunning(path.id)
|
|
211
|
+
if (running) {
|
|
212
|
+
yield* imposterServer.stop(path.id)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
yield* repo.remove(path.id).pipe(
|
|
216
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
217
|
+
Effect.fail(
|
|
218
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
219
|
+
))
|
|
220
|
+
)
|
|
221
|
+
yield* allocator.release(existing.config.port)
|
|
222
|
+
yield* metricsService.resetStats(path.id)
|
|
223
|
+
|
|
224
|
+
const now = yield* Effect.map(Clock.currentTimeMillis, (ms) => DateTime.unsafeMake(ms))
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
message: NonEmptyString.make(`Imposter ${path.id} deleted`),
|
|
228
|
+
id: NonEmptyString.make(path.id),
|
|
229
|
+
deletedAt: now
|
|
230
|
+
}
|
|
231
|
+
}))
|
|
232
|
+
.handle("addStub", ({ path, payload }) =>
|
|
233
|
+
Effect.gen(function*() {
|
|
234
|
+
const repo = yield* ImposterRepository
|
|
235
|
+
const uuid = yield* Uuid
|
|
236
|
+
const imposterServer = yield* ImposterServer
|
|
237
|
+
|
|
238
|
+
const id = yield* uuid.generateShort
|
|
239
|
+
const stub = {
|
|
240
|
+
id: NonEmptyString.make(id),
|
|
241
|
+
predicates: payload.predicates,
|
|
242
|
+
responses: payload.responses,
|
|
243
|
+
responseMode: payload.responseMode
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result = yield* repo.addStub(path.imposterId, stub).pipe(
|
|
247
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
248
|
+
Effect.fail(
|
|
249
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
250
|
+
))
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
// Hot-reload if running
|
|
254
|
+
const running = yield* imposterServer.isRunning(path.imposterId)
|
|
255
|
+
if (running) {
|
|
256
|
+
yield* imposterServer.updateStubs(path.imposterId)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
}))
|
|
261
|
+
.handle("listStubs", ({ path }) =>
|
|
262
|
+
Effect.gen(function*() {
|
|
263
|
+
const repo = yield* ImposterRepository
|
|
264
|
+
return yield* repo.getStubs(path.imposterId).pipe(
|
|
265
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
266
|
+
Effect.fail(
|
|
267
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
268
|
+
))
|
|
269
|
+
)
|
|
270
|
+
}))
|
|
271
|
+
.handle("updateStub", ({ path, payload }) =>
|
|
272
|
+
Effect.gen(function*() {
|
|
273
|
+
const repo = yield* ImposterRepository
|
|
274
|
+
const imposterServer = yield* ImposterServer
|
|
275
|
+
|
|
276
|
+
const result = yield* repo.updateStub(path.imposterId, path.stubId, (s) => ({
|
|
277
|
+
...s,
|
|
278
|
+
...(payload.predicates !== undefined ? { predicates: payload.predicates } : {}),
|
|
279
|
+
...(payload.responses !== undefined ? { responses: payload.responses } : {}),
|
|
280
|
+
...(payload.responseMode !== undefined ? { responseMode: payload.responseMode } : {})
|
|
281
|
+
})).pipe(
|
|
282
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
283
|
+
Effect.fail(
|
|
284
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
285
|
+
)),
|
|
286
|
+
Effect.catchTag("StubNotFoundError", (e) =>
|
|
287
|
+
Effect.fail(
|
|
288
|
+
new ApiNotFoundError({ message: "Stub not found", resourceType: "stub", resourceId: e.stubId })
|
|
289
|
+
))
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
// Hot-reload if running
|
|
293
|
+
const running = yield* imposterServer.isRunning(path.imposterId)
|
|
294
|
+
if (running) {
|
|
295
|
+
yield* imposterServer.updateStubs(path.imposterId)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return result
|
|
299
|
+
}))
|
|
300
|
+
.handle("deleteStub", ({ path }) =>
|
|
301
|
+
Effect.gen(function*() {
|
|
302
|
+
const repo = yield* ImposterRepository
|
|
303
|
+
const imposterServer = yield* ImposterServer
|
|
304
|
+
|
|
305
|
+
const result = yield* repo.removeStub(path.imposterId, path.stubId).pipe(
|
|
306
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
307
|
+
Effect.fail(
|
|
308
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
309
|
+
)),
|
|
310
|
+
Effect.catchTag(
|
|
311
|
+
"StubNotFoundError",
|
|
312
|
+
(e) =>
|
|
313
|
+
Effect.fail(
|
|
314
|
+
new ApiNotFoundError({ message: "Stub not found", resourceType: "stub", resourceId: e.stubId })
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
// Hot-reload if running
|
|
320
|
+
const running = yield* imposterServer.isRunning(path.imposterId)
|
|
321
|
+
if (running) {
|
|
322
|
+
yield* imposterServer.updateStubs(path.imposterId)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
}))
|
|
327
|
+
.handle("listRequests", ({ path, urlParams }) =>
|
|
328
|
+
Effect.gen(function*() {
|
|
329
|
+
const repo = yield* ImposterRepository
|
|
330
|
+
const requestLogger = yield* RequestLogger
|
|
331
|
+
yield* repo.get(path.id).pipe(
|
|
332
|
+
Effect.catchTag(
|
|
333
|
+
"ImposterNotFoundError",
|
|
334
|
+
(e) =>
|
|
335
|
+
Effect.fail(
|
|
336
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
return yield* requestLogger.getEntries(path.id, {
|
|
341
|
+
limit: urlParams.limit,
|
|
342
|
+
...(urlParams.method !== undefined ? { method: urlParams.method } : {}),
|
|
343
|
+
...(urlParams.path !== undefined ? { path: urlParams.path } : {}),
|
|
344
|
+
...(urlParams.status !== undefined ? { status: urlParams.status } : {})
|
|
345
|
+
})
|
|
346
|
+
}))
|
|
347
|
+
.handle("clearRequests", ({ path }) =>
|
|
348
|
+
Effect.gen(function*() {
|
|
349
|
+
const repo = yield* ImposterRepository
|
|
350
|
+
const requestLogger = yield* RequestLogger
|
|
351
|
+
yield* repo.get(path.id).pipe(
|
|
352
|
+
Effect.catchTag(
|
|
353
|
+
"ImposterNotFoundError",
|
|
354
|
+
(e) =>
|
|
355
|
+
Effect.fail(
|
|
356
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
yield* requestLogger.clear(path.id)
|
|
361
|
+
return { message: `Request log cleared for imposter ${path.id}` }
|
|
362
|
+
}))
|
|
363
|
+
.handle("getImposterStats", ({ path }) =>
|
|
364
|
+
Effect.gen(function*() {
|
|
365
|
+
const repo = yield* ImposterRepository
|
|
366
|
+
const metricsService = yield* MetricsService
|
|
367
|
+
yield* repo.get(path.id).pipe(
|
|
368
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
369
|
+
Effect.fail(
|
|
370
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
371
|
+
))
|
|
372
|
+
)
|
|
373
|
+
return yield* metricsService.getStats(path.id)
|
|
374
|
+
}))
|
|
375
|
+
.handle("resetImposterStats", ({ path }) =>
|
|
376
|
+
Effect.gen(function*() {
|
|
377
|
+
const repo = yield* ImposterRepository
|
|
378
|
+
const metricsService = yield* MetricsService
|
|
379
|
+
yield* repo.get(path.id).pipe(
|
|
380
|
+
Effect.catchTag("ImposterNotFoundError", (e) =>
|
|
381
|
+
Effect.fail(
|
|
382
|
+
new ApiNotFoundError({ message: "Imposter not found", resourceType: "imposter", resourceId: e.id })
|
|
383
|
+
))
|
|
384
|
+
)
|
|
385
|
+
yield* metricsService.resetStats(path.id)
|
|
386
|
+
return { message: `Statistics reset for imposter ${path.id}` }
|
|
387
|
+
})))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
|
|
2
|
+
import { HealthResponse, ServerInfoResponse } from "../schemas/ImposterSchema"
|
|
3
|
+
|
|
4
|
+
export const SystemGroup = HttpApiGroup.make("system", { topLevel: true })
|
|
5
|
+
.add(
|
|
6
|
+
HttpApiEndpoint.get("healthCheck", "/health")
|
|
7
|
+
.addSuccess(HealthResponse)
|
|
8
|
+
)
|
|
9
|
+
.add(
|
|
10
|
+
HttpApiEndpoint.get("serverInfo", "/info")
|
|
11
|
+
.addSuccess(ServerInfoResponse)
|
|
12
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { HttpApiBuilder } from "@effect/platform"
|
|
2
|
+
import * as Clock from "effect/Clock"
|
|
3
|
+
import * as DateTime from "effect/DateTime"
|
|
4
|
+
import * as Duration from "effect/Duration"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import { ImposterRepository } from "../repositories/ImposterRepository"
|
|
7
|
+
import { NonEmptyString, PortNumber } from "../schemas/common"
|
|
8
|
+
import { AppConfig } from "../services/AppConfig"
|
|
9
|
+
import { AdminApi } from "./AdminApi"
|
|
10
|
+
|
|
11
|
+
export const SystemHandlersLive = HttpApiBuilder.group(AdminApi, "system", (handlers) =>
|
|
12
|
+
handlers
|
|
13
|
+
.handle("healthCheck", () =>
|
|
14
|
+
Effect.gen(function*() {
|
|
15
|
+
const config = yield* AppConfig
|
|
16
|
+
const repo = yield* ImposterRepository
|
|
17
|
+
const all = yield* repo.getAll
|
|
18
|
+
|
|
19
|
+
const now = yield* Effect.map(Clock.currentTimeMillis, (ms) => DateTime.unsafeMake(ms))
|
|
20
|
+
const memUsage = process.memoryUsage()
|
|
21
|
+
|
|
22
|
+
const running = all.filter((r) => r.config.status === "running").length
|
|
23
|
+
const stopped = all.filter((r) => r.config.status === "stopped").length
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
status: "healthy" as const,
|
|
27
|
+
timestamp: now,
|
|
28
|
+
version: NonEmptyString.make("0.0.0"),
|
|
29
|
+
uptime: Duration.format(Duration.millis(process.uptime() * 1000)),
|
|
30
|
+
system: {
|
|
31
|
+
memory: {
|
|
32
|
+
used: NonEmptyString.make(`${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`),
|
|
33
|
+
free: NonEmptyString.make(`${Math.round((memUsage.heapTotal - memUsage.heapUsed) / 1024 / 1024)}MB`)
|
|
34
|
+
},
|
|
35
|
+
imposters: {
|
|
36
|
+
total: all.length,
|
|
37
|
+
running,
|
|
38
|
+
stopped
|
|
39
|
+
},
|
|
40
|
+
ports: {
|
|
41
|
+
available: config.portRangeMax - config.portRangeMin + 1 - all.length,
|
|
42
|
+
allocated: all.length
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}))
|
|
47
|
+
.handle("serverInfo", () =>
|
|
48
|
+
Effect.gen(function*() {
|
|
49
|
+
const config = yield* AppConfig
|
|
50
|
+
const now = yield* Effect.map(Clock.currentTimeMillis, (ms) => DateTime.unsafeMake(ms))
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
server: {
|
|
54
|
+
name: NonEmptyString.make("imposters"),
|
|
55
|
+
version: NonEmptyString.make("0.0.0"),
|
|
56
|
+
buildTime: now,
|
|
57
|
+
platform: NonEmptyString.make(process.platform),
|
|
58
|
+
protocols: ["HTTP" as const]
|
|
59
|
+
},
|
|
60
|
+
configuration: {
|
|
61
|
+
maxImposters: config.maxImposters,
|
|
62
|
+
portRange: {
|
|
63
|
+
min: PortNumber.make(config.portRangeMin),
|
|
64
|
+
max: PortNumber.make(config.portRangeMax)
|
|
65
|
+
},
|
|
66
|
+
defaultTimeout: 30000,
|
|
67
|
+
logLevel: config.logLevel
|
|
68
|
+
},
|
|
69
|
+
features: {
|
|
70
|
+
openApiGeneration: true,
|
|
71
|
+
clientGeneration: false,
|
|
72
|
+
authentication: false,
|
|
73
|
+
clustering: false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli"
|
|
2
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node"
|
|
3
|
+
import { Effect, Layer, Option } from "effect"
|
|
4
|
+
import { HandlerHttpClientLive } from "../client/HandlerHttpClient"
|
|
5
|
+
import { ImpostersClient, ImpostersClientLive } from "../client/ImpostersClient"
|
|
6
|
+
import { makeCompositeHandler } from "../server/AdminServer"
|
|
7
|
+
import { loadConfigFile } from "./ConfigLoader"
|
|
8
|
+
|
|
9
|
+
const configOption = Options.file("config").pipe(
|
|
10
|
+
Options.withAlias("c"),
|
|
11
|
+
Options.withDescription("Path to JSON config file"),
|
|
12
|
+
Options.optional
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const portOption = Options.integer("port").pipe(
|
|
16
|
+
Options.withAlias("p"),
|
|
17
|
+
Options.withDescription("Admin server port (default: 2525)"),
|
|
18
|
+
Options.optional
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const startCommand = Command.make(
|
|
22
|
+
"start",
|
|
23
|
+
{ config: configOption, port: portOption },
|
|
24
|
+
({ config, port }) =>
|
|
25
|
+
Effect.gen(function*() {
|
|
26
|
+
const adminPort = Option.isSome(port) ? port.value : Number(process.env.ADMIN_PORT ?? 2525)
|
|
27
|
+
|
|
28
|
+
const { dispose, handler } = makeCompositeHandler(adminPort)
|
|
29
|
+
const server = Bun.serve({ port: adminPort, fetch: handler })
|
|
30
|
+
|
|
31
|
+
console.log(`Imposters admin server running on http://localhost:${server.port}`)
|
|
32
|
+
console.log(`Admin UI: http://localhost:${server.port}/_ui`)
|
|
33
|
+
|
|
34
|
+
// Load config and create imposters if config file provided
|
|
35
|
+
if (Option.isSome(config)) {
|
|
36
|
+
const configData = yield* loadConfigFile(config.value).pipe(
|
|
37
|
+
Effect.catchTag("ConfigLoadError", (e) =>
|
|
38
|
+
Effect.sync(() => {
|
|
39
|
+
console.error(`Warning: ${e.message}`)
|
|
40
|
+
return null
|
|
41
|
+
}))
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (configData !== null && configData.imposters.length > 0) {
|
|
45
|
+
const clientLayer = ImpostersClientLive(`http://localhost:${server.port}`).pipe(
|
|
46
|
+
Layer.provide(HandlerHttpClientLive(handler))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
yield* Effect.provide(
|
|
50
|
+
Effect.gen(function*() {
|
|
51
|
+
const client = yield* ImpostersClient
|
|
52
|
+
for (const imp of configData.imposters) {
|
|
53
|
+
const created = yield* client.imposters.createImposter({
|
|
54
|
+
payload: {
|
|
55
|
+
port: imp.port,
|
|
56
|
+
...(imp.name !== undefined ? { name: imp.name } : {}),
|
|
57
|
+
protocol: "HTTP" as const,
|
|
58
|
+
adminPath: "/_admin"
|
|
59
|
+
}
|
|
60
|
+
}).pipe(Effect.catchAll((e) => {
|
|
61
|
+
console.error(`Failed to create imposter on port ${imp.port}: ${e}`)
|
|
62
|
+
return Effect.succeed(null)
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
if (created === null) continue
|
|
66
|
+
|
|
67
|
+
for (const stub of imp.stubs) {
|
|
68
|
+
yield* client.imposters.addStub({
|
|
69
|
+
path: { imposterId: created.id },
|
|
70
|
+
payload: stub
|
|
71
|
+
}).pipe(Effect.catchAll((e) => {
|
|
72
|
+
console.error(`Failed to add stub: ${e}`)
|
|
73
|
+
return Effect.void
|
|
74
|
+
}))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
yield* client.imposters.updateImposter({
|
|
78
|
+
path: { id: created.id },
|
|
79
|
+
payload: { status: "running" as const }
|
|
80
|
+
}).pipe(Effect.catchAll((e) => {
|
|
81
|
+
console.error(`Failed to start imposter ${created.id}: ${e}`)
|
|
82
|
+
return Effect.void
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
console.log(`Created imposter "${imp.name ?? created.id}" on port ${imp.port}`)
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
clientLayer
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Keep running until interrupted
|
|
94
|
+
yield* Effect.async<never, never>(() => {
|
|
95
|
+
const shutdown = () => {
|
|
96
|
+
console.log("Shutting down...")
|
|
97
|
+
server.stop(true)
|
|
98
|
+
dispose()
|
|
99
|
+
process.exit(0)
|
|
100
|
+
}
|
|
101
|
+
process.on("SIGINT", shutdown)
|
|
102
|
+
process.on("SIGTERM", shutdown)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const command = Command.make("imposters").pipe(
|
|
108
|
+
Command.withSubcommands([startCommand])
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
export const run = Command.run(command, {
|
|
112
|
+
name: "imposters",
|
|
113
|
+
version: "0.1.0"
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
export const main = run(process.argv).pipe(
|
|
117
|
+
Effect.provide(NodeContext.layer),
|
|
118
|
+
NodeRuntime.runMain
|
|
119
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from "effect"
|
|
2
|
+
import * as fs from "node:fs"
|
|
3
|
+
import { ConfigFile } from "../schemas/ConfigFileSchema"
|
|
4
|
+
|
|
5
|
+
export class ConfigLoadError extends Data.TaggedError("ConfigLoadError")<{
|
|
6
|
+
readonly message: string
|
|
7
|
+
readonly cause?: unknown
|
|
8
|
+
}> {}
|
|
9
|
+
|
|
10
|
+
export const loadConfigFile = (
|
|
11
|
+
filePath: string
|
|
12
|
+
): Effect.Effect<Schema.Schema.Type<typeof ConfigFile>, ConfigLoadError> =>
|
|
13
|
+
Effect.gen(function*() {
|
|
14
|
+
const content = yield* Effect.try({
|
|
15
|
+
try: () => fs.readFileSync(filePath, "utf-8"),
|
|
16
|
+
catch: (error) =>
|
|
17
|
+
new ConfigLoadError({
|
|
18
|
+
message: `Failed to read config file: ${filePath}`,
|
|
19
|
+
cause: error
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const json = yield* Effect.try({
|
|
24
|
+
try: () => JSON.parse(content) as unknown,
|
|
25
|
+
catch: (error) =>
|
|
26
|
+
new ConfigLoadError({
|
|
27
|
+
message: `Invalid JSON in config file: ${filePath}`,
|
|
28
|
+
cause: error
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return yield* Schema.decodeUnknown(ConfigFile)(json).pipe(
|
|
33
|
+
Effect.mapError(
|
|
34
|
+
(error) =>
|
|
35
|
+
new ConfigLoadError({
|
|
36
|
+
message: `Config validation failed: ${String(error)}`,
|
|
37
|
+
cause: error
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { HttpClient, HttpClientResponse } from "@effect/platform"
|
|
2
|
+
import { Effect, Layer } from "effect"
|
|
3
|
+
|
|
4
|
+
export const makeHandlerHttpClient = (
|
|
5
|
+
handler: (request: Request) => Promise<Response>
|
|
6
|
+
): HttpClient.HttpClient =>
|
|
7
|
+
HttpClient.make((request, url) =>
|
|
8
|
+
Effect.tryPromise({
|
|
9
|
+
try: async () => {
|
|
10
|
+
const method = request.method
|
|
11
|
+
const headers: Record<string, string> = {}
|
|
12
|
+
for (const key of Object.keys(request.headers)) {
|
|
13
|
+
headers[key] = request.headers[key]!
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let body: BodyInit | undefined
|
|
17
|
+
switch (request.body._tag) {
|
|
18
|
+
case "Uint8Array":
|
|
19
|
+
body = new globalThis.Uint8Array(request.body.body)
|
|
20
|
+
break
|
|
21
|
+
case "Raw": {
|
|
22
|
+
const raw = request.body.body
|
|
23
|
+
body = typeof raw === "string" ? raw : JSON.stringify(raw)
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
case "Empty":
|
|
27
|
+
body = undefined
|
|
28
|
+
break
|
|
29
|
+
default:
|
|
30
|
+
body = undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const webRequest = new Request(url.toString(), {
|
|
34
|
+
method,
|
|
35
|
+
headers,
|
|
36
|
+
...(body !== undefined ? { body } : {})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const webResponse = await handler(webRequest)
|
|
40
|
+
return HttpClientResponse.fromWeb(request, webResponse)
|
|
41
|
+
},
|
|
42
|
+
catch: (error) => {
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export const HandlerHttpClientLive = (
|
|
49
|
+
handler: (request: Request) => Promise<Response>
|
|
50
|
+
): Layer.Layer<HttpClient.HttpClient> => Layer.succeed(HttpClient.HttpClient, makeHandlerHttpClient(handler))
|