Skip to content

Commit e343dfd

Browse files
committed
feat(nest-plugin): introduce NestJS plugin for Intrig
- Added a new `@intrig/plugin-nest` for generating type-safe NestJS services from OpenAPI specs. - Supports tag-based service organization, type-safe request/response schemas, and NestJS DI-ready services. - Integrated with `@nestjs/axios` for HTTP operations and added a modular `IntrigModule` for easy service imports. - Includes extensive templates for modules, services, types, and documentation generation. - Updated monorepo configuration to include the NestJS plugin, build process, and publish setup.
1 parent 0fdad6f commit e343dfd

25 files changed

Lines changed: 1173 additions & 5 deletions

app/docs/docs/how-intrig-works/daemon-insight.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,18 +266,18 @@ lsof -ti:5050 | xargs kill
266266
## Performance Characteristics
267267

268268
**Indexing Time**:
269-
- Small APIs (<50 endpoints): <1 second
269+
- Small APIs (< 50 endpoints): < 1 second
270270
- Medium APIs (50-200 endpoints): 1-3 seconds
271-
- Large APIs (>500 endpoints): 3-5 seconds
271+
- Large APIs (> 500 endpoints): 3-5 seconds
272272

273273
**Memory Usage**:
274274
- Base daemon: ~30-50MB
275275
- Per API source: ~10-20MB
276276
- Typical project: 50-100MB total
277277

278278
**Search Performance**:
279-
- Average query response: <50ms
280-
- Complex pattern matches: <200ms
279+
- Average query response: < 50ms
280+
- Complex pattern matches: < 200ms
281281

282282
---
283283

nx.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@
158158
"@intrig/esbuild",
159159
"@intrig/plugin-sdk",
160160
"@intrig/plugin-react",
161-
"@intrig/plugin-next"
161+
"@intrig/plugin-next",
162+
"@intrig/plugin-nest"
162163
],
163164
"npmPackages": [
164165
{
@@ -184,6 +185,10 @@
184185
{
185186
"packageRoot": "dist/plugins/react",
186187
"packageName": "@intrig/plugin-react"
188+
},
189+
{
190+
"packageRoot": "dist/plugins/nest",
191+
"packageName": "@intrig/plugin-nest"
187192
}
188193
]
189194
},

