@sdk-it/generic 0.46.0 → 0.46.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.
Files changed (2) hide show
  1. package/README.md +94 -163
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,31 +1,36 @@
1
1
  # @sdk-it/generic
2
2
 
3
- <p align="center">A TypeScript analysis tool for generating OpenAPI specifications from TypeScript code</p>
3
+ <p align="center">Analyze TypeScript routes and produce an OpenAPI document</p>
4
4
 
5
- Analyzes TypeScript code to generate OpenAPI specifications. Extracts route information, parameter types, and response schemas from your codebase.
5
+ `@sdk-it/generic` extracts routes, validation schemas, and responses from a
6
+ TypeScript project. Framework packages provide the runtime middleware and
7
+ response analyzer.
6
8
 
7
- ## Frameworks specific integrations
9
+ ## Framework integrations
8
10
 
9
- - [hono](../hono/README.md)
11
+ - [Hono](../hono/README.md)
10
12
 
11
13
  ## Installation
12
14
 
15
+ For a Hono project:
16
+
13
17
  ```bash
14
- npm install @sdk-it/generic
18
+ npm install @sdk-it/hono hono zod
19
+ npm install --save-dev @sdk-it/core @sdk-it/generic typescript@^6.0.3
15
20
  ```
16
21
 
17
- ## Usage
18
-
19
- Consider the following example:
22
+ ## Analyze a Hono project
20
23
 
21
- - Create a route using your API framework of choice with the `@openapi` tag and validate middleware.
24
+ Define routes with `validate` and an `@openapi` JSDoc tag:
22
25
 
23
26
  ```typescript
24
- import z from 'zod';
27
+ // src/app.ts
28
+ import { Hono } from 'hono';
29
+ import { z } from 'zod';
25
30
 
26
- import { validate } from '@sdk-it/express/runtime';
31
+ import { validate } from '@sdk-it/hono/runtime';
27
32
 
28
- const app = express();
33
+ export const app = new Hono();
29
34
 
30
35
  /**
31
36
  * @openapi getAuthor
@@ -35,161 +40,109 @@ app.get(
35
40
  '/authors/:id',
36
41
  validate((payload) => ({
37
42
  id: {
38
- select: payload.param.id,
43
+ select: payload.params.id,
39
44
  against: z.string(),
40
45
  },
41
46
  })),
42
- async (req, res) => {
43
- const author = [{ name: 'John Doe' }];
44
- return res.json(author);
47
+ (c) => {
48
+ const { id } = c.var.input;
49
+ return c.json({ id, name: 'John Doe' });
45
50
  },
46
51
  );
47
52
  ```
48
53
 
49
- - Use the generate fn to create an OpenAPI spec from your routes.
54
+ Analyze the TypeScript project and write the resulting OpenAPI document:
50
55
 
51
56
  ```typescript
52
- import { join } from 'node:path';
57
+ // openapi.ts
58
+ import { writeFile } from 'node:fs/promises';
53
59
 
54
- import { analyze, responseAnalyzer } from '@sdk-it/generic';
55
- import { generate } from '@sdk-it/typescript';
60
+ import { analyze } from '@sdk-it/generic';
61
+ import { responseAnalyzer } from '@sdk-it/hono';
56
62
 
57
- const { paths, components } = await analyze('path/to/tsconfig.json', {
63
+ const { paths, components, tags } = await analyze('./tsconfig.json', {
58
64
  responseAnalyzer,
59
65
  });
60
66
 
61
67
  const spec = {
68
+ openapi: '3.1.0',
62
69
  info: {
63
70
  title: 'My API',
64
71
  version: '1.0.0',
65
72
  },
66
73
  paths,
67
74
  components,
75
+ tags: tags.map((name) => ({ name })),
68
76
  };
69
77
 
70
- await writeFile(
71
- join(process.cwd(), 'openapi.json'),
72
- JSON.stringify(spec, null, 2),
73
- );
78
+ await writeFile('openapi.json', JSON.stringify(spec, null, 2));
74
79
  ```
75
80
 
76
- > [!TIP]
77
- > See [typescript](../typescript/README.md) to create fully functional SDKs from the generated OpenAPI specification.
81
+ Run the script with Node.js 24 or newer:
78
82
 
79
- ### Customizing Operations
80
-
81
- You can customize the operations as well as add more through the `onOperation` fn.
82
-
83
- **Use file name as tag**
83
+ ```bash
84
+ node openapi.ts
85
+ ```
84
86
 
85
- Assuming your project is structured like the following, where routes are grouped by representative file names:
87
+ See [`@sdk-it/typescript`](../typescript/README.md) to generate a client from
88
+ the OpenAPI document.
86
89
 
