@webpieces/http-server 0.4.612 → 0.4.613

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-server",
3
- "version": "0.4.612",
3
+ "version": "0.4.613",
4
4
  "description": "WebPieces server with filter chain and dependency injection",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,10 +22,10 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@webpieces/core-context": "0.4.612",
26
- "@webpieces/core-util": "0.4.612",
27
- "@webpieces/gcp-identity": "0.4.612",
28
- "@webpieces/http-routing": "0.4.612",
25
+ "@webpieces/core-context": "0.4.613",
26
+ "@webpieces/core-util": "0.4.613",
27
+ "@webpieces/gcp-identity": "0.4.613",
28
+ "@webpieces/http-routing": "0.4.613",
29
29
  "cors": "2.8.5",
30
30
  "express": "5.1.0",
31
31
  "inversify": "7.10.4"
@@ -47,7 +47,7 @@ export declare class WebpiecesExpressRouter {
47
47
  /**
48
48
  * The "Svr Ready!!" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and
49
49
  * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied
50
- * verbatim from the trytami service so a familiar splash marks "the server is up and reachable".
50
+ * verbatim from the production service it was ported from, so a familiar splash marks "the server is up and reachable".
51
51
  */
52
52
  private logStartupBanner;
53
53
  /**
@@ -86,7 +86,7 @@ class WebpiecesExpressRouter {
86
86
  /**
87
87
  * The "Svr Ready!!" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and
88
88
  * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied
89
- * verbatim from the trytami service so a familiar splash marks "the server is up and reachable".
89
+ * verbatim from the production service it was ported from, so a familiar splash marks "the server is up and reachable".
90
90
  */
91
91
  logStartupBanner(port) {
92
92
  if (process.env['K_SERVICE']) {
@@ -1 +1 @@
1
- {"version":3,"file":"WebpiecesExpressRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpressRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { ApiFactory, ApiClient, getApiPath, getEndpoints, isFormPost, WebpiecesConfig } from '@webpieces/http-routing';\nimport { LogManager } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\n\nconst log = LogManager.getLogger('WebpiecesExpressRouter');\n\n/** The value returned by express `app.listen(...)` (a node http.Server). */\ntype HttpServer = ReturnType<Express['listen']>;\n\n/**\n * WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only\n * {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.\n *\n * It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each\n * an api + routeMeta + composed filter-chain→controller impl — and binds each to an express\n * route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The\n * RouteBuilder stays hidden inside the ApiFactory.\n *\n * ```typescript\n * const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });\n * apiFactory.addRoutes(SaveApi, SaveController);\n * const express = new WebpiecesExpressRouter(apiFactory);\n *\n * // legacy / side-by-side: mount onto an existing app; you own listen + your middleware\n * express.bindExpress(existingApp);\n *\n * // non-legacy: add webpieces global middleware + listen for you\n * await express.bindAndStartExpress(express(), 8080);\n * ```\n */\nexport class WebpiecesExpressRouter {\n private readonly middleware = new WebpiecesMiddleware();\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,\n * express-tier + api-tier filter chain, error→JSON) onto the caller's express app.\n *\n * Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other\n * routes must stay untouched. The caller owns app.listen() and any global middleware.\n */\n bindExpress(app: Express): void {\n let count = 0;\n for (const apiClient of this.apiFactory.apiClients()) {\n count += this.mountApiClient(app, apiClient);\n }\n log.info(`Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level\n * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where\n * webpieces owns the whole express app. Resolves with the http.Server once listening.\n *\n * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and\n * {@link WebpiecesMiddleware.corsMiddleware}.\n */\n async bindAndStartExpress(\n app: Express,\n port: number = 8080,\n config?: WebpiecesConfig,\n ): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does\n // not need it — a browser applies no cors check to a same-origin request — so mounting it\n // would only hand credentialed cross-origin read access to whatever it allows, for nothing.\n // It is needed solely when a browser on ANOTHER origin calls this api: `ng serve` in dev, or\n // a UI hosted on a different host. Those say so via corsOrigins.\n const corsOrigins = config?.corsOrigins ?? [];\n if (corsOrigins.length > 0) {\n app.use(this.middleware.corsMiddleware(config));\n }\n\n this.bindExpress(app);\n\n // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a\n // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does\n // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.\n app.use(this.middleware.errorHandler.bind(this.middleware));\n\n return new Promise<HttpServer>(\n (resolve: (server: HttpServer) => void, reject: (err: Error) => void) => {\n const server: HttpServer = app.listen(port, (error?: Error) => {\n if (error) {\n log.error(`Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`Listening on http://localhost:${port}`);\n this.logStartupBanner(port);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * The \"Svr Ready!!\" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and\n * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied\n * verbatim from the trytami service so a familiar splash marks \"the server is up and reachable\".\n */\n private logStartupBanner(port: number): void {\n if (process.env['K_SERVICE']) {\n return;\n }\n log.info(`\n ___ _____ _\n/ _| | _ \\\\ | |\n\\\\ \\`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _\n \\`--. \\\\/ _ \\\\ '_\\\\ \\\\ / / _ \\\\ '_| | // _ \\\\/ _\\` |/ _\\` | | | |\n/\\\\_/ / _/ | \\\\ V / _/ | | |\\\\ \\\\ _/ (_| | (_| | |_| |\n\\\\___/ \\\\_|_| \\\\_/ \\\\_|_| \\\\_| \\\\_\\\\_|\\\\_,_|\\\\_,_|\\\\_, |\n _/ |\n |_/\n\n Svr Ready!! port=${port}\n`);\n }\n\n /**\n * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;\n * for each we wrap the matching client method (the proxy — RequestContext.run + header read +\n * JSON body parse + error→ProtocolError all live in the wrapper/chain) and register the route.\n * This is one-to-one with a test: an HTTP POST maps straight to `client[method](dto)`.\n *\n * @returns the number of routes mounted for this api.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): number {\n const basePath = getApiPath(apiClient.api) || '';\n const endpoints = getEndpoints(apiClient.api) || {};\n let count = 0;\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const path = basePath + endpointPath;\n // The parser is chosen by the @Endpoint annotation, not the request Content-Type.\n const wrapper = this.middleware.createExpressWrapper(\n apiClient.client[methodName],\n path,\n isFormPost(apiClient.api, methodName),\n );\n // All webpieces routes are POST (the api-tier convention).\n this.registerHandler(app, 'POST', path, wrapper.execute.bind(wrapper));\n count++;\n }\n return count;\n }\n\n private registerHandler(\n app: Express,\n httpMethod: string,\n path: string,\n expressHandler: ExpressRouteHandler,\n ): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n app.get(path, expressHandler);\n break;\n case 'post':\n app.post(path, expressHandler);\n break;\n case 'put':\n app.put(path, expressHandler);\n break;\n case 'delete':\n app.delete(path, expressHandler);\n break;\n case 'patch':\n app.patch(path, expressHandler);\n break;\n default:\n log.warn(`Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"WebpiecesExpressRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpressRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { ApiFactory, ApiClient, getApiPath, getEndpoints, isFormPost, WebpiecesConfig } from '@webpieces/http-routing';\nimport { LogManager } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\n\nconst log = LogManager.getLogger('WebpiecesExpressRouter');\n\n/** The value returned by express `app.listen(...)` (a node http.Server). */\ntype HttpServer = ReturnType<Express['listen']>;\n\n/**\n * WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only\n * {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.\n *\n * It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each\n * an api + routeMeta + composed filter-chain→controller impl — and binds each to an express\n * route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The\n * RouteBuilder stays hidden inside the ApiFactory.\n *\n * ```typescript\n * const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });\n * apiFactory.addRoutes(SaveApi, SaveController);\n * const express = new WebpiecesExpressRouter(apiFactory);\n *\n * // legacy / side-by-side: mount onto an existing app; you own listen + your middleware\n * express.bindExpress(existingApp);\n *\n * // non-legacy: add webpieces global middleware + listen for you\n * await express.bindAndStartExpress(express(), 8080);\n * ```\n */\nexport class WebpiecesExpressRouter {\n private readonly middleware = new WebpiecesMiddleware();\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,\n * express-tier + api-tier filter chain, error→JSON) onto the caller's express app.\n *\n * Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other\n * routes must stay untouched. The caller owns app.listen() and any global middleware.\n */\n bindExpress(app: Express): void {\n let count = 0;\n for (const apiClient of this.apiFactory.apiClients()) {\n count += this.mountApiClient(app, apiClient);\n }\n log.info(`Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level\n * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where\n * webpieces owns the whole express app. Resolves with the http.Server once listening.\n *\n * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and\n * {@link WebpiecesMiddleware.corsMiddleware}.\n */\n async bindAndStartExpress(\n app: Express,\n port: number = 8080,\n config?: WebpiecesConfig,\n ): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does\n // not need it — a browser applies no cors check to a same-origin request — so mounting it\n // would only hand credentialed cross-origin read access to whatever it allows, for nothing.\n // It is needed solely when a browser on ANOTHER origin calls this api: `ng serve` in dev, or\n // a UI hosted on a different host. Those say so via corsOrigins.\n const corsOrigins = config?.corsOrigins ?? [];\n if (corsOrigins.length > 0) {\n app.use(this.middleware.corsMiddleware(config));\n }\n\n this.bindExpress(app);\n\n // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a\n // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does\n // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.\n app.use(this.middleware.errorHandler.bind(this.middleware));\n\n return new Promise<HttpServer>(\n (resolve: (server: HttpServer) => void, reject: (err: Error) => void) => {\n const server: HttpServer = app.listen(port, (error?: Error) => {\n if (error) {\n log.error(`Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`Listening on http://localhost:${port}`);\n this.logStartupBanner(port);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * The \"Svr Ready!!\" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and\n * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied\n * verbatim from the production service it was ported from, so a familiar splash marks \"the server is up and reachable\".\n */\n private logStartupBanner(port: number): void {\n if (process.env['K_SERVICE']) {\n return;\n }\n log.info(`\n ___ _____ _\n/ _| | _ \\\\ | |\n\\\\ \\`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _\n \\`--. \\\\/ _ \\\\ '_\\\\ \\\\ / / _ \\\\ '_| | // _ \\\\/ _\\` |/ _\\` | | | |\n/\\\\_/ / _/ | \\\\ V / _/ | | |\\\\ \\\\ _/ (_| | (_| | |_| |\n\\\\___/ \\\\_|_| \\\\_/ \\\\_|_| \\\\_| \\\\_\\\\_|\\\\_,_|\\\\_,_|\\\\_, |\n _/ |\n |_/\n\n Svr Ready!! port=${port}\n`);\n }\n\n /**\n * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;\n * for each we wrap the matching client method (the proxy — RequestContext.run + header read +\n * JSON body parse + error→ProtocolError all live in the wrapper/chain) and register the route.\n * This is one-to-one with a test: an HTTP POST maps straight to `client[method](dto)`.\n *\n * @returns the number of routes mounted for this api.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): number {\n const basePath = getApiPath(apiClient.api) || '';\n const endpoints = getEndpoints(apiClient.api) || {};\n let count = 0;\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const path = basePath + endpointPath;\n // The parser is chosen by the @Endpoint annotation, not the request Content-Type.\n const wrapper = this.middleware.createExpressWrapper(\n apiClient.client[methodName],\n path,\n isFormPost(apiClient.api, methodName),\n );\n // All webpieces routes are POST (the api-tier convention).\n this.registerHandler(app, 'POST', path, wrapper.execute.bind(wrapper));\n count++;\n }\n return count;\n }\n\n private registerHandler(\n app: Express,\n httpMethod: string,\n path: string,\n expressHandler: ExpressRouteHandler,\n ): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n app.get(path, expressHandler);\n break;\n case 'post':\n app.post(path, expressHandler);\n break;\n case 'put':\n app.put(path, expressHandler);\n break;\n case 'delete':\n app.delete(path, expressHandler);\n break;\n case 'patch':\n app.patch(path, expressHandler);\n break;\n default:\n log.warn(`Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}