nestjs-openapi 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,35 +1,32 @@
1
- # nestjs-openapi
1
+ <div align="center">
2
+ <img src="docs/public/logo.png" alt="nestjs-openapi" width="120" />
3
+ <h1>nestjs-openapi</h1>
4
+ <p>Static OpenAPI generation for NestJS.<br/>Analyzes TypeScript source directly—no build step, no app bootstrap.</p>
2
5
 
3
- Static OpenAPI generation for NestJS. Analyzes TypeScript source directly—no build step, no app bootstrap.
6
+ [![npm version](https://img.shields.io/npm/v/nestjs-openapi)](https://www.npmjs.com/package/nestjs-openapi)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
8
 
5
- [![npm version](https://img.shields.io/npm/v/nestjs-openapi)](https://www.npmjs.com/package/nestjs-openapi)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
+ <a href="https://nestjs-openapi.vercel.app">Documentation</a> · <a href="https://nestjs-openapi.vercel.app/docs/quick-start">Quick Start</a> · <a href="https://github.com/Newbie012/nestjs-openapi/issues">Report Bug</a>
10
+ </div>
7
11
 
8
- ## Why static analysis?
12
+ <br/>
9
13
 
10
- | | Runtime (`@nestjs/swagger`) | Static (`nestjs-openapi`) |
11
- |---|---|---|
12
- | Requires runtime execution | Yes | No |
13
- | Requires app bootstrap | Yes | No |
14
- | Preserves generics/unions | No | Yes |
14
+ ## Motivation
15
15
 
16
- ## Who is this for?
16
+ `@nestjs/swagger` relies on `reflect-metadata` at runtime, which only exposes basic type signatures. Unions, generics, and literal types are erased. To work around this, you duplicate type information in decorators:
17
17
 
18
- - Teams who want accurate OpenAPI from TypeScript types without runtime metadata
19
- - CI/CD pipelines that should not boot the app or require infrastructure
20
- - Projects that want OpenAPI as a build artifact or committed output
21
-
22
- ## Compatibility
18
+ ```typescript
19
+ // You already have this type
20
+ status: 'pending' | 'shipped' | 'delivered';
23
21
 
24
- - NestJS 10 or 11 (decorator-based controllers)
25
- - TypeScript 5+
26
- - Node 20+
22
+ // But you also need this decorator to make the spec accurate
23
+ @ApiProperty({ enum: ['pending', 'shipped', 'delivered'] })
24
+ status: 'pending' | 'shipped' | 'delivered';
25
+ ```
27
26
 
28
- ## Limitations
27
+ When they drift apart, your spec lies about your API.
29
28
 
30
- - Dynamic route registration at runtime is not supported
31
- - Response shortcut decorators like `@ApiOkResponse()` are not read
32
- - Controller versioning via `@Controller({ path, version })` is not supported
29
+ **nestjs-openapi** reads your TypeScript source directly using the AST. Your types are your spec—no duplication, no drift.
33
30
 
34
31
  ## Quick start
35
32
 
@@ -59,27 +56,16 @@ Generate:
59
56
  npx nestjs-openapi generate
60
57
  ```
61
58
 
62
- ## Migration from @nestjs/swagger
63
-
64
- 1. Keep your controllers and route decorators as-is.
65
- 2. Move `DocumentBuilder` config into `openapi.config.ts`.
66
- 3. Replace response shortcuts with `@ApiResponse({ status: ... })`.
67
- 4. (Optional) Use `OpenApiModule` to serve the generated spec at runtime.
68
-
69
- See the full guide in the docs.
70
-
71
59
  ## Documentation
72
60
 
73
- Full documentation: **[nestjs-openapi.dev](https://nestjs-openapi.dev)**
61
+ Full documentation at **[nestjs-openapi.vercel.app](https://nestjs-openapi.vercel.app)**
74
62
 
75
- - [Configuration](https://nestjs-openapi.dev/docs/guides/configuration)
76
- - [Security schemes](https://nestjs-openapi.dev/docs/guides/security)
77
- - [Validation extraction](https://nestjs-openapi.dev/docs/guides/validation)
78
- - [Serving specs at runtime](https://nestjs-openapi.dev/docs/guides/serving)
79
- - [Migration guide](https://nestjs-openapi.dev/docs/recipes/migration)
80
- - [CI/CD recipe](https://nestjs-openapi.dev/docs/recipes/ci-cd)
81
- - [FAQ](https://nestjs-openapi.dev/docs/faq)
82
- - [Decorator support](https://nestjs-openapi.dev/docs/guides/decorators)
63
+ - [Configuration](https://nestjs-openapi.vercel.app/docs/guides/configuration)
64
+ - [Security schemes](https://nestjs-openapi.vercel.app/docs/guides/security)
65
+ - [Serving specs at runtime](https://nestjs-openapi.vercel.app/docs/guides/serving)
66
+ - [Migration from @nestjs/swagger](https://nestjs-openapi.vercel.app/docs/recipes/migration)
67
+ - [CI/CD recipe](https://nestjs-openapi.vercel.app/docs/recipes/ci-cd)
68
+ - [FAQ](https://nestjs-openapi.vercel.app/docs/faq)
83
69
 
84
70
  ## Contributing
85
71
 
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import 'tsx';
3
- import { g as generate, e as formatValidationResult } from './shared/nestjs-openapi.DlNMM8Zq.mjs';
3
+ import { g as generate, e as formatValidationResult } from './shared/nestjs-openapi.yWMsjl_8.mjs';
4
4
  import minimist from 'minimist';
5
5
  import { relative } from 'node:path';
6
6
  import { createRequire } from 'node:module';
@@ -9,7 +9,7 @@ import 'node:fs';
9
9
  import 'ts-morph';
10
10
  import 'glob';
11
11
  import 'js-yaml';
12
- import './shared/nestjs-openapi.B1bBy_tG.mjs';
12
+ import './shared/nestjs-openapi.CBj9xHZ4.mjs';
13
13
  import 'ts-json-schema-generator';
14
14
  import 'node:crypto';
15
15
  import 'node:url';
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Effect, Context, Layer, Option } from 'effect';
2
- import { C as ConfigNotFoundError, O as OpenApiGeneratorConfig, a as ConfigError, R as ResolvedConfig, P as ProjectError, b as ProjectInitError, E as EntryNotFoundError, M as MethodInfo, c as OpenApiPaths$1 } from './shared/nestjs-openapi.BYUrTaMo.mjs';
3
- export { A as AnalysisError, i as ConfigLoadError, j as ConfigValidationError, G as GenerateOptions, k as GeneratorError, H as HttpMethod, I as InvalidMethodError, e as ParameterLocation, f as ResolvedParameter, h as ReturnTypeInfo, d as generateAsync, g as generateEffect } from './shared/nestjs-openapi.BYUrTaMo.mjs';
2
+ import { C as ConfigNotFoundError, O as OpenApiGeneratorConfig, a as ConfigError, R as ResolvedConfig, P as ProjectError, b as ProjectInitError, E as EntryNotFoundError, M as MethodInfo, c as OpenApiPaths$1 } from './shared/nestjs-openapi.OhsaHu32.mjs';
3
+ export { A as AnalysisError, i as ConfigLoadError, j as ConfigValidationError, G as GenerateOptions, k as GeneratorError, H as HttpMethod, I as InvalidMethodError, e as ParameterLocation, f as ResolvedParameter, h as ReturnTypeInfo, d as generateAsync, g as generateEffect } from './shared/nestjs-openapi.OhsaHu32.mjs';
4
4
  import { DynamicModule } from '@nestjs/common';
5
5
  import { Project, SourceFile, ClassDeclaration, MethodDeclaration, Decorator, Symbol, ObjectLiteralExpression, Expression } from 'ts-morph';
6
6
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Effect, Context, Layer, Option } from 'effect';
2
- import { C as ConfigNotFoundError, O as OpenApiGeneratorConfig, a as ConfigError, R as ResolvedConfig, P as ProjectError, b as ProjectInitError, E as EntryNotFoundError, M as MethodInfo, c as OpenApiPaths$1 } from './shared/nestjs-openapi.BYUrTaMo.js';
3
- export { A as AnalysisError, i as ConfigLoadError, j as ConfigValidationError, G as GenerateOptions, k as GeneratorError, H as HttpMethod, I as InvalidMethodError, e as ParameterLocation, f as ResolvedParameter, h as ReturnTypeInfo, d as generateAsync, g as generateEffect } from './shared/nestjs-openapi.BYUrTaMo.js';
2
+ import { C as ConfigNotFoundError, O as OpenApiGeneratorConfig, a as ConfigError, R as ResolvedConfig, P as ProjectError, b as ProjectInitError, E as EntryNotFoundError, M as MethodInfo, c as OpenApiPaths$1 } from './shared/nestjs-openapi.OhsaHu32.js';
3
+ export { A as AnalysisError, i as ConfigLoadError, j as ConfigValidationError, G as GenerateOptions, k as GeneratorError, H as HttpMethod, I as InvalidMethodError, e as ParameterLocation, f as ResolvedParameter, h as ReturnTypeInfo, d as generateAsync, g as generateEffect } from './shared/nestjs-openapi.OhsaHu32.js';
4
4
  import { DynamicModule } from '@nestjs/common';
5
5
  import { Project, SourceFile, ClassDeclaration, MethodDeclaration, Decorator, Symbol, ObjectLiteralExpression, Expression } from 'ts-morph';
6
6
 
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- export { c as categorizeBrokenRefs, d as defineConfig, f as findConfigFile, e as formatValidationResult, g as generate, b as loadAndResolveConfig, a as loadConfig, l as loadConfigFromFile, r as resolveConfig, v as validateSpec } from './shared/nestjs-openapi.DlNMM8Zq.mjs';
1
+ export { c as categorizeBrokenRefs, d as defineConfig, f as findConfigFile, e as formatValidationResult, g as generate, b as loadAndResolveConfig, a as loadConfig, l as loadConfigFromFile, r as resolveConfig, v as validateSpec } from './shared/nestjs-openapi.yWMsjl_8.mjs';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { Module } from '@nestjs/common';
5
5
  export { generateAsync, generate as generateEffect } from './internal.mjs';
6
- import { P as ProjectInitError, E as EntryNotFoundError } from './shared/nestjs-openapi.B1bBy_tG.mjs';
7
- export { a as ConfigLoadError, C as ConfigNotFoundError, b as ConfigValidationError, I as InvalidMethodError, c as getAllControllers, q as getArrayInitializer, o as getControllerMethodInfos, e as getControllerName, d as getControllerPrefix, j as getControllerTags, h as getDecoratorName, k as getHttpDecorator, f as getHttpMethods, m as getMethodInfo, w as getModuleDecoratorArg, z as getModuleMetadata, g as getModules, s as getStringLiteralValue, u as getSymbolFromIdentifier, l as isHttpDecorator, i as isHttpMethod, v as isModuleClass, n as normalizePath, y as resolveArrayOfClasses, x as resolveClassFromExpression, r as resolveClassFromSymbol, t as transformMethod, p as transformMethods } from './shared/nestjs-openapi.B1bBy_tG.mjs';
6
+ import { P as ProjectInitError, E as EntryNotFoundError } from './shared/nestjs-openapi.CBj9xHZ4.mjs';
7
+ export { a as ConfigLoadError, C as ConfigNotFoundError, b as ConfigValidationError, I as InvalidMethodError, c as getAllControllers, q as getArrayInitializer, o as getControllerMethodInfos, e as getControllerName, d as getControllerPrefix, j as getControllerTags, h as getDecoratorName, k as getHttpDecorator, f as getHttpMethods, m as getMethodInfo, w as getModuleDecoratorArg, z as getModuleMetadata, g as getModules, s as getStringLiteralValue, u as getSymbolFromIdentifier, l as isHttpDecorator, i as isHttpMethod, v as isModuleClass, n as normalizePath, y as resolveArrayOfClasses, x as resolveClassFromExpression, r as resolveClassFromSymbol, t as transformMethod, p as transformMethods } from './shared/nestjs-openapi.CBj9xHZ4.mjs';
8
8
  import { Context, Effect, Layer } from 'effect';
9
9
  import { Project } from 'ts-morph';
10
10
  import 'glob';
@@ -1,2 +1,2 @@
1
1
  import 'effect';
2
- export { G as GenerateOptions, g as generate, d as generateAsync } from './shared/nestjs-openapi.BYUrTaMo.mjs';
2
+ export { G as GenerateOptions, g as generate, d as generateAsync } from './shared/nestjs-openapi.OhsaHu32.mjs';
@@ -1,2 +1,2 @@
1
1
  import 'effect';
2
- export { G as GenerateOptions, g as generate, d as generateAsync } from './shared/nestjs-openapi.BYUrTaMo.js';
2
+ export { G as GenerateOptions, g as generate, d as generateAsync } from './shared/nestjs-openapi.OhsaHu32.js';
package/dist/internal.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Effect } from 'effect';
2
2
  import { Project } from 'ts-morph';
3
- import { E as EntryNotFoundError, g as getModules, o as getControllerMethodInfos, p as transformMethods } from './shared/nestjs-openapi.B1bBy_tG.mjs';
3
+ import { E as EntryNotFoundError, g as getModules, o as getControllerMethodInfos, p as transformMethods } from './shared/nestjs-openapi.CBj9xHZ4.mjs';
4
4
 
5
5
  const generate = (options) => Effect.gen(function* () {
6
6
  yield* Effect.logInfo("Starting OpenAPI generation").pipe(
@@ -850,7 +850,7 @@ const isExpandableType = (typeName) => {
850
850
  if (BUILT_IN_TYPES.has(typeName.split("<")[0])) return false;
851
851
  if (typeName.includes(" | ") || typeName.includes(" & ")) return false;
852
852
  if (typeName.endsWith("[]")) return false;
853
- return /^[A-Z][a-zA-Z0-9]*$/.test(typeName);
853
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(typeName);
854
854
  };
855
855
  const tsTypeToString = (typeText) => {
856
856
  const trimmed = typeText.trim();
@@ -1306,10 +1306,16 @@ const tsTypeToOpenApiSchema = (tsType) => {
1306
1306
  }
1307
1307
  }
1308
1308
  if (trimmed.includes(" | ")) {
1309
- const types = trimmed.split(" | ").map((t) => t.trim());
1310
- return {
1311
- oneOf: types.map((type) => tsTypeToOpenApiSchema(type))
1312
- };
1309
+ const allMembers = trimmed.split(" | ").map((t) => t.trim());
1310
+ const hasNull = allMembers.includes("null");
1311
+ const types = allMembers.filter((t) => t !== "undefined" && t !== "null");
1312
+ if (types.length === 0) return { type: "object" };
1313
+ const schema = types.length === 1 ? tsTypeToOpenApiSchema(types[0]) : { oneOf: types.map((type) => tsTypeToOpenApiSchema(type)) };
1314
+ if (!hasNull) return schema;
1315
+ if (schema.$ref) {
1316
+ return { allOf: [{ $ref: schema.$ref }], nullable: true };
1317
+ }
1318
+ return { ...schema, nullable: true };
1313
1319
  }
1314
1320
  switch (trimmed.toLowerCase()) {
1315
1321
  case "string":
@@ -1327,6 +1333,7 @@ const tsTypeToOpenApiSchema = (tsType) => {
1327
1333
  return { type: "object" };
1328
1334
  case "unknown":
1329
1335
  case "any":
1336
+ case "object":
1330
1337
  return { type: "object" };
1331
1338
  }
1332
1339
  if (trimmed === "StreamableFile" || trimmed === "Buffer" || trimmed === "Readable" || trimmed === "ReadableStream") {
@@ -1345,7 +1352,7 @@ const tsTypeToOpenApiSchema = (tsType) => {
1345
1352
  type: "object"
1346
1353
  };
1347
1354
  }
1348
- if (trimmed.match(/^[A-Z][a-zA-Z0-9]*(<[^>]+>)?$/)) {
1355
+ if (trimmed.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*(<[^>]+>)?$/)) {
1349
1356
  return { $ref: `#/components/schemas/${trimmed}` };
1350
1357
  }
1351
1358
  return { type: "object" };
@@ -173,9 +173,12 @@ interface OpenApiSchema {
173
173
  readonly format?: string;
174
174
  readonly $ref?: string;
175
175
  readonly oneOf?: readonly OpenApiSchema[];
176
+ readonly allOf?: readonly OpenApiSchema[];
176
177
  readonly items?: OpenApiSchema;
177
178
  readonly properties?: Record<string, OpenApiSchema>;
178
179
  readonly required?: readonly string[];
180
+ /** OpenAPI 3.0 nullable flag */
181
+ readonly nullable?: boolean;
179
182
  }
180
183
  declare const OpenApiOperation: Schema.Struct<{
181
184
  operationId: typeof Schema.String;
@@ -173,9 +173,12 @@ interface OpenApiSchema {
173
173
  readonly format?: string;
174
174
  readonly $ref?: string;
175
175
  readonly oneOf?: readonly OpenApiSchema[];
176
+ readonly allOf?: readonly OpenApiSchema[];
176
177
  readonly items?: OpenApiSchema;
177
178
  readonly properties?: Record<string, OpenApiSchema>;
178
179
  readonly required?: readonly string[];
180
+ /** OpenAPI 3.0 nullable flag */
181
+ readonly nullable?: boolean;
179
182
  }
180
183
  declare const OpenApiOperation: Schema.Struct<{
181
184
  operationId: typeof Schema.String;
@@ -4,7 +4,7 @@ import { join, dirname, resolve } from 'node:path';
4
4
  import { Project } from 'ts-morph';
5
5
  import { globSync, glob } from 'glob';
6
6
  import yaml from 'js-yaml';
7
- import { C as ConfigNotFoundError, a as ConfigLoadError, b as ConfigValidationError, p as transformMethods, A as extractClassConstraints, B as getRequiredProperties, D as mergeValidationConstraints, E as EntryNotFoundError, g as getModules, o as getControllerMethodInfos } from './nestjs-openapi.B1bBy_tG.mjs';
7
+ import { C as ConfigNotFoundError, a as ConfigLoadError, b as ConfigValidationError, p as transformMethods, A as extractClassConstraints, B as getRequiredProperties, D as mergeValidationConstraints, E as EntryNotFoundError, g as getModules, o as getControllerMethodInfos } from './nestjs-openapi.CBj9xHZ4.mjs';
8
8
  import { createGenerator } from 'ts-json-schema-generator';
9
9
  import { randomUUID } from 'node:crypto';
10
10
  import { pathToFileURL } from 'node:url';
@@ -466,6 +466,12 @@ const extractReferencedSchemas = (paths) => {
466
466
  if (schema.oneOf) {
467
467
  schema.oneOf.forEach(extractFromSchema);
468
468
  }
469
+ if (schema.allOf) {
470
+ schema.allOf.forEach(extractFromSchema);
471
+ }
472
+ if (schema.anyOf) {
473
+ schema.anyOf.forEach(extractFromSchema);
474
+ }
469
475
  if (schema.properties) {
470
476
  Object.values(schema.properties).forEach(extractFromSchema);
471
477
  }
@@ -529,7 +535,14 @@ const extractNestedReferences = (schemas, knownSchemas) => {
529
535
  };
530
536
  const convertToOpenApiSchema = (schema) => {
531
537
  const result = {};
532
- if (schema.type) result["type"] = schema.type;
538
+ if (Array.isArray(schema.type)) {
539
+ const nonNull = schema.type.filter((t) => t !== "null");
540
+ const isNullable = nonNull.length < schema.type.length;
541
+ result["type"] = nonNull.length === 1 ? nonNull[0] : schema.type;
542
+ if (isNullable && nonNull.length === 1) result["nullable"] = true;
543
+ } else if (schema.type) {
544
+ result["type"] = schema.type;
545
+ }
533
546
  if (schema.format) result["format"] = schema.format;
534
547
  if (schema.$ref) {
535
548
  result["$ref"] = schema.$ref.replace(
@@ -700,14 +713,81 @@ const transformSchemasForVersion = (schemas, version) => {
700
713
  ])
701
714
  );
702
715
  };
716
+ const transformOperationToV31 = (operation) => {
717
+ const parameters = operation.parameters?.map((param) => ({
718
+ ...param,
719
+ schema: transformSchemaToV31(param.schema)
720
+ }));
721
+ const requestBody = operation.requestBody ? {
722
+ ...operation.requestBody,
723
+ content: Object.fromEntries(
724
+ Object.entries(operation.requestBody.content).map(
725
+ ([contentType, mediaType]) => [
726
+ contentType,
727
+ { ...mediaType, schema: transformSchemaToV31(mediaType.schema) }
728
+ ]
729
+ )
730
+ )
731
+ } : void 0;
732
+ const responses = Object.fromEntries(
733
+ Object.entries(operation.responses).map(([code, response]) => [
734
+ code,
735
+ response.content ? {
736
+ ...response,
737
+ content: Object.fromEntries(
738
+ Object.entries(response.content).map(
739
+ ([contentType, mediaType]) => [
740
+ contentType,
741
+ {
742
+ ...mediaType,
743
+ schema: transformSchemaToV31(mediaType.schema)
744
+ }
745
+ ]
746
+ )
747
+ )
748
+ } : response
749
+ ])
750
+ );
751
+ return {
752
+ ...operation,
753
+ ...parameters && { parameters },
754
+ ...requestBody && { requestBody },
755
+ responses
756
+ };
757
+ };
758
+ const HTTP_METHODS = /* @__PURE__ */ new Set([
759
+ "get",
760
+ "put",
761
+ "post",
762
+ "delete",
763
+ "options",
764
+ "head",
765
+ "patch",
766
+ "trace"
767
+ ]);
768
+ const transformPathsForVersion = (paths, version) => {
769
+ if (version === "3.0.3") return paths;
770
+ return Object.fromEntries(
771
+ Object.entries(paths).map(([path, pathItem]) => [
772
+ path,
773
+ Object.fromEntries(
774
+ Object.entries(pathItem).map(
775
+ ([key, value]) => HTTP_METHODS.has(key) ? [key, transformOperationToV31(value)] : [key, value]
776
+ )
777
+ )
778
+ ])
779
+ );
780
+ };
703
781
  const transformSpecForVersion = (spec, version) => {
704
782
  if (version === "3.0.3") {
705
783
  return { ...spec, openapi: version };
706
784
  }
707
785
  const transformedSchemas = spec.components?.schemas ? transformSchemasForVersion(spec.components.schemas, version) : void 0;
786
+ const transformedPaths = transformPathsForVersion(spec.paths, version);
708
787
  return {
709
788
  ...spec,
710
789
  openapi: version,
790
+ paths: transformedPaths,
711
791
  ...transformedSchemas && {
712
792
  components: {
713
793
  ...spec.components,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nestjs-openapi",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Static code analysis tool to generate OpenAPI specifications from NestJS applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "module": "./dist/index.mjs",