87
- ```
88
- apps/
89
- backend/
90
- tsconfig.app.json
91
- src/
92
- routes/
93
- authors.ts
94
- ```
90
+ ## Customize operations
95
91
 
96
- The file name becomes the default tag for each operation. Specify a tag in JSDoc only to override this default.
92
+ `onOperation` receives every derived operation. This example uses the route
93
+ file name as its tag:
97
94
 
98
95
  ```typescript
99
96
  import { basename } from 'node:path';
100
- import { camelcase } from 'stringcase';
101
97
 
102
- import { analyze, responseAnalyzer } from '@sdk-it/generic';
98
+ import { analyze } from '@sdk-it/generic';
99
+ import { responseAnalyzer } from '@sdk-it/hono';
103
100
 
104
- const { paths, components } = await analyze('apps/backend/tsconfig.app.json', {
105
- responseAnalyzer,
106
- onOperation(sourceFile, method, path, operation) {
107
- const fileName = basename(sourceFile.split('/').at(-1), '.ts');
108
- return {
109
- [method]: {
110
- [path]: {
111
- ...operation,
112
- tags: [fileName],
113
- },
114
- },
115
- };
101
+ const { paths, components } = await analyze(
102
+ './apps/backend/tsconfig.app.json',
103
+ {
104
+ responseAnalyzer,
105
+ onOperation(sourceFile, _method, _path, operation) {
106
+ operation.tags = [basename(sourceFile, '.ts')];
107
+ return {};
108
+ },
116
109
  },
117
- });
110
+ );
118
111
  ```
119
112
 
120
- ### Customizing Type Mappings
113
+ ## Customize type mappings
121
114
 
122
- Custom type mappings handle non-standard TypeScript types like Prisma's `Decimal`. Use the `typesMap` option in `analyze` to define your mappings.
123
-
124
- The example below maps `Decimal` to `string`.
115
+ Use `typesMap` for TypeScript types without a direct OpenAPI representation.
116
+ For example, map Prisma's `Decimal` to a string so it keeps its precision on
117
+ the wire:
125
118
 
126
119
  ```typescript
127
- import { writeFile } from 'node:fs/promises';
128
- import { join } from 'node:path';
129
- import { cwd } from 'node:process';
130
-
131
120
  import { defaultTypesMap } from '@sdk-it/core';
132
121
  import { analyze } from '@sdk-it/generic';
133
122
  import { responseAnalyzer } from '@sdk-it/hono';
134
123
 
135
- // Define custom type mappings
136
- const customTypeMappings = {
137
- Decimal: 'string',
138
- };
139
-
140
- const { paths, components } = await analyze('path/to/tsconfig.json', {
124
+ const { paths, components } = await analyze('./tsconfig.json', {
141
125
  responseAnalyzer,
142
126
  typesMap: {
143
127
  ...defaultTypesMap,
144
128
  Decimal: 'string',
145
129
  },
146
130
  });
147
-
148
- const spec = {
149
- openapi: '3.1.0',
150
- info: {
151
- title: 'My API',
152
- version: '1.0.0',
153
- },
154
- paths,
155
- components,
156
- };
157
-
158
- await writeFile('openapi.json', JSON.stringify(spec, null, 2));
159
131
  ```
160
132
 
161
- With this, the generator maps `Decimal` properties to `string` in the OpenAPI specification.
133
+ ## Reference external schemas
162
134
 
163
- ### Referencing external schemas
164
-
165
- By default, the analyzer only sees schemas defined inline in the validate middleware.
166
-
167
- For instance the following route handler is perfectly valid and will be analyzed correctly.
135
+ The analyzer can evaluate inline Zod schemas directly:
168
136
 
169
137
  ```typescript
170
- /**
171
- * @openapi getAuthor
172
- * @tags authors
173
- */
174
- app.get(
175
- '/authors/:id',
176
- validate((payload) => ({
177
- id: {
178
- select: payload.param.id,
179
- against: z.string(),
180
- },
181
- })),
182
- async (req, res) => {
183
- const { id } = req.input;
184
- return res.json({ id, name: 'John Doe' });
185
- },
186
- );
138
+ against: z.string().min(2).max(100);
187
139
  ```
188
140
 
189
- However, if you want to reference external schemas as shown below, you need to provide a way for the analyzer to resolve the schema.
141
+ When a validator references a schema from another file, use a namespace import
142
+ in the route:
190
143
 
191
- ```ts
192
- // filename: schemas.ts
144
+ ```typescript
145
+ // src/schemas.ts
193
146
  import { z } from 'zod';
194
147
 