plugins/nest/README.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# @intrig/plugin-nest
2+
3+
NestJS plugin for Intrig that generates injectable services from OpenAPI specifications.
4+
5+
## Overview
6+
7+
This plugin generates type-safe NestJS services from OpenAPI specs, organized by API tags. Each service contains methods corresponding to REST endpoints, fully typed with request/response schemas.
8+
9+
## Features
10+
11+
- **Tag-based Service Organization**: Groups endpoints by OpenAPI tags into separate injectable services
12+
- **Type-safe Generated Code**: Full TypeScript types from OpenAPI schemas
13+
- **NestJS HttpService Integration**: Uses `@nestjs/axios` for HTTP operations
14+
- **Promise-based API**: All methods return promises (observables converted with `firstValueFrom`)
15+
- **Dependency Injection Ready**: All services are decorated with `@Injectable()`
16+
- **Module Export**: Single `IntrigModule` exports all generated services
17+
18+
## Installation
19+
20+
```bash
21+
npm install @intrig/plugin-nest
22+
```
23+
24+
## Usage
25+
26+
### 1. Configure Intrig
27+
28+
In your `intrig.config.json`:
29+
30+
```json
31+
{
32+
"generator": "nest",
33+
"sources": [
34+
{
35+
"id": "petstore",
36+
"name": "Petstore API",
37+
"specUrl": "https://petstore3.swagger.io/api/v3/openapi.json"
38+
}
39+
]
40+
}
41+
```
42+
43+
### 2. Generate Services
44+
45+
```bash
46+
npx intrig build
47+
```
48+
49+
This generates:
50+
- `@intrig/nest/IntrigModule` - NestJS module
51+
- Service classes per tag (e.g., `UsersService`, `ProductsService`)
52+
- TypeScript types for all schemas
53+
- Full type safety for requests and responses
54+
55+
### 3. Import IntrigModule
56+
57+
In your `app.module.ts`:
58+
59+
```typescript
60+
import { Module } from '@nestjs/common';
61+
import { IntrigModule } from '@intrig/nest';
62+
63+
@Module({
64+
imports: [IntrigModule],
65+
// ...
66+
})
67+
export class AppModule {}
68+
```
69+
70+
### 4. Inject and Use Services
71+
72+
```typescript
73+
import { Injectable } from '@nestjs/common';
74+
import { UsersService } from '@intrig/nest';
75+
76+
@Injectable()
77+
export class MyService {
78+
constructor(private readonly usersService: UsersService) {}
79+
80+
async getUser(id: string) {
81+
return this.usersService.getUser(id);
82+
}
83+
84+
async createUser(data: CreateUserDto) {
85+
return this.usersService.createUser(data);
86+
}
87+
}
88+
```
89+
90+
## Generated Code Structure
91+
92+
```
93+
node_modules/@intrig/nest/
94+
├── intrig.module.ts # Main NestJS module
95+
├── index.ts # Barrel exports
96+
├── users/
97+
│ └── users.service.ts # UsersService (from 'users' tag)
98+
├── products/
99+
│ └── products.service.ts # ProductsService (from 'products' tag)
100+
└── components/
101+
└── schemas/ # TypeScript types
102+
├── User.ts
103+
├── Product.ts
104+
└── ...
105+
```
106+
107+
## Example Generated Service
108+
109+
```typescript
110+
import { Injectable } from '@nestjs/common';
111+
import { HttpService } from '@nestjs/axios';
112+
import { firstValueFrom } from 'rxjs';
113+
import { User } from '../components/schemas/User';
114+
import { CreateUserDto } from '../components/schemas/CreateUserDto';
115+
116+
@Injectable()
117+
export class UsersService {
118+
constructor(private readonly httpService: HttpService) {}
119+
120+
async getUser(id: string): Promise<User> {
121+
const response = await firstValueFrom(
122+
this.httpService.get<User>(`/users/${id}`)
123+
);
124+
return response.data;
125+
}
126+
127+
async createUser(data: CreateUserDto): Promise<User> {
128+
const response = await firstValueFrom(
129+
this.httpService.post<User>('/users', data)
130+
);
131+
return response.data;
132+
}
133+
134+
async deleteUser(id: string): Promise<void> {
135+
await firstValueFrom(
136+
this.httpService.delete(`/users/${id}`)
137+
);
138+
}
139+
}
140+
```
141+
142+
## Configuration Options
143+
144+
### Plugin Options
145+
146+
```typescript
147+
export type NestPluginOptions = {
148+
baseURL?: string; // Optional base URL for HTTP client
149+
};
150+
```
151+
152+
Currently, the plugin doesn't require specific options. The `baseURL` option is reserved for future use to configure the HTTP client's base URL.
153+
154+
## Architecture
155+
156+
- **Services**: One service per OpenAPI tag
157+
- **Methods**: One async method per endpoint operation
158+
- **Types**: Generated from OpenAPI schemas (no runtime validation)
159+
- **HTTP Client**: NestJS `HttpService` (axios wrapper)
160+
- **Error Handling**: Let NestJS exception filters handle errors
161+
162+
## Comparison with React Plugin
163+
164+
| Feature | React Plugin | NestJS Plugin |
165+
|---------|-------------|---------------|
166+
| **Target** | Browser (React hooks) | Server (NestJS services) |
167+
| **State Management** | Yes (React state) | No |
168+
| **Caching** | Yes (built-in) | No (use NestJS interceptors) |
169+
| **Validation** | Zod schemas | TypeScript types only |
170+
| **Organization** | By source | By OpenAPI tag |
171+
| **DI** | React Context | NestJS DI |
172+
173+
## Development
174+
175+
### Build
176+
177+
```bash
178+
nx build @intrig/plugin-nest
179+
```
180+
181+
### Test
182+
183+
```bash
184+
nx test @intrig/plugin-nest
185+
```
186+
187+
## License
188+
189+
MIT

plugins/nest/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@intrig/plugin-nest",
3+
"version": "0.0.1",
4+
"type": "module",
5+
"main": "./dist/index.cjs",
6+
"module": "./dist/index.js",
7+
"types": "./dist/index.d.ts",
8+
"exports": {
9+
"./package.json": "./package.json",
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js",
13+
"require": "./dist/index.cjs",
14+
"default": "./dist/index.js"
15+
}
16+
},
17+
"dependencies": {
18+
"@intrig/plugin-sdk": "^0.0.2-6",
19+
"@swc/helpers": "~0.5.11"
20+
},
21+
"files": [
22+
"dist",
23+
"package.json"
24+
],
25+
"publishConfig": {
26+
"access": "public"
27+
}
28+
}

