@zudojs/openapi 1.3.0 → 1.4.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.
@@ -1,5 +1,27 @@
1
- import { convertRouteToOpenAPI } from "./routeConverter.core.js";
1
+ import { convertRouteToOpenAPI, toOpenAPIPath } from "./routeConverter.core.js";
2
2
  import { OpenAPIRouteError } from "../openApiErrors/openApiError.types.js";
3
+ /**
4
+ * Identity of a route inside the generated document.
5
+ *
6
+ * Keyed on the OpenAPI path template rather than the source path, because
7
+ * that is what the document is keyed on: `/users/:id` and `/users/{id}` are
8
+ * one path item. Keying on the raw spelling let both register, and the
9
+ * second then replaced the first during generation — one operation vanished
10
+ * from the published spec with `validate()` reporting nothing.
11
+ *
12
+ * A path `toOpenAPIPath` cannot express keeps its raw spelling here so the
13
+ * conversion error still surfaces from `scan()`, where it always has.
14
+ */
15
+ function routeKey(method, path) {
16
+ let template;
17
+ try {
18
+ template = toOpenAPIPath(path);
19
+ }
20
+ catch {
21
+ template = path;
22
+ }
23
+ return `${method.toLowerCase()}:${template}`;
24
+ }
3
25
  /**
4
26
  * Collects routes and converts them into OpenAPI operations.
5
27
  *
@@ -12,23 +34,33 @@ export class OpenAPIRouteScannerImpl {
12
34
  routes = new Map();
13
35
  /** Registers a route. */
14
36
  addRoute(route) {
15
- const key = `${route.method.toLowerCase()}:${route.path}`;
16
- if (this.routes.has(key)) {
17
- throw new OpenAPIRouteError(`Route ${route.method.toUpperCase()} ${route.path} is already registered.`, { metadata: { method: route.method, path: route.path } });
37
+ const key = routeKey(route.method, route.path);
38
+ const existing = this.routes.get(key);
39
+ if (existing !== undefined) {
40
+ throw new OpenAPIRouteError(`Route ${route.method.toUpperCase()} ${route.path} is already registered` +
41
+ (existing.path === route.path
42
+ ? "."
43
+ : ` as ${existing.method.toUpperCase()} ${existing.path}; both describe the same OpenAPI path.`), {
44
+ metadata: {
45
+ method: route.method,
46
+ path: route.path,
47
+ existingPath: existing.path,
48
+ },
49
+ });
18
50
  }
19
51
  this.routes.set(key, route);
20
52
  }
21
53
  /** Registers a route, replacing any existing one for the same method+path. */
22
54
  setRoute(route) {
23
- this.routes.set(`${route.method.toLowerCase()}:${route.path}`, route);
55
+ this.routes.set(routeKey(route.method, route.path), route);
24
56
  }
25
57
  /** True when a route is registered for this method and path. */
26
58
  hasRoute(method, path) {
27
- return this.routes.has(`${method.toLowerCase()}:${path}`);
59
+ return this.routes.has(routeKey(method, path));
28
60
  }
29
61
  /** Removes a route. Returns whether one was removed. */
30
62
  removeRoute(method, path) {
31
- return this.routes.delete(`${method.toLowerCase()}:${path}`);
63
+ return this.routes.delete(routeKey(method, path));
32
64
  }
33
65
  /** Number of registered routes. */
34
66
  get size() {
@@ -11,6 +11,8 @@ import type { OpenAPISchema } from "../openApiTypes/openApiTypes.core.js";
11
11
  *
12
12
  * Field names as of `@zudojs/schema@0.1.0`:
13
13
  * object `_config.shape`, `_config.requiredKeys` (a Set), `_config.unknownKeys`
14
+ * (`"strip" | "strict" | "passthrough"`; absent means the parser's
15
+ * own `?? "strip"` default, so it is read with that same default)
14
16
  * array `_config.itemSchema`, `_config.min`, `_config.max`, `_config.length`
15
17
  * (no `max` means the parser's implicit ceiling, which is emitted)
16
18
  * string `_config.min|max|length|pattern|format`
@@ -309,14 +309,19 @@ function convertSchemaNode(schema, state) {
309
309
  if (forced || !acceptsMissingKey(value))
310
310
  required.push(key);
311
311
  }
312
- const unknownKeys = c["unknownKeys"];
312
+ // `ObjectSchema` applies `?? "strip"` internally, so an absent
313
+ // `unknownKeys` is the same contract as an explicit `.strip()` and
314
+ // must document the same. Only `strict` emits
315
+ // `additionalProperties: false`: that is OpenAPI for "reject the
316
+ // payload", while strip accepts it and discards the extra key, so
317
+ // emitting it for strip made a generated client refuse what the
318
+ // service accepts.
319
+ const unknownKeys = c["unknownKeys"] ?? "strip";
313
320
  return {
314
321
  type: "object",
315
322
  ...(Object.keys(properties).length > 0 ? { properties } : {}),
316
323
  ...(required.length > 0 ? { required } : {}),
317
- ...(unknownKeys === "strip" || unknownKeys === "strict"
318
- ? { additionalProperties: false }
319
- : {}),
324
+ ...(unknownKeys === "strict" ? { additionalProperties: false } : {}),
320
325
  };
321
326
  }
322
327
  case "record": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/openapi",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "OpenAPI 3.0 and 3.1 specification generation, validation, and serialization for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -26,8 +26,8 @@
26
26
  "!dist/.tsbuildinfo"
27
27
  ],
28
28
  "dependencies": {
29
- "@zudojs/constants": "1.1.0",
30
- "@zudojs/errors": "1.1.0"
29
+ "@zudojs/constants": "1.1.1",
30
+ "@zudojs/errors": "1.2.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^26.4.1",