@constantant/openapi-resource-gen 1.1.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.
- package/README.md +230 -0
- package/generators.json +9 -0
- package/package.json +34 -0
- package/src/generators/api-resource/endpoint-model.d.ts +14 -0
- package/src/generators/api-resource/endpoint-model.js +3 -0
- package/src/generators/api-resource/endpoint-model.js.map +1 -0
- package/src/generators/api-resource/files/api-base-url.token.ts__tmpl__ +6 -0
- package/src/generators/api-resource/generator.d.ts +11 -0
- package/src/generators/api-resource/generator.js +143 -0
- package/src/generators/api-resource/generator.js.map +1 -0
- package/src/generators/api-resource/parse-spec.d.ts +7 -0
- package/src/generators/api-resource/parse-spec.js +107 -0
- package/src/generators/api-resource/parse-spec.js.map +1 -0
- package/src/generators/api-resource/render-token.d.ts +2 -0
- package/src/generators/api-resource/render-token.js +80 -0
- package/src/generators/api-resource/render-token.js.map +1 -0
- package/src/generators/api-resource/schema.json +40 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +8 -0
- package/src/index.js.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# openapi-resource-gen
|
|
2
|
+
|
|
3
|
+
Nx generator that reads an OpenAPI 3.x spec and emits one `InjectionToken` per
|
|
4
|
+
endpoint, each in its own `.ts` file. The result is a tree-shakeable Angular
|
|
5
|
+
data-access library: only tokens that are actually injected end up in the bundle.
|
|
6
|
+
|
|
7
|
+
## Concept
|
|
8
|
+
|
|
9
|
+
Each endpoint becomes a typed `InjectionToken` whose value is a factory function.
|
|
10
|
+
Calling the token's factory function returns an `httpResource` — Angular 22's
|
|
11
|
+
signal-native HTTP wrapper.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
OpenAPI spec → generator → one .token.ts per endpoint
|
|
15
|
+
└─ InjectionToken + typed factory function
|
|
16
|
+
└─ export type Params / Body / Response
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
All types are derived from the generated `schema.d.ts` (via `openapi-typescript`)
|
|
20
|
+
so there is zero hand-written interface code and no runtime overhead from a
|
|
21
|
+
generated client library.
|
|
22
|
+
|
|
23
|
+
### Why one file per endpoint?
|
|
24
|
+
|
|
25
|
+
esbuild performs tree-shaking at the file boundary. A token that is never
|
|
26
|
+
`inject()`-ed is never imported, so the entire file is dropped from the bundle.
|
|
27
|
+
Bundling all endpoints into a single file would prevent this.
|
|
28
|
+
|
|
29
|
+
### `providedIn: 'none'` vs `'root'`
|
|
30
|
+
|
|
31
|
+
| Mode | Token has factory? | How to provide |
|
|
32
|
+
|------|--------------------|----------------|
|
|
33
|
+
| `'none'` (default) | No | Call the emitted `provide{Name}()` helper in `app.config.ts` or a route provider |
|
|
34
|
+
| `'root'` | Yes — self-registers | Just `inject()` it anywhere; Angular handles registration |
|
|
35
|
+
|
|
36
|
+
`'none'` is the default because it lets you inject the same token with different
|
|
37
|
+
`API_BASE_URL` values in different route sub-trees (e.g. staging vs production,
|
|
38
|
+
or different micro-frontends). `'root'` is simpler but prevents per-scope
|
|
39
|
+
base URL overrides.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Running the generator
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx nx g openapi-resource-gen:api-resource \
|
|
47
|
+
--specPath=specs/petstore.yaml \
|
|
48
|
+
--outputDir=libs/petstore-data-access/src
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Options
|
|
52
|
+
|
|
53
|
+
| Option | Required | Default | Description |
|
|
54
|
+
|--------|----------|---------|-------------|
|
|
55
|
+
| `specPath` | yes | — | Path to the OpenAPI 3.x YAML or JSON spec file |
|
|
56
|
+
| `outputDir` | yes | — | Output directory relative to the workspace root |
|
|
57
|
+
| `baseUrlToken` | no | `API_BASE_URL` | Name of the base-URL `InjectionToken` emitted alongside the endpoint tokens |
|
|
58
|
+
| `tagFilter` | no | all tags | Comma-separated list of OpenAPI tags to include |
|
|
59
|
+
| `namingConvention` | no | `kebab` | `kebab` → `find-pets-by-status.token.ts`; `camel` → `findPetsByStatus.token.ts` |
|
|
60
|
+
| `providedIn` | no | `none` | `none` or `root` — see table above |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Output structure
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
{outputDir}/
|
|
68
|
+
schema.d.ts # openapi-typescript output, never edit manually
|
|
69
|
+
api-base-url.token.ts # InjectionToken<string> for the API root URL
|
|
70
|
+
index.ts # re-exports all tag barrels + api-base-url.token
|
|
71
|
+
{tag}/
|
|
72
|
+
index.ts # re-exports all token files in this tag folder
|
|
73
|
+
{operation-id}.token.ts # one file per endpoint
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Tags map to subfolders; untagged operations go into `default/`.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Generated token anatomy
|
|
81
|
+
|
|
82
|
+
### GET with query params — `find-pets-by-status.token.ts`
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { InjectionToken, inject, FactoryProvider } from '@angular/core';
|
|
86
|
+
import { httpResource } from '@angular/common/http';
|
|
87
|
+
import type { paths } from '../schema.d';
|
|
88
|
+
import { PETSTORE_BASE_URL } from '../api-base-url.token';
|
|
89
|
+
|
|
90
|
+
export type FindPetsByStatusParams =
|
|
91
|
+
paths['/pet/findByStatus']['get']['parameters']['query'];
|
|
92
|
+
|
|
93
|
+
export type FindPetsByStatusResponse =
|
|
94
|
+
paths['/pet/findByStatus']['get']['responses']['200']['content']['application/json'];
|
|
95
|
+
|
|
96
|
+
export const FIND_PETS_BY_STATUS = new InjectionToken<
|
|
97
|
+
(params?: FindPetsByStatusParams | (() => FindPetsByStatusParams | undefined))
|
|
98
|
+
=> ReturnType<typeof httpResource<FindPetsByStatusResponse>>
|
|
99
|
+
>('FIND_PETS_BY_STATUS');
|
|
100
|
+
|
|
101
|
+
export function provideFindPetsByStatus(): FactoryProvider {
|
|
102
|
+
return {
|
|
103
|
+
provide: FIND_PETS_BY_STATUS,
|
|
104
|
+
useFactory: () => {
|
|
105
|
+
const base = inject(PETSTORE_BASE_URL);
|
|
106
|
+
return (params?) =>
|
|
107
|
+
httpResource<FindPetsByStatusResponse>(() => ({
|
|
108
|
+
url: `${base}/pet/findByStatus`,
|
|
109
|
+
params: (typeof params === 'function' ? params() : params) as ...,
|
|
110
|
+
}));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### GET with path params — `repos-get.token.ts`
|
|
117
|
+
|
|
118
|
+
Path params (`/repos/{owner}/{repo}`) become required positional arguments on
|
|
119
|
+
the returned function and are interpolated into the URL template:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
export const REPOS_GET = new InjectionToken<
|
|
123
|
+
(owner: string, repo: string) => ReturnType<typeof httpResource<ReposGetResponse>>
|
|
124
|
+
>('REPOS_GET');
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Mutation (POST/PUT/PATCH/DELETE)
|
|
128
|
+
|
|
129
|
+
The factory returns `(body: BodyType | Signal<BodyType>) => httpResource(...)`.
|
|
130
|
+
The resource config receives `method: 'POST'` and `body` automatically.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Consuming tokens in a component
|
|
135
|
+
|
|
136
|
+
### 1. Register providers in `app.config.ts`
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import { PETSTORE_BASE_URL, provideFindPetsByStatus } from '@angular-openapi-gen/petstore-data-access';
|
|
140
|
+
|
|
141
|
+
export const appConfig: ApplicationConfig = {
|
|
142
|
+
providers: [
|
|
143
|
+
provideHttpClient(),
|
|
144
|
+
{ provide: PETSTORE_BASE_URL, useValue: 'https://petstore3.swagger.io/api/v3' },
|
|
145
|
+
provideFindPetsByStatus(),
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2. Inject and call in a component
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
@Component({ ... })
|
|
154
|
+
export class PetsPageComponent {
|
|
155
|
+
private findPetsByStatus = inject(FIND_PETS_BY_STATUS);
|
|
156
|
+
|
|
157
|
+
readonly status = signal<'available' | 'pending' | 'sold'>('available');
|
|
158
|
+
|
|
159
|
+
// Pass a thunk so httpResource re-fetches whenever status() changes
|
|
160
|
+
readonly pets = this.findPetsByStatus(() => ({ status: this.status() }));
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
@if (pets.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
166
|
+
@for (pet of pets.value() ?? []; track pet.id) {
|
|
167
|
+
<p>{{ pet.name }}</p>
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Reactive params (signal-driven re-fetch)
|
|
172
|
+
|
|
173
|
+
The `params` argument accepts either a plain object or a **thunk**
|
|
174
|
+
`() => ParamsType`. When a thunk is passed it is called inside the
|
|
175
|
+
`httpResource` reactive lambda, so any signal reads inside it register as
|
|
176
|
+
dependencies — the request re-fires automatically when a signal changes.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Exported types
|
|
181
|
+
|
|
182
|
+
Every token file exports `Params`, `Body`, and `Response` type aliases so
|
|
183
|
+
consumers can derive domain types without duplicating type expressions:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import type { FindPetsByStatusParams } from '@angular-openapi-gen/petstore-data-access';
|
|
187
|
+
|
|
188
|
+
type PetStatus = FindPetsByStatusParams['status']; // 'available' | 'pending' | 'sold'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Adding a new data-access lib
|
|
194
|
+
|
|
195
|
+
1. Fetch the spec into `specs/`:
|
|
196
|
+
```bash
|
|
197
|
+
curl -L https://example.com/openapi.yaml -o specs/myapi.yaml
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
2. Run the generator:
|
|
201
|
+
```bash
|
|
202
|
+
npx nx g openapi-resource-gen:api-resource \
|
|
203
|
+
--specPath=specs/myapi.yaml \
|
|
204
|
+
--outputDir=libs/myapi-data-access/src
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
3. Create `libs/myapi-data-access/package.json` (or add a path alias to
|
|
208
|
+
`tsconfig.base.json`) so the lib is importable as
|
|
209
|
+
`@angular-openapi-gen/myapi-data-access`.
|
|
210
|
+
|
|
211
|
+
4. Add base URL provider and token providers to `app.config.ts`.
|
|
212
|
+
|
|
213
|
+
To regenerate after a spec update, re-run the same command — the generator
|
|
214
|
+
overwrites all files in `outputDir`.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Implementation notes
|
|
219
|
+
|
|
220
|
+
| Step | Tool | Purpose |
|
|
221
|
+
|------|------|---------|
|
|
222
|
+
| Spec loading | `js-yaml` + custom `stripNonSchemaRefs()` | Handle YAML and remove non-spec `$ref` links (markdown, images) that would break parsing |
|
|
223
|
+
| Type generation | `openapi-typescript` CLI | Emit `schema.d.ts` — the single source of truth for all request/response types |
|
|
224
|
+
| Spec dereferencing | `@apidevtools/swagger-parser` | Resolve all `$ref` chains for endpoint extraction |
|
|
225
|
+
| Code generation | `@nx/devkit` `generateFiles()` + inline string building | EJS template for `api-base-url.token.ts`; direct string assembly for token files |
|
|
226
|
+
| Formatting | `@nx/devkit` `formatFiles()` | Runs Prettier over all written files |
|
|
227
|
+
|
|
228
|
+
Hyphenated path parameter names (e.g. `{enterprise-team}` in the GitHub spec)
|
|
229
|
+
are converted to camelCase in the function signature and the URL template via
|
|
230
|
+
`toCamelCase()` to produce valid JavaScript identifiers.
|
package/generators.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@constantant/openapi-resource-gen",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Nx generator: one InjectionToken per OpenAPI endpoint — tree-shakeable Angular data-access libs via httpResource",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"generators": "./generators.json",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"nx",
|
|
11
|
+
"nx-plugin",
|
|
12
|
+
"angular",
|
|
13
|
+
"openapi",
|
|
14
|
+
"generator",
|
|
15
|
+
"httpResource",
|
|
16
|
+
"InjectionToken",
|
|
17
|
+
"tree-shaking"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@nx/devkit": ">=22.0.0",
|
|
24
|
+
"nx": ">=22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@apidevtools/swagger-parser": "^12.1.0",
|
|
28
|
+
"js-yaml": "^4.2.0",
|
|
29
|
+
"openapi-typescript": "^6.7.6",
|
|
30
|
+
"openapi-types": "^12.1.3",
|
|
31
|
+
"tslib": "^2.3.0"
|
|
32
|
+
},
|
|
33
|
+
"type": "commonjs"
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EndpointModel {
|
|
2
|
+
tag: string;
|
|
3
|
+
operationId: string;
|
|
4
|
+
method: string;
|
|
5
|
+
apiPath: string;
|
|
6
|
+
pathParams: string[];
|
|
7
|
+
tokenName: string;
|
|
8
|
+
fileName: string;
|
|
9
|
+
hasQueryParams: boolean;
|
|
10
|
+
hasBody: boolean;
|
|
11
|
+
hasResponse: boolean;
|
|
12
|
+
responseStatus: string | null;
|
|
13
|
+
bodyContentType: string | null;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoint-model.js","sourceRoot":"","sources":["../../../../../../tools/openapi-resource-gen/src/generators/api-resource/endpoint-model.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit';
|
|
2
|
+
export interface ApiResourceGeneratorSchema {
|
|
3
|
+
specPath: string;
|
|
4
|
+
outputDir: string;
|
|
5
|
+
baseUrlToken?: string;
|
|
6
|
+
tagFilter?: string;
|
|
7
|
+
namingConvention?: 'camel' | 'kebab';
|
|
8
|
+
providedIn?: 'root' | 'none';
|
|
9
|
+
}
|
|
10
|
+
export declare function apiResourceGenerator(tree: Tree, options: ApiResourceGeneratorSchema): Promise<void>;
|
|
11
|
+
export default apiResourceGenerator;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.apiResourceGenerator = apiResourceGenerator;
|
|
40
|
+
const devkit_1 = require("@nx/devkit");
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const jsYaml = __importStar(require("js-yaml"));
|
|
43
|
+
// openapi-typescript ships as ESM-only; use the bundled CJS build so this
|
|
44
|
+
// CommonJS generator can call it without a dynamic import().
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
const openapiTS = require('openapi-typescript/dist/index.cjs');
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
|
|
49
|
+
const parse_spec_1 = require("./parse-spec");
|
|
50
|
+
const render_token_1 = require("./render-token");
|
|
51
|
+
/** Replace $ref values that point to non-YAML/JSON files with {} so
|
|
52
|
+
* swagger-parser doesn't try to load markdown or other binary assets. */
|
|
53
|
+
function stripNonSchemaRefs(obj) {
|
|
54
|
+
if (Array.isArray(obj))
|
|
55
|
+
return obj.map(stripNonSchemaRefs);
|
|
56
|
+
if (obj && typeof obj === 'object') {
|
|
57
|
+
const record = obj;
|
|
58
|
+
if ('$ref' in record && typeof record['$ref'] === 'string') {
|
|
59
|
+
const ref = record['$ref'];
|
|
60
|
+
if (!ref.startsWith('#') && !/\.(json|ya?ml)(#.*)?$/i.test(ref)) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const result = {};
|
|
65
|
+
for (const [key, value] of Object.entries(record)) {
|
|
66
|
+
result[key] = stripNonSchemaRefs(value);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
return obj;
|
|
71
|
+
}
|
|
72
|
+
async function apiResourceGenerator(tree, options) {
|
|
73
|
+
const { specPath, outputDir, baseUrlToken = 'API_BASE_URL', tagFilter, namingConvention = 'kebab', providedIn = 'none', } = options;
|
|
74
|
+
const allowedTags = tagFilter
|
|
75
|
+
? tagFilter
|
|
76
|
+
.split(',')
|
|
77
|
+
.map((t) => t.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
: null;
|
|
80
|
+
// 1. Parse spec with js-yaml, strip any $refs pointing to non-spec files
|
|
81
|
+
// (e.g. x-topics.$ref: ./docs/getting-started.md in the travel spec).
|
|
82
|
+
// Write the cleaned spec next to the original so relative file $refs
|
|
83
|
+
// within the spec still resolve when swagger-parser dereferences.
|
|
84
|
+
const absoluteSpecPath = path.resolve(specPath);
|
|
85
|
+
const rawParsed = jsYaml.load(fs.readFileSync(absoluteSpecPath, 'utf-8'));
|
|
86
|
+
const cleanedParsed = stripNonSchemaRefs(rawParsed);
|
|
87
|
+
const tmpClean = path
|
|
88
|
+
.join(path.dirname(absoluteSpecPath), `_tmp_oas_${Date.now()}.json`)
|
|
89
|
+
.replace(/\\/g, '/');
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(tmpClean, JSON.stringify(cleanedParsed));
|
|
92
|
+
// 2. Emit schema.d.ts via openapi-typescript programmatic API, using the
|
|
93
|
+
// cleaned spec (not the dereferenced result — dereferenced Stripe has
|
|
94
|
+
// circular refs; openapi-typescript resolves $refs itself).
|
|
95
|
+
const schemaDts = await openapiTS(tmpClean);
|
|
96
|
+
tree.write((0, devkit_1.joinPathFragments)(outputDir, 'schema.d.ts'), schemaDts);
|
|
97
|
+
// 3. Dereference for endpoint extraction (may produce circular objects —
|
|
98
|
+
// that's fine because we only iterate over it, never serialize it).
|
|
99
|
+
const api = (await swagger_parser_1.default.dereference(tmpClean));
|
|
100
|
+
// 4. Emit api-base-url.token.ts from the EJS template in files/
|
|
101
|
+
(0, devkit_1.generateFiles)(tree, path.join(__dirname, 'files'), outputDir, {
|
|
102
|
+
baseUrlToken,
|
|
103
|
+
tmpl: '',
|
|
104
|
+
});
|
|
105
|
+
// 5. Build EndpointModels from the already-dereferenced API
|
|
106
|
+
const endpoints = (0, parse_spec_1.buildEndpoints)(api, allowedTags, namingConvention);
|
|
107
|
+
// 6. Group by tag
|
|
108
|
+
const byTag = new Map();
|
|
109
|
+
for (const ep of endpoints) {
|
|
110
|
+
if (!byTag.has(ep.tag))
|
|
111
|
+
byTag.set(ep.tag, []);
|
|
112
|
+
byTag.get(ep.tag).push(ep);
|
|
113
|
+
}
|
|
114
|
+
// 7. One token file per endpoint + per-tag barrel index
|
|
115
|
+
for (const [tag, tagEndpoints] of byTag) {
|
|
116
|
+
const tagDir = (0, devkit_1.joinPathFragments)(outputDir, tag);
|
|
117
|
+
for (const ep of tagEndpoints) {
|
|
118
|
+
tree.write((0, devkit_1.joinPathFragments)(tagDir, `${ep.fileName}.token.ts`), (0, render_token_1.renderTokenFile)(ep, baseUrlToken, providedIn));
|
|
119
|
+
}
|
|
120
|
+
const tagBarrel = tagEndpoints
|
|
121
|
+
.map((ep) => `export * from './${ep.fileName}.token';`)
|
|
122
|
+
.join('\n') + '\n';
|
|
123
|
+
tree.write((0, devkit_1.joinPathFragments)(tagDir, 'index.ts'), tagBarrel);
|
|
124
|
+
}
|
|
125
|
+
// 8. Root barrel index (includes api-base-url token for app.config.ts providers)
|
|
126
|
+
const rootBarrel = `export * from './api-base-url.token';\n` +
|
|
127
|
+
[...byTag.keys()]
|
|
128
|
+
.map((tag) => `export * from './${tag}';`)
|
|
129
|
+
.join('\n') + '\n';
|
|
130
|
+
tree.write((0, devkit_1.joinPathFragments)(outputDir, 'index.ts'), rootBarrel);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
try {
|
|
134
|
+
fs.unlinkSync(tmpClean);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* ignore */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
141
|
+
}
|
|
142
|
+
exports.default = apiResourceGenerator;
|
|
143
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../../../../tools/openapi-resource-gen/src/generators/api-resource/generator.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,oDA8FC;AAhJD,uCAKoB;AACpB,uCAAyB;AACzB,gDAAkC;AAClC,0EAA0E;AAC1E,6DAA6D;AAC7D,iEAAiE;AACjE,MAAM,SAAS,GAAG,OAAO,CAAC,mCAAmC,CAEzC,CAAC;AACrB,2CAA6B;AAC7B,iFAAwD;AAExD,6CAA8C;AAC9C,iDAAiD;AAWjD;0EAC0E;AAC1E,SAAS,kBAAkB,CAAC,GAAY;IACtC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC3D,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,GAA8B,CAAC;QAC9C,IAAI,MAAM,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC3D,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3B,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChE,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAClD,MAAM,CAAC,GAAG,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAEM,KAAK,UAAU,oBAAoB,CACxC,IAAU,EACV,OAAmC;IAEnC,MAAM,EACJ,QAAQ,EACR,SAAS,EACT,YAAY,GAAG,cAAc,EAC7B,SAAS,EACT,gBAAgB,GAAG,OAAO,EAC1B,UAAU,GAAG,MAAM,GACpB,GAAG,OAAO,CAAC;IAEZ,MAAM,WAAW,GAAG,SAAS;QAC3B,CAAC,CAAC,SAAS;aACN,KAAK,CAAC,GAAG,CAAC;aACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACpB,MAAM,CAAC,OAAO,CAAC;QACpB,CAAC,CAAC,IAAI,CAAC;IAET,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,qEAAqE;IACrE,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1E,MAAM,aAAa,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,IAAI;SAClB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,YAAY,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC;SACnE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAEvB,IAAI,CAAC;QACH,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC;QAE1D,yEAAyE;QACzE,yEAAyE;QACzE,+DAA+D;QAC/D,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,CAAC,KAAK,CAAC,IAAA,0BAAiB,EAAC,SAAS,EAAE,aAAa,CAAC,EAAE,SAAS,CAAC,CAAC;QAEnE,yEAAyE;QACzE,uEAAuE;QACvE,MAAM,GAAG,GAAG,CAAC,MAAM,wBAAa,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAuB,CAAC;QAE9E,gEAAgE;QAChE,IAAA,sBAAa,EAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE;YAC5D,YAAY;YACZ,IAAI,EAAE,EAAE;SACT,CAAC,CAAC;QAEH,4DAA4D;QAC5D,MAAM,SAAS,GAAG,IAAA,2BAAc,EAAC,GAAG,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC;QAErE,kBAAkB;QAClB,MAAM,KAAK,GAAG,IAAI,GAAG,EAA4B,CAAC;QAClD,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC9C,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9B,CAAC;QAED,wDAAwD;QACxD,KAAK,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,IAAI,KAAK,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,IAAA,0BAAiB,EAAC,SAAS,EAAE,GAAG,CAAC,CAAC;YAEjD,KAAK,MAAM,EAAE,IAAI,YAAY,EAAE,CAAC;gBAC9B,IAAI,CAAC,KAAK,CACR,IAAA,0BAAiB,EAAC,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,WAAW,CAAC,EACpD,IAAA,8BAAe,EAAC,EAAE,EAAE,YAAY,EAAE,UAAU,CAAC,CAC9C,CAAC;YACJ,CAAC;YAED,MAAM,SAAS,GACb,YAAY;iBACT,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,oBAAoB,EAAE,CAAC,QAAQ,UAAU,CAAC;iBACtD,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,KAAK,CAAC,IAAA,0BAAiB,EAAC,MAAM,EAAE,UAAU,CAAC,EAAE,SAAS,CAAC,CAAC;QAC/D,CAAC;QAED,iFAAiF;QACjF,MAAM,UAAU,GACd,yCAAyC;YACzC,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;iBACd,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,GAAG,IAAI,CAAC;iBACzC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,IAAA,0BAAiB,EAAC,SAAS,EAAE,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;IACnE,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;IACH,CAAC;IAED,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,kBAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import type { EndpointModel } from './endpoint-model';
|
|
3
|
+
export declare function toScreamingSnake(str: string): string;
|
|
4
|
+
export declare function toKebabCase(str: string): string;
|
|
5
|
+
export declare function buildEndpoints(api: OpenAPIV3.Document, allowedTags: string[] | null, namingConvention: 'camel' | 'kebab'): EndpointModel[];
|
|
6
|
+
/** Convenience wrapper that dereferences the spec before building endpoints. */
|
|
7
|
+
export declare function parseSpec(specPath: string, allowedTags: string[] | null, namingConvention: 'camel' | 'kebab'): Promise<EndpointModel[]>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.toScreamingSnake = toScreamingSnake;
|
|
7
|
+
exports.toKebabCase = toKebabCase;
|
|
8
|
+
exports.buildEndpoints = buildEndpoints;
|
|
9
|
+
exports.parseSpec = parseSpec;
|
|
10
|
+
const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
|
|
11
|
+
const openapi_types_1 = require("openapi-types");
|
|
12
|
+
const HTTP_METHODS = [
|
|
13
|
+
openapi_types_1.OpenAPIV3.HttpMethods.GET,
|
|
14
|
+
openapi_types_1.OpenAPIV3.HttpMethods.POST,
|
|
15
|
+
openapi_types_1.OpenAPIV3.HttpMethods.PUT,
|
|
16
|
+
openapi_types_1.OpenAPIV3.HttpMethods.PATCH,
|
|
17
|
+
openapi_types_1.OpenAPIV3.HttpMethods.DELETE,
|
|
18
|
+
];
|
|
19
|
+
function toScreamingSnake(str) {
|
|
20
|
+
return str
|
|
21
|
+
.replace(/\//g, '_')
|
|
22
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
23
|
+
.replace(/[-\s.]+/g, '_')
|
|
24
|
+
.toUpperCase()
|
|
25
|
+
.replace(/_+/g, '_')
|
|
26
|
+
.replace(/^_|_$/g, '');
|
|
27
|
+
}
|
|
28
|
+
function toKebabCase(str) {
|
|
29
|
+
return str
|
|
30
|
+
.replace(/\//g, '-')
|
|
31
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
32
|
+
.replace(/[_\s.]+/g, '-')
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/-+/g, '-')
|
|
35
|
+
.replace(/^-|-$/g, '');
|
|
36
|
+
}
|
|
37
|
+
function buildEndpoints(api, allowedTags, namingConvention) {
|
|
38
|
+
const endpoints = [];
|
|
39
|
+
for (const [apiPath, pathItem] of Object.entries(api.paths ?? {})) {
|
|
40
|
+
if (!pathItem)
|
|
41
|
+
continue;
|
|
42
|
+
for (const method of HTTP_METHODS) {
|
|
43
|
+
const operation = pathItem[method];
|
|
44
|
+
if (!operation)
|
|
45
|
+
continue;
|
|
46
|
+
const tag = operation.tags?.[0] ?? 'default';
|
|
47
|
+
if (allowedTags && !allowedTags.includes(tag))
|
|
48
|
+
continue;
|
|
49
|
+
const rawId = operation.operationId ??
|
|
50
|
+
`${method}_${apiPath.replace(/\W+/g, '_').replace(/^_|_$/g, '')}`;
|
|
51
|
+
const fileName = namingConvention === 'kebab' ? toKebabCase(rawId) : rawId;
|
|
52
|
+
const tokenName = toScreamingSnake(rawId);
|
|
53
|
+
const allParams = [
|
|
54
|
+
...(pathItem.parameters ?? []),
|
|
55
|
+
...(operation.parameters ?? []),
|
|
56
|
+
];
|
|
57
|
+
const pathParams = allParams
|
|
58
|
+
.filter((p) => p.in === 'path')
|
|
59
|
+
.map((p) => p.name);
|
|
60
|
+
const hasQueryParams = allParams.some((p) => p.in === 'query');
|
|
61
|
+
const requestBody = operation.requestBody;
|
|
62
|
+
const bodyContent = requestBody?.content ?? {};
|
|
63
|
+
const BODY_CONTENT_TYPES = [
|
|
64
|
+
'application/json',
|
|
65
|
+
'application/x-www-form-urlencoded',
|
|
66
|
+
'multipart/form-data',
|
|
67
|
+
];
|
|
68
|
+
const bodyContentType = BODY_CONTENT_TYPES.find((ct) => bodyContent[ct]) ??
|
|
69
|
+
Object.keys(bodyContent)[0] ??
|
|
70
|
+
null;
|
|
71
|
+
const hasBody = bodyContentType !== null;
|
|
72
|
+
const rawStatus = operation.responses?.['200']
|
|
73
|
+
? '200'
|
|
74
|
+
: operation.responses?.['201']
|
|
75
|
+
? '201'
|
|
76
|
+
: null;
|
|
77
|
+
// Only treat the response as typed JSON when the response actually has
|
|
78
|
+
// application/json content (not PDF, octet-stream, etc.)
|
|
79
|
+
const responseObj = rawStatus
|
|
80
|
+
? operation.responses?.[rawStatus]
|
|
81
|
+
: null;
|
|
82
|
+
const responseStatus = responseObj?.content?.['application/json'] ? rawStatus : null;
|
|
83
|
+
const hasResponse = responseStatus !== null;
|
|
84
|
+
endpoints.push({
|
|
85
|
+
tag: toKebabCase(tag),
|
|
86
|
+
operationId: rawId,
|
|
87
|
+
method,
|
|
88
|
+
apiPath,
|
|
89
|
+
pathParams,
|
|
90
|
+
tokenName,
|
|
91
|
+
fileName,
|
|
92
|
+
hasQueryParams,
|
|
93
|
+
hasBody,
|
|
94
|
+
hasResponse,
|
|
95
|
+
responseStatus,
|
|
96
|
+
bodyContentType,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return endpoints;
|
|
101
|
+
}
|
|
102
|
+
/** Convenience wrapper that dereferences the spec before building endpoints. */
|
|
103
|
+
async function parseSpec(specPath, allowedTags, namingConvention) {
|
|
104
|
+
const api = (await swagger_parser_1.default.dereference(specPath));
|
|
105
|
+
return buildEndpoints(api, allowedTags, namingConvention);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=parse-spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-spec.js","sourceRoot":"","sources":["../../../../../../tools/openapi-resource-gen/src/generators/api-resource/parse-spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderTokenFile = renderTokenFile;
|
|
4
|
+
function toPascalCase(str) {
|
|
5
|
+
return str
|
|
6
|
+
.replace(/\//g, '-')
|
|
7
|
+
.split(/[-_\s]+/)
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
function toCamelCase(str) {
|
|
13
|
+
const parts = str.split(/[-_]+/).filter(Boolean);
|
|
14
|
+
return parts[0] + parts.slice(1).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
15
|
+
}
|
|
16
|
+
function renderTokenFile(ep, baseUrlToken, providedIn = 'none') {
|
|
17
|
+
const pascal = toPascalCase(ep.operationId);
|
|
18
|
+
const urlTemplate = ep.apiPath.replace(/\{([\w-]+)\}/g, (_, p) => `\${${toCamelCase(p)}}`);
|
|
19
|
+
const isGet = ep.method === 'get';
|
|
20
|
+
const { responseStatus } = ep;
|
|
21
|
+
const lines = [];
|
|
22
|
+
// Imports
|
|
23
|
+
const coreImports = ['InjectionToken', 'inject'];
|
|
24
|
+
if (!isGet && ep.hasBody)
|
|
25
|
+
coreImports.push('Signal');
|
|
26
|
+
if (providedIn === 'none')
|
|
27
|
+
coreImports.push('FactoryProvider');
|
|
28
|
+
lines.push(`import { ${coreImports.join(', ')} } from '@angular/core';`);
|
|
29
|
+
lines.push(`import { httpResource } from '@angular/common/http';`);
|
|
30
|
+
lines.push(`import type { paths } from '../schema.d';`);
|
|
31
|
+
lines.push(`import { ${baseUrlToken} } from '../api-base-url.token';`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
// Exported type aliases sourced directly from the generated paths type.
|
|
34
|
+
// NonNullable<> guards optional requestBody fields that are typed as T | undefined.
|
|
35
|
+
if (isGet && ep.hasQueryParams) {
|
|
36
|
+
lines.push(`export type ${pascal}Params =`, ` paths['${ep.apiPath}']['${ep.method}']['parameters']['query'];`, '');
|
|
37
|
+
}
|
|
38
|
+
if (!isGet && ep.hasBody && ep.bodyContentType) {
|
|
39
|
+
lines.push(`export type ${pascal}Body =`, ` NonNullable<paths['${ep.apiPath}']['${ep.method}']['requestBody']>['content']['${ep.bodyContentType}'];`, '');
|
|
40
|
+
}
|
|
41
|
+
if (responseStatus) {
|
|
42
|
+
lines.push(`export type ${pascal}Response =`, ` paths['${ep.apiPath}']['${ep.method}']['responses']['${responseStatus}']['content']['application/json'];`, '');
|
|
43
|
+
}
|
|
44
|
+
const responseT = responseStatus ? `${pascal}Response` : 'unknown';
|
|
45
|
+
const fnArgs = buildFnArgs(ep, pascal, isGet);
|
|
46
|
+
lines.push(`export const ${ep.tokenName} = new InjectionToken<`, ` (${fnArgs}) => ReturnType<typeof httpResource<${responseT}>>`, `>('${ep.tokenName}'${providedIn === 'root' ? `, {` : ')'}`);
|
|
47
|
+
if (providedIn === 'root') {
|
|
48
|
+
lines.push(` providedIn: 'root',`, ` factory: () => {`, ` const base = inject(${baseUrlToken});`, ` return (${fnArgs}) =>`, ` httpResource<${responseT}>(() => ({`, ` url: \`\${base}${urlTemplate}\`,`);
|
|
49
|
+
appendResourceOptions(lines, ep, isGet, ' ');
|
|
50
|
+
lines.push(` }));`, ` },`, `});`, '');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// providedIn: 'none' — token has no factory; provide via the helper below
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push(`export function provide${pascal}(): FactoryProvider {`, ` return {`, ` provide: ${ep.tokenName},`, ` useFactory: () => {`, ` const base = inject(${baseUrlToken});`, ` return (${fnArgs}) =>`, ` httpResource<${responseT}>(() => ({`, ` url: \`\${base}${urlTemplate}\`,`);
|
|
56
|
+
appendResourceOptions(lines, ep, isGet, ' ');
|
|
57
|
+
lines.push(` }));`, ` },`, ` };`, `}`, '');
|
|
58
|
+
}
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
function appendResourceOptions(lines, ep, isGet, indent) {
|
|
62
|
+
if (!isGet) {
|
|
63
|
+
lines.push(`${indent}method: '${ep.method.toUpperCase()}',`);
|
|
64
|
+
}
|
|
65
|
+
if (isGet && ep.hasQueryParams) {
|
|
66
|
+
lines.push(`${indent}params: (typeof params === 'function' ? params() : params) as unknown as Record<string, string | number | boolean | readonly (string | number | boolean)[]>,`);
|
|
67
|
+
}
|
|
68
|
+
if (!isGet && ep.hasBody) {
|
|
69
|
+
lines.push(`${indent}body,`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildFnArgs(ep, pascal, isGet) {
|
|
73
|
+
const args = ep.pathParams.map((p) => `${toCamelCase(p)}: string`);
|
|
74
|
+
if (isGet && ep.hasQueryParams)
|
|
75
|
+
args.push(`params?: ${pascal}Params | (() => ${pascal}Params | undefined)`);
|
|
76
|
+
if (!isGet && ep.hasBody)
|
|
77
|
+
args.push(`body: ${pascal}Body | Signal<${pascal}Body>`);
|
|
78
|
+
return args.join(', ');
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=render-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-token.js","sourceRoot":"","sources":["../../../../../../tools/openapi-resource-gen/src/generators/api-resource/render-token.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"cli": "nx",
|
|
4
|
+
"$id": "ApiResource",
|
|
5
|
+
"title": "Generate API resource tokens from an OpenAPI spec",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"specPath": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Path to the OpenAPI 3.x YAML or JSON spec file",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 }
|
|
12
|
+
},
|
|
13
|
+
"outputDir": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Output directory for generated files (relative to workspace root)"
|
|
16
|
+
},
|
|
17
|
+
"baseUrlToken": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Name of the InjectionToken holding the API base URL",
|
|
20
|
+
"default": "API_BASE_URL"
|
|
21
|
+
},
|
|
22
|
+
"tagFilter": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Comma-separated list of OpenAPI tags to include (empty = all tags)"
|
|
25
|
+
},
|
|
26
|
+
"namingConvention": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["camel", "kebab"],
|
|
29
|
+
"default": "kebab",
|
|
30
|
+
"description": "File naming convention: kebab-case filenames with SCREAMING_SNAKE token names"
|
|
31
|
+
},
|
|
32
|
+
"providedIn": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": ["root", "none"],
|
|
35
|
+
"default": "none",
|
|
36
|
+
"description": "none: token has no factory; a provide{Name}() helper is exported for explicit scoped provision. root: token self-registers at root (tree-shakeable singleton, no multi-context support)"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": ["specPath", "outputDir"]
|
|
40
|
+
}
|
package/src/index.d.ts
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apiResourceGenerator = exports.default = void 0;
|
|
4
|
+
var generator_1 = require("./generators/api-resource/generator");
|
|
5
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return generator_1.apiResourceGenerator; } });
|
|
6
|
+
var generator_2 = require("./generators/api-resource/generator");
|
|
7
|
+
Object.defineProperty(exports, "apiResourceGenerator", { enumerable: true, get: function () { return generator_2.apiResourceGenerator; } });
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../tools/openapi-resource-gen/src/index.ts"],"names":[],"mappings":";;;AAAA,iEAAsF;AAA7E,oGAAA,oBAAoB,OAAW;AACxC,iEAA2E;AAAlE,iHAAA,oBAAoB,OAAA"}
|