plugins/nest/project.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@intrig/plugin-nest",
3+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "plugins/nest/src",
5+
"projectType": "library",
6+
"tags": ["Intrig", "nest", "generator"],
7+
"targets": {
8+
"build": {
9+
"executor": "@nx/rollup:rollup",
10+
"outputs": ["{options.outputPath}"],
11+
"options": {
12+
"outputPath": "dist/plugins/nest",
13+
"main": "plugins/nest/src/index.ts",
14+
"tsConfig": "plugins/nest/tsconfig.lib.json",
15+
"compiler": "swc",
16+
"format": ["esm", "cjs"],
17+
"outputFileName": "index.js",
18+
"assets": [
19+
{
20+
"input": "plugins/nest",
21+
"glob": "package.json",
22+
"output": "."
23+
}
24+
],
25+
"rollupConfig": "plugins/nest/rollup.config.cjs"
26+
}
27+
},
28+
"nx-release-publish": {
29+
"executor": "@nx/js:release-publish",
30+
"options": {
31+
"packageRoot": "dist/plugins/nest"
32+
},
33+
"dependsOn": [
34+
"^build"
35+
]
36+
}
37+
}
38+
}

plugins/nest/rollup.config.cjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const fs = require('node:fs');
2+
const path = require('node:path');
3+
4+
function getPkgJson(pkgDir) {
5+
const pkgPath = path.join(pkgDir, 'package.json');
6+
if (fs.existsSync(pkgPath)) {
7+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
8+
}
9+
return {};
10+
}
11+
12+
const pkg = getPkgJson(__dirname);
13+
const rootPkg = getPkgJson(path.resolve(__dirname, '../..'));
14+
15+
const deps = new Set([
16+
...Object.keys(pkg.dependencies || {}),
17+
...Object.keys(pkg.peerDependencies || {}),
18+
...Object.keys(rootPkg.dependencies || {}),
19+
...Object.keys(rootPkg.peerDependencies || {}),
20+
]);
21+
22+
function external(id) {
23+
if (id.startsWith('.') || path.isAbsolute(id)) return false; // bundle local files
24+
// Treat any bare module specifier or node_modules import as external
25+
if (id.includes('node_modules')) return true;
26+
const [scopeOrName, maybeName] = id.split('/');
27+
const name = scopeOrName && scopeOrName.startsWith('@') && maybeName ? `${scopeOrName}/${maybeName}` : scopeOrName;
28+
return deps.has(name);
29+
}
30+
31+
module.exports = (config) => {
32+
const newConfig = { ...config };
33+
if (Array.isArray(newConfig.output)) {
34+
newConfig.output = newConfig.output.map((o) => ({
35+
...o,
36+
// Emit all JS files under a nested 'dist' directory inside the outputPath
37+
entryFileNames: o.format === 'cjs' ? 'dist/index.cjs' : 'dist/index.js',
38+
chunkFileNames: o.format === 'cjs' ? 'dist/[name].cjs' : 'dist/[name].js',
39+
assetFileNames: 'dist/[name][extname]',
40+
exports: o.format === 'cjs' ? 'named' : undefined
41+
}));
42+
} else if (newConfig.output) {
43+
newConfig.output = {
44+
...newConfig.output,
45+
// Emit all JS files under a nested 'dist' directory inside the outputPath
46+
entryFileNames: newConfig.output.format === 'cjs' ? 'dist/index.cjs' : 'dist/index.js',
47+
chunkFileNames: newConfig.output.format === 'cjs' ? 'dist/[name].cjs' : 'dist/[name].js',
48+
assetFileNames: 'dist/[name][extname]',
49+
exports: newConfig.output.format === 'cjs' ? 'named' : undefined
50+
};
51+
}
52+
newConfig.external = external;
53+
return newConfig;
54+
};

plugins/nest/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { createPlugin } from './lib/plugin-nest.js';
2+
export { createPlugin as default } from './lib/plugin-nest.js';
3+
export type { NestPluginOptions } from './lib/init-plugin.js';

0 commit comments

Comments
 (0)