195
148
  export const authorSchema = z.object({
@@ -198,88 +151,62 @@ export const authorSchema = z.object({
198
151
  });
199
152
  ```
200
153
 
201
- ```ts
154
+ ```typescript
155
+ // src/app.ts
156
+ import { Hono } from 'hono';
202
157
  import crypto from 'node:crypto';
203
158
  import { z } from 'zod';
204
159
 
205
- import { validate } from '@sdk-it/express/runtime';
160
+ import { validate } from '@sdk-it/hono/runtime';
206
161
 
207
- import { authorSchema } from './schemas';
162
+ import * as schemas from './schemas.ts';
163
+
164
+ const app = new Hono();
208
165
 
209
- /**
210
- * @openapi createBook
211
- * @tags books
212
- */
213
166
  app.post(
214
167
  '/books',
215
- validate((payload) => ({
168
+ validate('application/json', (payload) => ({
216
169
  title: {
217
170
  select: payload.body.title,
218
171
  against: z.string().min(2).max(100),
219
172
  },
220
173
  author: {
221
174
  select: payload.body.author,
222
- against: authorSchema, // <-- Referencing external schema
175
+ against: schemas.authorSchema,
223
176
  },
224
177
  })),
225
- async (req, res) => {
226
- const { title, author } = req.input;
227
- return res.json({ id: crypto.randomUUID(), title, author });
178
+ (c) => {
179
+ const { title, author } = c.var.input;
180
+ return c.json({ id: crypto.randomUUID(), title, author }, 201);
228
181
  },
229
182
  );
230
183
  ```
231
184
 
232
- The analyzer must resolve the `authorSchema` reference to generate the correct OpenAPI schema. Otherwise it will fail.
233
-
234
- The analyzer's `imports` option lets you include additional files in the analysis.
185
+ Then inject that namespace when analyzing the project:
235
186
 
236
- ```ts
237
- import { join } from 'node:path';
187
+ ```typescript
188
+ import { fileURLToPath } from 'node:url';
238
189
 
239
190
  import { analyze } from '@sdk-it/generic';
191
+ import { responseAnalyzer } from '@sdk-it/hono';
240
192
 
241
- const { paths, components } = await analyze('path/to/tsconfig.json', {
193
+ const { paths, components } = await analyze('./tsconfig.json', {
242
194
  responseAnalyzer,
243
195
  imports: [
244
196
  {
245
197
  import: 'schemas',
246
- from: join(process.cwd(), 'path/to/schemas.ts'), // <-- Path to the file containing the external schema
198
+ from: fileURLToPath(new URL('./src/schemas.ts', import.meta.url)),
247
199
  },
248
200
  ],
249
201
  });
250
202
  ```
251
203
 
252
- Now you need to update the import to namespace imports in the route handler where the `schemas` variable is used.
253
-
254
- ```ts
255
- import * as schemas from './schemas';
256
-
257
- /**
258
- * @openapi createBook
259
- * @tags books
260
- */
261
- app.post(
262
- '/books',
263
- validate((payload) => ({
264
- title: {
265
- select: payload.body.title,
266
- against: z.string().min(2).max(100),
267
- },
268
- author: {
269
- select: payload.body.author,
270
- against: schemas.authorSchema,
271
- },
272
- })),
273
- async (req, res) => {
274
- const { title, author } = req.input;
275
- return res.json({ id: crypto.randomUUID(), title, author });
276
- },
277
- );
278
- ```
204
+ The injected file must be loadable by the Node.js process running the
205
+ analyzer.
279
206
 
280
- ### Control endpoint/operation visibility
207
+ ## Hide an operation
281
208
 
282
- Control endpoint visibility with the `@access` tag in JSDoc comments. For now, only `private` is supported.
209
+ Add `@access private` to exclude a route from the generated OpenAPI document:
283
210
 
284
211
  ```typescript
285
212
  /**
@@ -287,10 +214,14 @@ Control endpoint visibility with the `@access` tag in JSDoc comments. For now, o
287
214
  * @tags authors
288
215
  * @access private
289
216
  */
290
- app.get('/authors/:id', async (req, res) => {
291
- const author = [{ name: 'John Doe' }];
292
- return res.json(author);
293
- });
217
+ app.get(
218
+ '/authors/:id',
219
+ validate((payload) => ({
220
+ id: {
221
+ select: payload.params.id,
222
+ against: z.string(),
223
+ },
224
+ })),
225
+ (c) => c.json({ id: c.var.input.id, name: 'John Doe' }),
226
+ );
294
227
  ```
295
-
296
- In this example, the `getAuthor` operation will be hidden from the generated OpenAPI specification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdk-it/generic",
3
- "version": "0.46.0",
3
+ "version": "0.46.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "!**/*.test.*"
23
23
  ],
24
24
  "dependencies": {
25
- "@sdk-it/core": "0.46.0",
25
+ "@sdk-it/core": "0.46.2",
26
26
  "stringcase": "^4.3.1",
27
27
  "debug": "^4.4.0",
28
28
  "openapi3-ts": "^4.4.0"