aecfe699f4362407a1aaadab82c58b236e3b1e2a
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
70bb011daa |
fix: map FlatEntityMaps and WorkspaceMigrationRunner exceptions to proper status codes on REST and GraphQL (#20494)
## Context Calling `POST /rest/views` (and other metadata mutations) currently returns a generic `500` for user-input failures: Ex: 1. **Invalid `objectMetadataId`** — `resolveEntityRelationUniversalIdentifiers` throws `FlatEntityMapsException(RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND)`. Should be `404`. 2. **Missing required field** (e.g. `icon`) — Postgres raises a `NOT NULL` violation, wrapped as `WorkspaceMigrationRunnerException(EXECUTION_FAILED)` carrying a `QueryFailedError`. Should be `400`. Neither was caught by `ViewRestApiExceptionFilter`, so both fell through to `UnhandledExceptionFilter` and were emitted as `500`s without reaching Sentry. Same gap existed on most metadata GraphQL resolvers — only `page-layout*` and `role` resolvers covered `WorkspaceMigrationRunnerException` via `WorkspaceMigrationGraphqlApiExceptionInterceptor`. ## Changes ### New filters REST (`HttpExceptionHandlerService` + Sentry-aware): - `FlatEntityMapsRestApiExceptionFilter` — maps `RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND` / `ENTITY_NOT_FOUND` → `404`, `ENTITY_ALREADY_EXISTS` → `409`, others → `500`. - `WorkspaceMigrationRunnerRestApiExceptionFilter` — for `EXECUTION_FAILED`, unwraps the underlying `metadata` / `workspaceSchema` / `actionTranspilation` error; if it's a `QueryFailedError` it gets remapped to `400` via `HttpExceptionHandlerService`. `APPLICATION_NOT_FOUND` → `404`, `DDL_LOCKED` → `503`, otherwise `500`. GraphQL (graphql-errors + existing formatter): - `FlatEntityMapsGraphqlApiExceptionFilter` — kept as the GraphQL-shaped counterpart (`NotFoundError` / `InternalServerError`). - `WorkspaceMigrationRunnerGraphqlApiExceptionFilter` — reuses `workspaceMigrationRunnerExceptionFormatter` for parity with the existing interceptor. ### Wiring Filters are now declared **per controller / resolver** via `@UseFilters` (no global `APP_FILTER` registration) so they participate in the normal NestJS filter chain instead of being preempted by `UnhandledExceptionFilter`. REST: - `view.controller.ts` — adds `FlatEntityMapsRestApiExceptionFilter` and `WorkspaceMigrationRunnerRestApiExceptionFilter`. GraphQL (14 resolvers, all that mutate flat entities): - `FlatEntityMapsGraphqlApiExceptionFilter` added to: `view`, `view-field`, `view-field-group`, `view-sort`, `view-group`, `view-filter`, `view-filter-group`, `page-layout`, `page-layout-tab`, `page-layout-widget`, `role`, `object-metadata`, `field-metadata`, `index-metadata`. - `WorkspaceMigrationRunnerGraphqlApiExceptionFilter` added to the same list **except** the four already covered by `WorkspaceMigrationGraphqlApiExceptionInterceptor` (`page-layout`, `page-layout-tab`, `page-layout-widget`, `role`) — to avoid double-handling. ## Why per-resolver / per-controller instead of global Earlier attempt to register the filters globally via `APP_FILTER` regressed: NestJS reverses the global filter list and `selectExceptionFilterMetadata` is first-match-wins, so `UnhandledExceptionFilter` (registered last via `app.useGlobalFilters` in `main.ts`) ended up first in the iteration order and preempted every domain-specific filter. The per-resolver / per-controller approach is explicit and predictable. ## Before <img width="953" height="450" alt="Screenshot 2026-05-12 at 15 31 40" src="https://github.com/user-attachments/assets/3c3bc6a8-f6bc-4032-97d0-7243540cfb90" /> ## After <img width="1050" height="598" alt="Screenshot 2026-05-12 at 15 31 17" src="https://github.com/user-attachments/assets/c66c9ce5-d1ea-4f1d-b2fe-07979e2261f7" /> <img width="1068" height="503" alt="Screenshot 2026-05-12 at 15 31 09" src="https://github.com/user-attachments/assets/ddd9eed8-812b-47d6-96cb-b019b807991b" /> |
||
|
|
96bc3594a3 |
Serve frontend components (#17798)
## Context Ability to serve frontend component See: ```typescript curl -i 'http://localhost:3000/rest/front-components/35063b3f-bc4c-4358-8966-7762677802a3' \ --header 'Authorization: Bearer eyJhb...' HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Content-Type: application/javascript Date: Mon, 09 Feb 2026 10:28:17 GMT Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked // react-globals:react/jsx-runtime var jsx = globalThis.jsx; var jsxs = globalThis.jsxs; var Fragment = globalThis.React.Fragment; // src/front-components/test.tsx var RemoteComponents = globalThis.RemoteComponents; var Component = () => { return /* @__PURE__ */ jsxs(RemoteComponents.HtmlDiv, { style: { padding: "20px", fontFamily: "sans-serif" }, children: [ /* @__PURE__ */ jsx(RemoteComponents.HtmlH1, { children: "My new component!" }), /* @__PURE__ */ jsx(RemoteComponents.HtmlP, { children: "This is your front component: test" }) ] }); }; var test_default = globalThis.jsx(Component, {}); export { test_default as default }; //# sourceMappingURL=test.mjs.map ``` readFile_v2 returns a Node.js Stream object (Readable). Here we are using stream pipeline which connects the readable stream (file) to the writable stream (HTTP response) which efficiently streams the file content directly to the HTTP response without loading the entire file into memory. (in chunks, handling backpressure and closing the connection when the file is fully sent) |