nest-hex 0.2.2 โ 0.3.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 +152 -443
- package/dist/src/cli/bin.js +19 -5
- package/dist/src/cli/commands/index.js +19 -5
- package/dist/src/cli/generators/adapter.generator.js +1 -1
- package/dist/src/cli/generators/base.generator.js +1 -1
- package/dist/src/cli/generators/index.d.ts +16 -16
- package/dist/src/cli/generators/index.js +1 -1
- package/dist/src/cli/generators/port.generator.js +1 -1
- package/dist/src/cli/generators/service.generator.js +1 -1
- package/dist/src/cli/ui/components/index.d.ts +12 -12
- package/dist/src/cli/ui/components/index.js +18 -4
- package/dist/src/index.d.ts +60 -30
- package/dist/src/index.js +31 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,172 +1,28 @@
|
|
|
1
1
|
# nest-hex
|
|
2
2
|
|
|
3
|
-
> A tiny, **class-based**, **NestJS-native**
|
|
3
|
+
> A tiny, **class-based**, **NestJS-native** library for building **pluggable adapters** following the Ports & Adapters (Hexagonal Architecture) pattern with minimal boilerplate.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## What is nest-hex?
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
**nest-hex** eliminates boilerplate when building NestJS applications with the Ports & Adapters (Hexagonal Architecture) pattern. It provides decorators and base classes that handle all the repetitive wiring, letting you focus on business logic.
|
|
10
10
|
|
|
11
|
-
###
|
|
12
|
-
Your business logic should never depend on infrastructure details like databases, APIs, or file systems. By defining **ports** (interfaces), your domain layer stays clean and focused on what matters: solving business problems.
|
|
11
|
+
### Why Hexagonal Architecture?
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Mock external services by creating test adapters. No complex setup, no database connections, no API calls. Just simple, fast unit tests that focus on business logic.
|
|
19
|
-
|
|
20
|
-
### ๐ Environment Flexibility
|
|
21
|
-
- **Development**: Use filesystem storage
|
|
22
|
-
- **Testing**: Use in-memory mocks
|
|
23
|
-
- **Production**: Use AWS S3
|
|
24
|
-
|
|
25
|
-
Same domain code, different adapters. Configure once, swap anywhere.
|
|
26
|
-
|
|
27
|
-
### ๐ฆ Independent Deployment
|
|
28
|
-
Infrastructure changes don't require redeploying your entire application. Update an adapter independently without touching core business logic.
|
|
29
|
-
|
|
30
|
-
## Why This Library?
|
|
31
|
-
|
|
32
|
-
Building NestJS applications with the Ports & Adapters pattern involves repetitive boilerplate:
|
|
33
|
-
|
|
34
|
-
- Registering concrete implementation classes
|
|
35
|
-
- Aliasing port tokens to implementations (`useExisting`)
|
|
36
|
-
- Exporting only the port token (not provider objects)
|
|
37
|
-
- Supporting both `register()` and `registerAsync()` patterns
|
|
38
|
-
- Keeping the app responsible for configuration (no `process.env` in libraries)
|
|
39
|
-
|
|
40
|
-
**nest-hex eliminates this boilerplate** while maintaining compile-time type safety and providing a delightful developer experience through both decorators and a powerful CLI.
|
|
13
|
+
- ๐งช **Testable** - Mock infrastructure easily, test business logic in isolation
|
|
14
|
+
- ๐ **Swappable** - Switch from S3 to Azure Blob Storage without touching domain code
|
|
15
|
+
- ๐ฏ **Clean** - Keep business logic free of infrastructure concerns
|
|
16
|
+
- ๐ **Flexible** - Use different adapters for dev, test, and production
|
|
41
17
|
|
|
42
18
|
## Features
|
|
43
19
|
|
|
44
|
-
- ๐ฏ **Declarative
|
|
45
|
-
- ๐๏ธ **Class-based
|
|
46
|
-
- ๐ **Type-safe
|
|
47
|
-
- โก **Zero runtime overhead
|
|
48
|
-
- ๐ฆ **Tiny
|
|
49
|
-
-
|
|
50
|
-
- ๐ ๏ธ **Powerful CLI**: Generate ports, adapters, and services with a single command
|
|
51
|
-
|
|
52
|
-
## CLI
|
|
53
|
-
|
|
54
|
-
**nest-hex** includes a powerful CLI to scaffold ports, adapters, and services instantly. No more manual file creation!
|
|
55
|
-
|
|
56
|
-
### Quick Start
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# Initialize configuration
|
|
60
|
-
npx nest-hex init
|
|
61
|
-
|
|
62
|
-
# Generate a port (domain interface)
|
|
63
|
-
npx nest-hex generate port ObjectStorage
|
|
64
|
-
|
|
65
|
-
# Generate an adapter for the port
|
|
66
|
-
npx nest-hex generate adapter S3 --port ObjectStorage
|
|
67
|
-
|
|
68
|
-
# Generate both port and adapter together
|
|
69
|
-
npx nest-hex generate full ObjectStorage S3
|
|
70
|
-
|
|
71
|
-
# Generate a service that uses a port
|
|
72
|
-
npx nest-hex generate service FileUpload
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Available Commands
|
|
76
|
-
|
|
77
|
-
#### `init`
|
|
78
|
-
Create a `nest-hex.config.ts` configuration file in your project.
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx nest-hex init
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
#### `generate` (or `g`)
|
|
85
|
-
Generate ports, adapters, services, or complete modules.
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
# Generate a port
|
|
89
|
-
npx nest-hex generate port <name>
|
|
90
|
-
npx nest-hex g port PaymentGateway
|
|
91
|
-
|
|
92
|
-
# Generate an adapter
|
|
93
|
-
npx nest-hex generate adapter <name> --port <portName>
|
|
94
|
-
npx nest-hex g adapter Stripe --port PaymentGateway
|
|
95
|
-
|
|
96
|
-
# Generate both port and adapter
|
|
97
|
-
npx nest-hex generate full <portName> <adapterName>
|
|
98
|
-
npx nest-hex g full EmailService SendGrid
|
|
99
|
-
|
|
100
|
-
# Generate a service
|
|
101
|
-
npx nest-hex generate service <name>
|
|
102
|
-
npx nest-hex g service UserRegistration
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Interactive Mode
|
|
106
|
-
|
|
107
|
-
Run commands without arguments for interactive prompts:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
npx nest-hex generate
|
|
111
|
-
# โ Select type: port, adapter, service, or full
|
|
112
|
-
# โ Enter name(s)
|
|
113
|
-
# โ Files generated!
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Configuration
|
|
117
|
-
|
|
118
|
-
The CLI uses `nest-hex.config.ts` to customize output paths and naming conventions:
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
// nest-hex.config.ts
|
|
122
|
-
import { defineConfig } from 'nest-hex/cli';
|
|
123
|
-
|
|
124
|
-
export default defineConfig({
|
|
125
|
-
output: {
|
|
126
|
-
portsDir: 'src/domain/ports', // Where to generate ports
|
|
127
|
-
adaptersDir: 'src/infrastructure', // Where to generate adapters
|
|
128
|
-
servicesDir: 'src/application', // Where to generate services
|
|
129
|
-
},
|
|
130
|
-
naming: {
|
|
131
|
-
portSuffix: 'Port', // ObjectStoragePort
|
|
132
|
-
tokenSuffix: '_PORT', // OBJECT_STORAGE_PORT
|
|
133
|
-
adapterSuffix: 'Adapter', // S3Adapter
|
|
134
|
-
serviceSuffix: 'Service', // FileUploadService
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### What Gets Generated
|
|
140
|
-
|
|
141
|
-
#### Port Generation
|
|
142
|
-
Creates a complete port with:
|
|
143
|
-
- Token definition (`OBJECT_STORAGE_PORT`)
|
|
144
|
-
- TypeScript interface with example methods
|
|
145
|
-
- Service implementation with `@InjectPort`
|
|
146
|
-
- Module that accepts adapters
|
|
147
|
-
- Barrel exports (`index.ts`)
|
|
148
|
-
|
|
149
|
-
#### Adapter Generation
|
|
150
|
-
Creates a production-ready adapter with:
|
|
151
|
-
- Implementation service class
|
|
152
|
-
- Options interface
|
|
153
|
-
- Adapter class with `@Port` decorator
|
|
154
|
-
- Complete TypeScript types
|
|
155
|
-
- Barrel exports
|
|
156
|
-
|
|
157
|
-
#### Service Generation
|
|
158
|
-
Creates a domain service with:
|
|
159
|
-
- Service class with `@InjectPort` usage
|
|
160
|
-
- Type-safe port injection
|
|
161
|
-
- Example business logic methods
|
|
162
|
-
|
|
163
|
-
### CLI Benefits
|
|
164
|
-
|
|
165
|
-
โ
**Instant scaffolding** - Generate complete, type-safe modules in seconds
|
|
166
|
-
โ
**Consistent structure** - All team members follow the same patterns
|
|
167
|
-
โ
**Best practices built-in** - Generated code follows hexagonal architecture principles
|
|
168
|
-
โ
**Customizable** - Configure paths and naming to match your project
|
|
169
|
-
โ
**Interactive** - Friendly prompts guide you through generation
|
|
20
|
+
- ๐ฏ **Declarative** - Declare port tokens and implementations once using `@Adapter({ portToken, implementation })`
|
|
21
|
+
- ๐๏ธ **Class-based** - Standard NestJS dynamic modules, no function factories
|
|
22
|
+
- ๐ **Type-safe** - Compile-time proof that adapters provide the correct port tokens
|
|
23
|
+
- โก **Zero runtime overhead** - Uses TypeScript decorators and metadata
|
|
24
|
+
- ๐ฆ **Tiny** - Core library under 1KB minified
|
|
25
|
+
- ๐ ๏ธ **Powerful CLI** - Generate ports, adapters, and services instantly
|
|
170
26
|
|
|
171
27
|
## Installation
|
|
172
28
|
|
|
@@ -180,8 +36,7 @@ pnpm add nest-hex
|
|
|
180
36
|
bun add nest-hex
|
|
181
37
|
```
|
|
182
38
|
|
|
183
|
-
|
|
184
|
-
|
|
39
|
+
**Peer dependencies:**
|
|
185
40
|
```bash
|
|
186
41
|
npm install @nestjs/common @nestjs/core reflect-metadata
|
|
187
42
|
```
|
|
@@ -192,11 +47,11 @@ npm install @nestjs/common @nestjs/core reflect-metadata
|
|
|
192
47
|
|
|
193
48
|
```typescript
|
|
194
49
|
// storage.port.ts
|
|
195
|
-
export const STORAGE_PORT = Symbol('STORAGE_PORT')
|
|
50
|
+
export const STORAGE_PORT = Symbol('STORAGE_PORT')
|
|
196
51
|
|
|
197
52
|
export interface StoragePort {
|
|
198
|
-
upload(
|
|
199
|
-
download(key: string): Promise<Buffer
|
|
53
|
+
upload(key: string, data: Buffer): Promise<string>
|
|
54
|
+
download(key: string): Promise<Buffer>
|
|
200
55
|
}
|
|
201
56
|
```
|
|
202
57
|
|
|
@@ -204,103 +59,116 @@ export interface StoragePort {
|
|
|
204
59
|
|
|
205
60
|
```typescript
|
|
206
61
|
// s3.adapter.ts
|
|
207
|
-
import { Injectable } from '@nestjs/common'
|
|
208
|
-
import { Adapter
|
|
209
|
-
import { STORAGE_PORT, type StoragePort } from './storage.port'
|
|
62
|
+
import { Injectable } from '@nestjs/common'
|
|
63
|
+
import { Adapter } from 'nest-hex'
|
|
64
|
+
import { STORAGE_PORT, type StoragePort } from './storage.port'
|
|
210
65
|
|
|
211
66
|
// Implementation service
|
|
212
67
|
@Injectable()
|
|
213
|
-
class
|
|
214
|
-
|
|
68
|
+
class S3Service implements StoragePort {
|
|
69
|
+
constructor(private options: { bucket: string; region: string }) {}
|
|
70
|
+
|
|
71
|
+
async upload(key: string, data: Buffer): Promise<string> {
|
|
215
72
|
// AWS S3 upload logic here
|
|
216
|
-
return
|
|
73
|
+
return `https://s3.amazonaws.com/${this.options.bucket}/${key}`
|
|
217
74
|
}
|
|
218
75
|
|
|
219
|
-
async download(key: string) {
|
|
76
|
+
async download(key: string): Promise<Buffer> {
|
|
220
77
|
// AWS S3 download logic here
|
|
221
|
-
return Buffer.from('file contents')
|
|
78
|
+
return Buffer.from('file contents')
|
|
222
79
|
}
|
|
223
80
|
}
|
|
224
81
|
|
|
225
|
-
// Adapter configuration
|
|
226
|
-
interface S3Options {
|
|
227
|
-
bucket: string;
|
|
228
|
-
region: string;
|
|
229
|
-
accessKeyId?: string;
|
|
230
|
-
secretAccessKey?: string;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
82
|
// Adapter module - single decorator declares everything!
|
|
234
|
-
@
|
|
235
|
-
|
|
236
|
-
implementation:
|
|
83
|
+
@Adapter({
|
|
84
|
+
portToken: STORAGE_PORT,
|
|
85
|
+
implementation: S3Service
|
|
237
86
|
})
|
|
238
|
-
export class S3Adapter extends
|
|
87
|
+
export class S3Adapter extends AdapterBase<{ bucket: string; region: string }> {}
|
|
239
88
|
```
|
|
240
89
|
|
|
241
|
-
### 3. Create a
|
|
90
|
+
### 3. Create a Domain Service
|
|
242
91
|
|
|
243
92
|
```typescript
|
|
244
|
-
//
|
|
245
|
-
import { Injectable
|
|
246
|
-
import { InjectPort
|
|
247
|
-
import { STORAGE_PORT, type StoragePort } from './storage.port'
|
|
93
|
+
// file.service.ts
|
|
94
|
+
import { Injectable } from '@nestjs/common'
|
|
95
|
+
import { InjectPort } from 'nest-hex'
|
|
96
|
+
import { STORAGE_PORT, type StoragePort } from './storage.port'
|
|
248
97
|
|
|
249
|
-
// Domain service that uses the port
|
|
250
98
|
@Injectable()
|
|
251
|
-
export class
|
|
99
|
+
export class FileService {
|
|
252
100
|
constructor(
|
|
253
101
|
@InjectPort(STORAGE_PORT)
|
|
254
|
-
private readonly storage: StoragePort
|
|
102
|
+
private readonly storage: StoragePort
|
|
255
103
|
) {}
|
|
256
104
|
|
|
257
|
-
async uploadUserAvatar(userId: string, image: Buffer) {
|
|
258
|
-
const key = `avatars/${userId}.jpg
|
|
259
|
-
return this.storage.upload(
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async downloadUserAvatar(userId: string) {
|
|
263
|
-
const key = `avatars/${userId}.jpg`;
|
|
264
|
-
return this.storage.download(key);
|
|
105
|
+
async uploadUserAvatar(userId: string, image: Buffer): Promise<string> {
|
|
106
|
+
const key = `avatars/${userId}.jpg`
|
|
107
|
+
return this.storage.upload(key, image)
|
|
265
108
|
}
|
|
266
109
|
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 4. Create a Port Module
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// file.module.ts
|
|
116
|
+
import { Module } from '@nestjs/common'
|
|
117
|
+
import { PortModule } from 'nest-hex'
|
|
118
|
+
import { FileService } from './file.service'
|
|
267
119
|
|
|
268
|
-
// Port module that accepts any adapter
|
|
269
120
|
@Module({})
|
|
270
|
-
export class
|
|
121
|
+
export class FileModule extends PortModule<typeof FileService> {}
|
|
271
122
|
```
|
|
272
123
|
|
|
273
|
-
###
|
|
124
|
+
### 5. Wire It Up
|
|
274
125
|
|
|
275
126
|
```typescript
|
|
276
127
|
// app.module.ts
|
|
277
|
-
import { Module } from '@nestjs/common'
|
|
278
|
-
import {
|
|
279
|
-
import S3Adapter from './
|
|
128
|
+
import { Module } from '@nestjs/common'
|
|
129
|
+
import { FileModule } from './file.module'
|
|
130
|
+
import { S3Adapter } from './s3.adapter'
|
|
280
131
|
|
|
281
132
|
@Module({
|
|
282
133
|
imports: [
|
|
283
|
-
|
|
134
|
+
FileModule.register({
|
|
284
135
|
adapter: S3Adapter.register({
|
|
285
|
-
bucket: 'my-
|
|
286
|
-
region: 'us-east-1'
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}),
|
|
291
|
-
],
|
|
136
|
+
bucket: process.env.S3_BUCKET || 'my-bucket',
|
|
137
|
+
region: process.env.AWS_REGION || 'us-east-1'
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
]
|
|
292
141
|
})
|
|
293
142
|
export class AppModule {}
|
|
294
143
|
```
|
|
295
144
|
|
|
296
145
|
That's it! You now have a fully type-safe, pluggable storage adapter. ๐
|
|
297
146
|
|
|
147
|
+
## CLI
|
|
148
|
+
|
|
149
|
+
Generate ports, adapters, and services instantly with the built-in CLI:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Initialize configuration
|
|
153
|
+
npx nest-hex init
|
|
154
|
+
|
|
155
|
+
# Generate a port (domain interface)
|
|
156
|
+
npx nest-hex generate port ObjectStorage
|
|
157
|
+
|
|
158
|
+
# Generate an adapter for the port
|
|
159
|
+
npx nest-hex generate adapter S3 --port ObjectStorage
|
|
160
|
+
|
|
161
|
+
# Or generate both at once
|
|
162
|
+
npx nest-hex generate full ObjectStorage S3
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**See [CLI Documentation](./docs/cli.md) for complete command reference, configuration options, and template customization.**
|
|
166
|
+
|
|
298
167
|
## Key Benefits
|
|
299
168
|
|
|
300
169
|
### Before (Manual Boilerplate)
|
|
301
170
|
|
|
302
171
|
```typescript
|
|
303
|
-
// Lots of manual wiring...
|
|
304
172
|
@Module({})
|
|
305
173
|
export class S3StorageModule {
|
|
306
174
|
static register(options: S3Options): DynamicModule {
|
|
@@ -312,7 +180,7 @@ export class S3StorageModule {
|
|
|
312
180
|
// More boilerplate...
|
|
313
181
|
],
|
|
314
182
|
exports: [STORAGE_PORT],
|
|
315
|
-
}
|
|
183
|
+
}
|
|
316
184
|
}
|
|
317
185
|
}
|
|
318
186
|
```
|
|
@@ -320,267 +188,109 @@ export class S3StorageModule {
|
|
|
320
188
|
### After (With nest-hex)
|
|
321
189
|
|
|
322
190
|
```typescript
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
implementation: S3StorageService,
|
|
191
|
+
@Adapter({
|
|
192
|
+
portToken: STORAGE_PORT,
|
|
193
|
+
implementation: S3StorageService
|
|
327
194
|
})
|
|
328
|
-
export class S3Adapter extends
|
|
195
|
+
export class S3Adapter extends AdapterBase<S3Options> {}
|
|
329
196
|
```
|
|
330
197
|
|
|
331
|
-
##
|
|
198
|
+
## Swappable Infrastructure
|
|
332
199
|
|
|
333
|
-
|
|
200
|
+
The real power: swap infrastructure without touching business logic.
|
|
334
201
|
|
|
335
202
|
```typescript
|
|
336
|
-
|
|
203
|
+
// Development: Use local filesystem
|
|
204
|
+
const adapter = process.env.NODE_ENV === 'production'
|
|
205
|
+
? S3Adapter.register({ bucket: 'prod-bucket', region: 'us-east-1' })
|
|
206
|
+
: LocalStorageAdapter.register({ basePath: './uploads' })
|
|
337
207
|
|
|
338
208
|
@Module({
|
|
339
|
-
imports: [
|
|
340
|
-
StorageModule.register({
|
|
341
|
-
adapter: S3Adapter.registerAsync({
|
|
342
|
-
imports: [ConfigModule],
|
|
343
|
-
inject: [ConfigService],
|
|
344
|
-
useFactory: (config: ConfigService) => ({
|
|
345
|
-
bucket: config.get('S3_BUCKET'),
|
|
346
|
-
region: config.get('AWS_REGION'),
|
|
347
|
-
accessKeyId: config.get('AWS_ACCESS_KEY_ID'),
|
|
348
|
-
secretAccessKey: config.get('AWS_SECRET_ACCESS_KEY'),
|
|
349
|
-
}),
|
|
350
|
-
}),
|
|
351
|
-
}),
|
|
352
|
-
],
|
|
209
|
+
imports: [FileModule.register({ adapter })]
|
|
353
210
|
})
|
|
354
211
|
export class AppModule {}
|
|
355
212
|
```
|
|
356
213
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
```typescript
|
|
360
|
-
@Port({
|
|
361
|
-
token: HTTP_CLIENT_PORT,
|
|
362
|
-
implementation: AxiosHttpClient,
|
|
363
|
-
})
|
|
364
|
-
class AxiosAdapterClass extends Adapter<AxiosOptions> {
|
|
365
|
-
protected override imports(options: AxiosOptions) {
|
|
366
|
-
return [
|
|
367
|
-
HttpModule.register({
|
|
368
|
-
baseURL: options.baseUrl,
|
|
369
|
-
timeout: options.timeout,
|
|
370
|
-
}),
|
|
371
|
-
];
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
protected override extraPoviders(options: AxiosOptions) {
|
|
375
|
-
return [
|
|
376
|
-
{
|
|
377
|
-
provide: 'HTTP_CLIENT_CONFIG',
|
|
378
|
-
useValue: options,
|
|
379
|
-
},
|
|
380
|
-
];
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### Swapping Adapters - The Power of Pluggability
|
|
214
|
+
Your `FileService` business logic **never changes**. Only the adapter changes.
|
|
386
215
|
|
|
387
|
-
|
|
216
|
+
## Advanced Features
|
|
388
217
|
|
|
389
|
-
|
|
218
|
+
### Async Configuration with Dependency Injection
|
|
390
219
|
|
|
391
220
|
```typescript
|
|
392
|
-
// Development: Use filesystem storage
|
|
393
|
-
import FilesystemAdapter from './storage/adapters/filesystem.adapter';
|
|
394
|
-
|
|
395
|
-
// Production: Use AWS S3
|
|
396
|
-
import S3Adapter from './storage/adapters/s3.adapter';
|
|
397
|
-
|
|
398
|
-
const adapter = process.env.NODE_ENV === 'production'
|
|
399
|
-
? S3Adapter.register({ bucket: 'prod-bucket', region: 'us-east-1' })
|
|
400
|
-
: FilesystemAdapter.register({ basePath: './uploads' });
|
|
401
|
-
|
|
402
221
|
@Module({
|
|
403
222
|
imports: [
|
|
404
|
-
|
|
405
|
-
|
|
223
|
+
FileModule.register({
|
|
224
|
+
adapter: S3Adapter.registerAsync({
|
|
225
|
+
imports: [ConfigModule],
|
|
226
|
+
inject: [ConfigService],
|
|
227
|
+
useFactory: (config: ConfigService) => ({
|
|
228
|
+
bucket: config.get('S3_BUCKET')!,
|
|
229
|
+
region: config.get('AWS_REGION')!
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
]
|
|
406
234
|
})
|
|
407
235
|
export class AppModule {}
|
|
408
236
|
```
|
|
409
237
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
// Easily switch cloud providers without changing domain code
|
|
414
|
-
const storageAdapter = process.env.CLOUD_PROVIDER === 'aws'
|
|
415
|
-
? S3Adapter.register({ bucket: 'my-bucket', region: 'us-east-1' })
|
|
416
|
-
: process.env.CLOUD_PROVIDER === 'gcp'
|
|
417
|
-
? GCSAdapter.register({ bucket: 'my-bucket' })
|
|
418
|
-
: AzureBlobAdapter.register({ containerName: 'my-container' });
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
#### Feature Flags
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
// Gradually migrate to new infrastructure
|
|
425
|
-
const emailAdapter = featureFlags.useNewEmailProvider
|
|
426
|
-
? SendGridAdapter.register({ apiKey: process.env.SENDGRID_KEY })
|
|
427
|
-
: SESAdapter.register({ region: 'us-east-1' });
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
#### Testing with Mocks
|
|
238
|
+
### Adapters with Dependencies
|
|
431
239
|
|
|
432
240
|
```typescript
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
241
|
+
@Adapter({
|
|
242
|
+
portToken: HTTP_CLIENT_PORT,
|
|
243
|
+
implementation: AxiosHttpClient,
|
|
244
|
+
imports: [HttpModule],
|
|
245
|
+
providers: [
|
|
246
|
+
{ provide: 'HTTP_CONFIG', useValue: { timeout: 5000 } }
|
|
247
|
+
]
|
|
248
|
+
})
|
|
249
|
+
export class AxiosAdapter extends AdapterBase<AxiosOptions> {}
|
|
440
250
|
```
|
|
441
251
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
### Testing with Mock Adapters
|
|
252
|
+
### Mock Adapters for Testing
|
|
445
253
|
|
|
446
254
|
```typescript
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return { url: `mock://storage/${key}` };
|
|
255
|
+
@Injectable()
|
|
256
|
+
class MockStorageService implements StoragePort {
|
|
257
|
+
async upload(key: string, data: Buffer): Promise<string> {
|
|
258
|
+
return `mock://storage/${key}`
|
|
452
259
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return Buffer.from('mock file contents');
|
|
260
|
+
async download(key: string): Promise<Buffer> {
|
|
261
|
+
return Buffer.from('mock data')
|
|
456
262
|
}
|
|
457
263
|
}
|
|
458
264
|
|
|
459
|
-
@
|
|
460
|
-
|
|
461
|
-
implementation: MockStorageService
|
|
265
|
+
@Adapter({
|
|
266
|
+
portToken: STORAGE_PORT,
|
|
267
|
+
implementation: MockStorageService
|
|
462
268
|
})
|
|
463
|
-
export class MockStorageAdapter extends
|
|
269
|
+
export class MockStorageAdapter extends AdapterBase<{}> {}
|
|
464
270
|
|
|
465
271
|
// Use in tests
|
|
466
272
|
const module = await Test.createTestingModule({
|
|
467
273
|
imports: [
|
|
468
|
-
|
|
469
|
-
adapter: MockStorageAdapter.register(
|
|
470
|
-
})
|
|
471
|
-
]
|
|
472
|
-
}).compile()
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
## API Reference
|
|
476
|
-
|
|
477
|
-
### Core Classes
|
|
478
|
-
|
|
479
|
-
#### `Adapter<TOptions>`
|
|
480
|
-
|
|
481
|
-
Abstract base class for building adapter modules.
|
|
482
|
-
|
|
483
|
-
**Methods:**
|
|
484
|
-
- `static register<TToken, TOptions>(options: TOptions): AdapterModule<TToken>` - Synchronous registration
|
|
485
|
-
- `static registerAsync<TToken, TOptions>(config: AsyncConfig): AdapterModule<TToken>` - Async registration with DI
|
|
486
|
-
|
|
487
|
-
**Protected Hooks:**
|
|
488
|
-
- `protected imports(options?: TOptions): unknown[]` - Override to import other NestJS modules
|
|
489
|
-
- `protected extraPoviders(options: TOptions): Port[]` - Override to add additional providers
|
|
490
|
-
|
|
491
|
-
#### `PortModule<TService>`
|
|
492
|
-
|
|
493
|
-
Abstract base class for building port modules that consume adapters.
|
|
494
|
-
|
|
495
|
-
**Methods:**
|
|
496
|
-
- `static register<TToken>({ adapter }: { adapter?: AdapterModule<TToken> }): DynamicModule`
|
|
497
|
-
|
|
498
|
-
### Decorators
|
|
499
|
-
|
|
500
|
-
#### `@Port({ token, implementation })`
|
|
501
|
-
|
|
502
|
-
Class decorator that declares which port token an adapter provides and its implementation class.
|
|
503
|
-
|
|
504
|
-
**Parameters:**
|
|
505
|
-
- `token: TToken` - The port token (usually a Symbol)
|
|
506
|
-
- `implementation: Type<unknown>` - The concrete implementation class
|
|
507
|
-
|
|
508
|
-
**Example:**
|
|
509
|
-
```typescript
|
|
510
|
-
@Port({
|
|
511
|
-
token: STORAGE_PORT,
|
|
512
|
-
implementation: S3StorageService,
|
|
513
|
-
})
|
|
514
|
-
class S3Adapter extends Adapter<S3Options> {}
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
#### `@InjectPort(token)`
|
|
518
|
-
|
|
519
|
-
Parameter decorator for injecting a port token into service constructors.
|
|
520
|
-
|
|
521
|
-
**Example:**
|
|
522
|
-
```typescript
|
|
523
|
-
constructor(
|
|
524
|
-
@InjectPort(STORAGE_PORT)
|
|
525
|
-
private readonly storage: StoragePort,
|
|
526
|
-
) {}
|
|
274
|
+
FileModule.register({
|
|
275
|
+
adapter: MockStorageAdapter.register({})
|
|
276
|
+
})
|
|
277
|
+
]
|
|
278
|
+
}).compile()
|
|
527
279
|
```
|
|
528
280
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
#### `AdapterModule<TToken>`
|
|
532
|
-
|
|
533
|
-
A DynamicModule that carries compile-time proof it provides `TToken`.
|
|
534
|
-
|
|
535
|
-
```typescript
|
|
536
|
-
type AdapterModule<TToken> = DynamicModule & {
|
|
537
|
-
__provides: TToken;
|
|
538
|
-
};
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
## Best Practices
|
|
542
|
-
|
|
543
|
-
### โ
Do's
|
|
544
|
-
|
|
545
|
-
- **Export port tokens, not provider objects**
|
|
546
|
-
```typescript
|
|
547
|
-
exports: [STORAGE_PORT] // โ
Correct
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
- **Keep configuration in the app layer**
|
|
551
|
-
```typescript
|
|
552
|
-
// โ
Good: App provides config
|
|
553
|
-
S3Adapter.register({
|
|
554
|
-
bucket: process.env.S3_BUCKET,
|
|
555
|
-
})
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
- **Use `@InjectPort` for clarity**
|
|
559
|
-
```typescript
|
|
560
|
-
@InjectPort(STORAGE_PORT) // โ
Clear intent
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
- **Create small, focused adapters**
|
|
564
|
-
- One adapter = one infrastructure concern
|
|
565
|
-
|
|
566
|
-
### โ Don'ts
|
|
567
|
-
|
|
568
|
-
- **Don't export provider objects**
|
|
569
|
-
```typescript
|
|
570
|
-
exports: [{ provide: STORAGE_PORT, useExisting: S3Service }] // โ Wrong
|
|
571
|
-
```
|
|
281
|
+
## Documentation
|
|
572
282
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
class S3Adapter {
|
|
577
|
-
bucket = process.env.S3_BUCKET;
|
|
578
|
-
}
|
|
579
|
-
```
|
|
283
|
+
๐ **Complete Documentation:**
|
|
284
|
+
- **[Library Documentation](./docs/library.md)** - Full API reference, architecture guide, advanced patterns, and examples
|
|
285
|
+
- **[CLI Documentation](./docs/cli.md)** - Complete CLI reference, configuration, templates, and best practices
|
|
580
286
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
287
|
+
๐ **Quick Links:**
|
|
288
|
+
- [Core Concepts](./docs/library.md#core-concepts) - Understand ports, adapters, and services
|
|
289
|
+
- [Why Hexagonal Architecture?](./docs/library.md#why-hexagonal-architecture) - Benefits with code examples
|
|
290
|
+
- [Architecture Overview](./docs/library.md#architecture-overview) - Visual diagrams
|
|
291
|
+
- [API Reference](./docs/library.md#api-reference) - Complete API documentation
|
|
292
|
+
- [Testing Guide](./docs/library.md#testing) - Mock adapters and integration testing
|
|
293
|
+
- [Migration Guide](./docs/library.md#migration-guide) - Upgrading from @Port to @Adapter
|
|
584
294
|
|
|
585
295
|
## Examples
|
|
586
296
|
|
|
@@ -588,17 +298,16 @@ See the [`examples/`](./examples) directory for complete working examples:
|
|
|
588
298
|
|
|
589
299
|
- **Object Storage** - S3 adapter with file upload/download
|
|
590
300
|
- **Currency Rates** - HTTP API adapter with rate conversion
|
|
591
|
-
- **
|
|
592
|
-
|
|
593
|
-
## Documentation
|
|
594
|
-
|
|
595
|
-
- ๐ [Full Specification](./spec/spec.md) - Complete implementation guide with AWS S3 and HTTP API examples
|
|
596
|
-
- ๐ง [API Reference](#api-reference) - Detailed API documentation
|
|
301
|
+
- **Mock Patterns** - Testing with mock adapters
|
|
597
302
|
|
|
598
303
|
## License
|
|
599
304
|
|
|
600
|
-
MIT
|
|
305
|
+
MIT ยฉ [Your Name]
|
|
601
306
|
|
|
602
307
|
## Contributing
|
|
603
308
|
|
|
604
|
-
Contributions are welcome! Please
|
|
309
|
+
Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details on our code of conduct and development process.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
**Built with โค๏ธ for the NestJS community**
|
package/dist/src/cli/bin.js
CHANGED
|
@@ -1156,7 +1156,7 @@ __export(exports_base_generator, {
|
|
|
1156
1156
|
});
|
|
1157
1157
|
module.exports = __toCommonJS(exports_base_generator);
|
|
1158
1158
|
var import_node_path3 = require("node:path");
|
|
1159
|
-
var __dirname = "
|
|
1159
|
+
var __dirname = "/home/runner/work/nest-hex/nest-hex/src/cli/generators";
|
|
1160
1160
|
|
|
1161
1161
|
class BaseGenerator {
|
|
1162
1162
|
config;
|
|
@@ -1483,7 +1483,11 @@ function Confirm({
|
|
|
1483
1483
|
// src/cli/ui/components/FileProgress.tsx
|
|
1484
1484
|
var import_ink3 = require("ink");
|
|
1485
1485
|
var jsx_dev_runtime3 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1486
|
-
function FileProgress({
|
|
1486
|
+
function FileProgress({
|
|
1487
|
+
fileName,
|
|
1488
|
+
status,
|
|
1489
|
+
error
|
|
1490
|
+
}) {
|
|
1487
1491
|
const getStatusIcon = () => {
|
|
1488
1492
|
switch (status) {
|
|
1489
1493
|
case "completed":
|
|
@@ -1558,7 +1562,11 @@ function FileProgress({ fileName, status, error }) {
|
|
|
1558
1562
|
var import_ui3 = require("@inkjs/ui");
|
|
1559
1563
|
var import_ink4 = require("ink");
|
|
1560
1564
|
var jsx_dev_runtime4 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1561
|
-
function NameInput({
|
|
1565
|
+
function NameInput({
|
|
1566
|
+
type,
|
|
1567
|
+
step,
|
|
1568
|
+
onSubmit
|
|
1569
|
+
}) {
|
|
1562
1570
|
if (type === "full") {
|
|
1563
1571
|
const currentStep = step || "port";
|
|
1564
1572
|
const labels = {
|
|
@@ -1649,7 +1657,10 @@ function NameInput({ type, step, onSubmit }) {
|
|
|
1649
1657
|
var import_ui4 = require("@inkjs/ui");
|
|
1650
1658
|
var import_ink5 = require("ink");
|
|
1651
1659
|
var jsx_dev_runtime5 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1652
|
-
function PortSelector({
|
|
1660
|
+
function PortSelector({
|
|
1661
|
+
ports,
|
|
1662
|
+
onSubmit
|
|
1663
|
+
}) {
|
|
1653
1664
|
if (ports.length === 0) {
|
|
1654
1665
|
return /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(import_ink5.Box, {
|
|
1655
1666
|
flexDirection: "column",
|
|
@@ -1734,7 +1745,10 @@ function PortSelector({ ports, onSubmit }) {
|
|
|
1734
1745
|
var import_ui5 = require("@inkjs/ui");
|
|
1735
1746
|
var import_ink6 = require("ink");
|
|
1736
1747
|
var jsx_dev_runtime6 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1737
|
-
function ProgressIndicator({
|
|
1748
|
+
function ProgressIndicator({
|
|
1749
|
+
steps,
|
|
1750
|
+
title
|
|
1751
|
+
}) {
|
|
1738
1752
|
return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(import_ink6.Box, {
|
|
1739
1753
|
flexDirection: "column",
|
|
1740
1754
|
paddingY: 1,
|
|
@@ -1155,7 +1155,7 @@ __export(exports_base_generator, {
|
|
|
1155
1155
|
});
|
|
1156
1156
|
module.exports = __toCommonJS(exports_base_generator);
|
|
1157
1157
|
var import_node_path3 = require("node:path");
|
|
1158
|
-
var __dirname = "
|
|
1158
|
+
var __dirname = "/home/runner/work/nest-hex/nest-hex/src/cli/generators";
|
|
1159
1159
|
|
|
1160
1160
|
class BaseGenerator {
|
|
1161
1161
|
config;
|
|
@@ -1482,7 +1482,11 @@ function Confirm({
|
|
|
1482
1482
|
// src/cli/ui/components/FileProgress.tsx
|
|
1483
1483
|
var import_ink3 = require("ink");
|
|
1484
1484
|
var jsx_dev_runtime3 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1485
|
-
function FileProgress({
|
|
1485
|
+
function FileProgress({
|
|
1486
|
+
fileName,
|
|
1487
|
+
status,
|
|
1488
|
+
error
|
|
1489
|
+
}) {
|
|
1486
1490
|
const getStatusIcon = () => {
|
|
1487
1491
|
switch (status) {
|
|
1488
1492
|
case "completed":
|
|
@@ -1557,7 +1561,11 @@ function FileProgress({ fileName, status, error }) {
|
|
|
1557
1561
|
var import_ui3 = require("@inkjs/ui");
|
|
1558
1562
|
var import_ink4 = require("ink");
|
|
1559
1563
|
var jsx_dev_runtime4 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1560
|
-
function NameInput({
|
|
1564
|
+
function NameInput({
|
|
1565
|
+
type,
|
|
1566
|
+
step,
|
|
1567
|
+
onSubmit
|
|
1568
|
+
}) {
|
|
1561
1569
|
if (type === "full") {
|
|
1562
1570
|
const currentStep = step || "port";
|
|
1563
1571
|
const labels = {
|
|
@@ -1648,7 +1656,10 @@ function NameInput({ type, step, onSubmit }) {
|
|
|
1648
1656
|
var import_ui4 = require("@inkjs/ui");
|
|
1649
1657
|
var import_ink5 = require("ink");
|
|
1650
1658
|
var jsx_dev_runtime5 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1651
|
-
function PortSelector({
|
|
1659
|
+
function PortSelector({
|
|
1660
|
+
ports,
|
|
1661
|
+
onSubmit
|
|
1662
|
+
}) {
|
|
1652
1663
|
if (ports.length === 0) {
|
|
1653
1664
|
return /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(import_ink5.Box, {
|
|
1654
1665
|
flexDirection: "column",
|
|
@@ -1733,7 +1744,10 @@ function PortSelector({ ports, onSubmit }) {
|
|
|
1733
1744
|
var import_ui5 = require("@inkjs/ui");
|
|
1734
1745
|
var import_ink6 = require("ink");
|
|
1735
1746
|
var jsx_dev_runtime6 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1736
|
-
function ProgressIndicator({
|
|
1747
|
+
function ProgressIndicator({
|
|
1748
|
+
steps,
|
|
1749
|
+
title
|
|
1750
|
+
}) {
|
|
1737
1751
|
return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(import_ink6.Box, {
|
|
1738
1752
|
flexDirection: "column",
|
|
1739
1753
|
paddingY: 1,
|
|
@@ -237,7 +237,7 @@ __export(exports_base_generator, {
|
|
|
237
237
|
});
|
|
238
238
|
module.exports = __toCommonJS(exports_base_generator);
|
|
239
239
|
var import_node_path2 = require("node:path");
|
|
240
|
-
var __dirname = "
|
|
240
|
+
var __dirname = "/home/runner/work/nest-hex/nest-hex/src/cli/generators";
|
|
241
241
|
|
|
242
242
|
class BaseGenerator {
|
|
243
243
|
config;
|
|
@@ -237,7 +237,7 @@ __export(exports_base_generator, {
|
|
|
237
237
|
});
|
|
238
238
|
module.exports = __toCommonJS(exports_base_generator);
|
|
239
239
|
var import_node_path2 = require("node:path");
|
|
240
|
-
var __dirname = "
|
|
240
|
+
var __dirname = "/home/runner/work/nest-hex/nest-hex/src/cli/generators";
|
|
241
241
|
|
|
242
242
|
class BaseGenerator {
|
|
243
243
|
config;
|
|
@@ -193,6 +193,22 @@ declare abstract class BaseGenerator {
|
|
|
193
193
|
protected generateFiles(files: FileToGenerate[], dryRun?: boolean): Promise<string[]>;
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
|
+
* Generator for creating port files.
|
|
197
|
+
*
|
|
198
|
+
* Generates:
|
|
199
|
+
* - Port interface (domain contract)
|
|
200
|
+
* - Port token (dependency injection token)
|
|
201
|
+
* - Port service (optional - domain service using the port)
|
|
202
|
+
* - Port module (optional - feature module wrapper)
|
|
203
|
+
* - Index file (barrel exports)
|
|
204
|
+
*/
|
|
205
|
+
declare class PortGenerator extends BaseGenerator {
|
|
206
|
+
/**
|
|
207
|
+
* Generate all port files.
|
|
208
|
+
*/
|
|
209
|
+
generate(options: GeneratorOptions): Promise<GeneratorResult>;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
196
212
|
* Additional options for adapter generation.
|
|
197
213
|
*/
|
|
198
214
|
interface AdapterGeneratorOptions extends GeneratorOptions {
|
|
@@ -233,22 +249,6 @@ declare class AdapterGenerator extends BaseGenerator {
|
|
|
233
249
|
generate(options: AdapterGeneratorOptions): Promise<GeneratorResult>;
|
|
234
250
|
}
|
|
235
251
|
/**
|
|
236
|
-
* Generator for creating port files.
|
|
237
|
-
*
|
|
238
|
-
* Generates:
|
|
239
|
-
* - Port interface (domain contract)
|
|
240
|
-
* - Port token (dependency injection token)
|
|
241
|
-
* - Port service (optional - domain service using the port)
|
|
242
|
-
* - Port module (optional - feature module wrapper)
|
|
243
|
-
* - Index file (barrel exports)
|
|
244
|
-
*/
|
|
245
|
-
declare class PortGenerator extends BaseGenerator {
|
|
246
|
-
/**
|
|
247
|
-
* Generate all port files.
|
|
248
|
-
*/
|
|
249
|
-
generate(options: GeneratorOptions): Promise<GeneratorResult>;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
252
|
* Generator for creating standalone services.
|
|
253
253
|
*
|
|
254
254
|
* Generates:
|
|
@@ -237,7 +237,7 @@ __export(exports_base_generator, {
|
|
|
237
237
|
});
|
|
238
238
|
module.exports = __toCommonJS(exports_base_generator);
|
|
239
239
|
var import_node_path2 = require("node:path");
|
|
240
|
-
var __dirname = "
|
|
240
|
+
var __dirname = "/home/runner/work/nest-hex/nest-hex/src/cli/generators";
|
|
241
241
|
|
|
242
242
|
class BaseGenerator {
|
|
243
243
|
config;
|
|
@@ -237,7 +237,7 @@ __export(exports_base_generator, {
|
|
|
237
237
|
});
|
|
238
238
|
module.exports = __toCommonJS(exports_base_generator);
|
|
239
239
|
var import_node_path2 = require("node:path");
|
|
240
|
-
var __dirname = "
|
|
240
|
+
var __dirname = "/home/runner/work/nest-hex/nest-hex/src/cli/generators";
|
|
241
241
|
|
|
242
242
|
class BaseGenerator {
|
|
243
243
|
config;
|
|
@@ -237,7 +237,7 @@ __export(exports_base_generator, {
|
|
|
237
237
|
});
|
|
238
238
|
module.exports = __toCommonJS(exports_base_generator);
|
|
239
239
|
var import_node_path2 = require("node:path");
|
|
240
|
-
var __dirname = "
|
|
240
|
+
var __dirname = "/home/runner/work/nest-hex/nest-hex/src/cli/generators";
|
|
241
241
|
|
|
242
242
|
class BaseGenerator {
|
|
243
243
|
config;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
interface PortInfo {
|
|
2
|
+
/** Port name in kebab-case (e.g., 'object-storage') */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Port name in PascalCase (e.g., 'ObjectStorage') */
|
|
5
|
+
pascalName: string;
|
|
6
|
+
/** Token name (e.g., 'OBJECT_STORAGE_PORT') */
|
|
7
|
+
tokenName: string;
|
|
8
|
+
/** Relative import path from adapter to port token file */
|
|
9
|
+
tokenImportPath: string;
|
|
10
|
+
/** Absolute path to port directory */
|
|
11
|
+
portPath: string;
|
|
12
|
+
}
|
|
1
13
|
interface ComponentOption {
|
|
2
14
|
value: string;
|
|
3
15
|
label: string;
|
|
@@ -39,18 +51,6 @@ interface NameInputProps {
|
|
|
39
51
|
* Name input for component generation.
|
|
40
52
|
*/
|
|
41
53
|
declare function NameInput({ type, step, onSubmit }: NameInputProps): JSX.Element;
|
|
42
|
-
interface PortInfo {
|
|
43
|
-
/** Port name in kebab-case (e.g., 'object-storage') */
|
|
44
|
-
name: string;
|
|
45
|
-
/** Port name in PascalCase (e.g., 'ObjectStorage') */
|
|
46
|
-
pascalName: string;
|
|
47
|
-
/** Token name (e.g., 'OBJECT_STORAGE_PORT') */
|
|
48
|
-
tokenName: string;
|
|
49
|
-
/** Relative import path from adapter to port token file */
|
|
50
|
-
tokenImportPath: string;
|
|
51
|
-
/** Absolute path to port directory */
|
|
52
|
-
portPath: string;
|
|
53
|
-
}
|
|
54
54
|
interface PortSelectorProps {
|
|
55
55
|
ports: PortInfo[];
|
|
56
56
|
onSubmit: (portInfo: PortInfo) => void;
|
|
@@ -1037,7 +1037,11 @@ function Confirm({
|
|
|
1037
1037
|
// src/cli/ui/components/FileProgress.tsx
|
|
1038
1038
|
var import_ink3 = require("ink");
|
|
1039
1039
|
var jsx_dev_runtime3 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1040
|
-
function FileProgress({
|
|
1040
|
+
function FileProgress({
|
|
1041
|
+
fileName,
|
|
1042
|
+
status,
|
|
1043
|
+
error
|
|
1044
|
+
}) {
|
|
1041
1045
|
const getStatusIcon = () => {
|
|
1042
1046
|
switch (status) {
|
|
1043
1047
|
case "completed":
|
|
@@ -1112,7 +1116,11 @@ function FileProgress({ fileName, status, error }) {
|
|
|
1112
1116
|
var import_ui3 = require("@inkjs/ui");
|
|
1113
1117
|
var import_ink4 = require("ink");
|
|
1114
1118
|
var jsx_dev_runtime4 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1115
|
-
function NameInput({
|
|
1119
|
+
function NameInput({
|
|
1120
|
+
type,
|
|
1121
|
+
step,
|
|
1122
|
+
onSubmit
|
|
1123
|
+
}) {
|
|
1116
1124
|
if (type === "full") {
|
|
1117
1125
|
const currentStep = step || "port";
|
|
1118
1126
|
const labels = {
|
|
@@ -1203,7 +1211,10 @@ function NameInput({ type, step, onSubmit }) {
|
|
|
1203
1211
|
var import_ui4 = require("@inkjs/ui");
|
|
1204
1212
|
var import_ink5 = require("ink");
|
|
1205
1213
|
var jsx_dev_runtime5 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1206
|
-
function PortSelector({
|
|
1214
|
+
function PortSelector({
|
|
1215
|
+
ports,
|
|
1216
|
+
onSubmit
|
|
1217
|
+
}) {
|
|
1207
1218
|
if (ports.length === 0) {
|
|
1208
1219
|
return /* @__PURE__ */ jsx_dev_runtime5.jsxDEV(import_ink5.Box, {
|
|
1209
1220
|
flexDirection: "column",
|
|
@@ -1288,7 +1299,10 @@ function PortSelector({ ports, onSubmit }) {
|
|
|
1288
1299
|
var import_ui5 = require("@inkjs/ui");
|
|
1289
1300
|
var import_ink6 = require("ink");
|
|
1290
1301
|
var jsx_dev_runtime6 = __toESM(require_jsx_dev_runtime(), 1);
|
|
1291
|
-
function ProgressIndicator({
|
|
1302
|
+
function ProgressIndicator({
|
|
1303
|
+
steps,
|
|
1304
|
+
title
|
|
1305
|
+
}) {
|
|
1292
1306
|
return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(import_ink6.Box, {
|
|
1293
1307
|
flexDirection: "column",
|
|
1294
1308
|
paddingY: 1,
|
package/dist/src/index.d.ts
CHANGED
|
@@ -30,31 +30,43 @@ type PortConfig<
|
|
|
30
30
|
*
|
|
31
31
|
* Adapters are dynamic modules that provide a port token and hide infrastructure details.
|
|
32
32
|
* This base class automatically handles provider registration, token aliasing, and exports
|
|
33
|
-
* by reading metadata from the @
|
|
33
|
+
* by reading metadata from the @Adapter decorator.
|
|
34
34
|
*
|
|
35
35
|
* @template TOptions - The options type for configuring this adapter
|
|
36
36
|
*
|
|
37
37
|
* @example
|
|
38
|
+
* Basic adapter with decorator options:
|
|
38
39
|
* ```typescript
|
|
39
|
-
* @
|
|
40
|
-
*
|
|
40
|
+
* @Adapter({
|
|
41
|
+
* portToken: STORAGE_PORT,
|
|
42
|
+
* implementation: S3Service,
|
|
43
|
+
* imports: [HttpModule],
|
|
44
|
+
* providers: [{ provide: 'CONFIG', useValue: {...} }]
|
|
45
|
+
* })
|
|
46
|
+
* class S3Adapter extends AdapterBase<S3Options> {}
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* Advanced adapter with dynamic configuration:
|
|
50
|
+
* ```typescript
|
|
51
|
+
* @Adapter({
|
|
52
|
+
* portToken: STORAGE_PORT,
|
|
41
53
|
* implementation: S3Service
|
|
42
54
|
* })
|
|
43
|
-
* class S3Adapter extends
|
|
44
|
-
* protected
|
|
45
|
-
* return [
|
|
55
|
+
* class S3Adapter extends AdapterBase<S3Options> {
|
|
56
|
+
* protected imports(options: S3Options) {
|
|
57
|
+
* return [HttpModule.register({ timeout: options.timeout })]
|
|
46
58
|
* }
|
|
47
59
|
*
|
|
48
|
-
* protected
|
|
49
|
-
* return [
|
|
60
|
+
* protected extraProviders(options: S3Options) {
|
|
61
|
+
* return [{ provide: 'S3_CONFIG', useValue: options }]
|
|
50
62
|
* }
|
|
51
63
|
* }
|
|
52
64
|
* ```
|
|
53
65
|
*/
|
|
54
|
-
declare class
|
|
66
|
+
declare class AdapterBase<TOptions> {
|
|
55
67
|
/**
|
|
56
68
|
* Optional hook to import other NestJS modules.
|
|
57
|
-
* Override this method to add module dependencies.
|
|
69
|
+
* Override this method to add module dependencies based on options.
|
|
58
70
|
*
|
|
59
71
|
* @param _options - The adapter configuration options
|
|
60
72
|
* @returns Array of modules to import
|
|
@@ -62,31 +74,31 @@ declare class Adapter<TOptions> {
|
|
|
62
74
|
protected imports(_options?: TOptions): unknown[];
|
|
63
75
|
/**
|
|
64
76
|
* Optional hook to provide additional providers.
|
|
65
|
-
* Override this method to add helper services, factories, or initialization logic.
|
|
77
|
+
* Override this method to add helper services, factories, or initialization logic based on options.
|
|
66
78
|
*
|
|
67
79
|
* @param _options - The adapter configuration options
|
|
68
80
|
* @returns Array of additional providers
|
|
69
81
|
*/
|
|
70
|
-
protected
|
|
82
|
+
protected extraProviders(_options: TOptions): Provider[];
|
|
71
83
|
/**
|
|
72
84
|
* Synchronous registration method.
|
|
73
85
|
* Creates a dynamic module with the adapter's port token and implementation.
|
|
74
86
|
*
|
|
75
87
|
* @param options - The adapter configuration options
|
|
76
88
|
* @returns An AdapterModule with compile-time token proof
|
|
77
|
-
* @throws Error if @
|
|
89
|
+
* @throws Error if @Adapter decorator is missing or incomplete
|
|
78
90
|
*/
|
|
79
91
|
static register<
|
|
80
92
|
TToken,
|
|
81
93
|
TOptions
|
|
82
|
-
>(this: new () =>
|
|
94
|
+
>(this: new () => AdapterBase<TOptions>, options: TOptions): AdapterModule<TToken>;
|
|
83
95
|
/**
|
|
84
96
|
* Asynchronous registration method with dependency injection support.
|
|
85
97
|
* Creates a dynamic module where options are resolved via DI.
|
|
86
98
|
*
|
|
87
99
|
* @param options - Async configuration with factory, imports, and inject
|
|
88
100
|
* @returns An AdapterModule with compile-time token proof
|
|
89
|
-
* @throws Error if @
|
|
101
|
+
* @throws Error if @Adapter decorator is missing or incomplete
|
|
90
102
|
*
|
|
91
103
|
* @example
|
|
92
104
|
* ```typescript
|
|
@@ -100,35 +112,53 @@ declare class Adapter<TOptions> {
|
|
|
100
112
|
static registerAsync<
|
|
101
113
|
TToken,
|
|
102
114
|
TOptions
|
|
103
|
-
>(this: new () =>
|
|
115
|
+
>(this: new () => AdapterBase<TOptions>, options: {
|
|
104
116
|
imports?: unknown[];
|
|
105
117
|
inject?: unknown[];
|
|
106
118
|
useFactory: (...args: unknown[]) => TOptions | Promise<TOptions>;
|
|
107
119
|
}): AdapterModule<TToken>;
|
|
108
120
|
}
|
|
109
|
-
import { Type } from "@nestjs/common";
|
|
121
|
+
import { DynamicModule as DynamicModule2, Provider as Provider2, Type } from "@nestjs/common";
|
|
110
122
|
/**
|
|
111
|
-
* Declares the
|
|
123
|
+
* Declares the adapter configuration (port token, implementation, imports, and extra providers).
|
|
112
124
|
*
|
|
113
|
-
* This decorator stores
|
|
114
|
-
* which is read at runtime by the Adapter base
|
|
125
|
+
* This decorator stores the port token, implementation class, optional imports, and
|
|
126
|
+
* optional extra providers in metadata, which is read at runtime by the Adapter base
|
|
127
|
+
* class's register() and registerAsync() methods.
|
|
115
128
|
*
|
|
116
|
-
* @param config -
|
|
117
|
-
* @param config.
|
|
129
|
+
* @param config - Adapter configuration object
|
|
130
|
+
* @param config.portToken - The port token this adapter provides
|
|
118
131
|
* @param config.implementation - The concrete implementation class that provides the port functionality
|
|
132
|
+
* @param config.imports - Optional array of NestJS modules to import (module classes or DynamicModule objects)
|
|
133
|
+
* @param config.providers - Optional array of additional providers to register
|
|
119
134
|
*
|
|
120
135
|
* @example
|
|
136
|
+
* Basic adapter:
|
|
121
137
|
* ```typescript
|
|
122
|
-
* @
|
|
123
|
-
*
|
|
138
|
+
* @Adapter({
|
|
139
|
+
* portToken: OBJECT_STORAGE_PORT,
|
|
124
140
|
* implementation: S3ObjectStorageService
|
|
125
141
|
* })
|
|
126
|
-
* class S3Adapter extends
|
|
142
|
+
* class S3Adapter extends AdapterBase<S3Options> {}
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* Adapter with imports and extra providers:
|
|
147
|
+
* ```typescript
|
|
148
|
+
* @Adapter({
|
|
149
|
+
* portToken: HTTP_CLIENT_PORT,
|
|
150
|
+
* implementation: AxiosHttpClient,
|
|
151
|
+
* imports: [HttpModule],
|
|
152
|
+
* providers: [{ provide: 'HTTP_CONFIG', useValue: { timeout: 5000 } }]
|
|
153
|
+
* })
|
|
154
|
+
* class AxiosAdapter extends AdapterBase<AxiosOptions> {}
|
|
127
155
|
* ```
|
|
128
156
|
*/
|
|
129
|
-
declare function
|
|
130
|
-
|
|
157
|
+
declare function Adapter<C extends PortConfig<any, any>>(config: {
|
|
158
|
+
portToken: C["token"];
|
|
131
159
|
implementation: Type<C["port"]>;
|
|
160
|
+
imports?: Array<Type<any> | DynamicModule2 | Promise<DynamicModule2>>;
|
|
161
|
+
providers?: Provider2[];
|
|
132
162
|
}): ClassDecorator;
|
|
133
163
|
/**
|
|
134
164
|
* DX decorator for injecting a port token into service constructors.
|
|
@@ -148,7 +178,7 @@ declare function Port2<C extends PortConfig<any, any>>(config: {
|
|
|
148
178
|
* ```
|
|
149
179
|
*/
|
|
150
180
|
declare function InjectPort<TToken>(token: TToken): ParameterDecorator;
|
|
151
|
-
import { DynamicModule as
|
|
181
|
+
import { DynamicModule as DynamicModule3 } from "@nestjs/common";
|
|
152
182
|
declare class PortModule<_TService> {
|
|
153
183
|
/**
|
|
154
184
|
* Registers the port module with an adapter.
|
|
@@ -159,6 +189,6 @@ declare class PortModule<_TService> {
|
|
|
159
189
|
*/
|
|
160
190
|
static register<TToken>({ adapter }: {
|
|
161
191
|
adapter?: AdapterModule<TToken>;
|
|
162
|
-
}):
|
|
192
|
+
}): DynamicModule3;
|
|
163
193
|
}
|
|
164
|
-
export { PortModule, PortConfig,
|
|
194
|
+
export { PortModule, PortConfig, InjectPort, AdapterModule, AdapterBase, Adapter };
|
package/dist/src/index.js
CHANGED
|
@@ -55,8 +55,8 @@ var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
|
55
55
|
var exports_src = {};
|
|
56
56
|
__export(exports_src, {
|
|
57
57
|
PortModule: () => PortModule,
|
|
58
|
-
Port: () => Port,
|
|
59
58
|
InjectPort: () => InjectPort,
|
|
59
|
+
AdapterBase: () => AdapterBase,
|
|
60
60
|
Adapter: () => Adapter
|
|
61
61
|
});
|
|
62
62
|
module.exports = __toCommonJS(exports_src);
|
|
@@ -67,32 +67,37 @@ var import_reflect_metadata = require("reflect-metadata");
|
|
|
67
67
|
// src/core/constants.ts
|
|
68
68
|
var PORT_TOKEN_METADATA = Symbol("PORT_TOKEN_METADATA");
|
|
69
69
|
var PORT_IMPLEMENTATION_METADATA = Symbol("PORT_IMPLEMENTATION_METADATA");
|
|
70
|
+
var ADAPTER_IMPORTS_METADATA = Symbol("ADAPTER_IMPORTS_METADATA");
|
|
71
|
+
var ADAPTER_PROVIDERS_METADATA = Symbol("ADAPTER_PROVIDERS_METADATA");
|
|
70
72
|
|
|
71
73
|
// src/core/adapter.base.ts
|
|
72
|
-
class
|
|
74
|
+
class AdapterBase {
|
|
73
75
|
imports(_options) {
|
|
74
76
|
return [];
|
|
75
77
|
}
|
|
76
|
-
|
|
78
|
+
extraProviders(_options) {
|
|
77
79
|
return [];
|
|
78
80
|
}
|
|
79
81
|
static register(options) {
|
|
80
82
|
const instance = new this;
|
|
81
83
|
const token = Reflect.getMetadata(PORT_TOKEN_METADATA, this);
|
|
82
84
|
const implementation = Reflect.getMetadata(PORT_IMPLEMENTATION_METADATA, this);
|
|
85
|
+
const decoratorImports = Reflect.getMetadata(ADAPTER_IMPORTS_METADATA, this) ?? [];
|
|
86
|
+
const decoratorProviders = Reflect.getMetadata(ADAPTER_PROVIDERS_METADATA, this) ?? [];
|
|
83
87
|
if (!token) {
|
|
84
|
-
throw new Error(`${this.name} must be decorated with @
|
|
88
|
+
throw new Error(`${this.name} must be decorated with @Adapter() and specify 'portToken'`);
|
|
85
89
|
}
|
|
86
90
|
if (!implementation) {
|
|
87
|
-
throw new Error(`${this.name} must be decorated with @
|
|
91
|
+
throw new Error(`${this.name} must be decorated with @Adapter() and specify 'implementation'`);
|
|
88
92
|
}
|
|
89
93
|
return {
|
|
90
94
|
module: this,
|
|
91
|
-
imports: instance.imports(options),
|
|
95
|
+
imports: [...decoratorImports, ...instance.imports(options)],
|
|
92
96
|
providers: [
|
|
93
97
|
implementation,
|
|
94
98
|
{ provide: token, useExisting: implementation },
|
|
95
|
-
...
|
|
99
|
+
...decoratorProviders,
|
|
100
|
+
...instance.extraProviders(options)
|
|
96
101
|
],
|
|
97
102
|
exports: [token],
|
|
98
103
|
__provides: token
|
|
@@ -102,19 +107,26 @@ class Adapter {
|
|
|
102
107
|
const instance = new this;
|
|
103
108
|
const token = Reflect.getMetadata(PORT_TOKEN_METADATA, this);
|
|
104
109
|
const implementation = Reflect.getMetadata(PORT_IMPLEMENTATION_METADATA, this);
|
|
110
|
+
const decoratorImports = Reflect.getMetadata(ADAPTER_IMPORTS_METADATA, this) ?? [];
|
|
111
|
+
const decoratorProviders = Reflect.getMetadata(ADAPTER_PROVIDERS_METADATA, this) ?? [];
|
|
105
112
|
if (!token) {
|
|
106
|
-
throw new Error(`${this.name} must be decorated with @
|
|
113
|
+
throw new Error(`${this.name} must be decorated with @Adapter() and specify 'portToken'`);
|
|
107
114
|
}
|
|
108
115
|
if (!implementation) {
|
|
109
|
-
throw new Error(`${this.name} must be decorated with @
|
|
116
|
+
throw new Error(`${this.name} must be decorated with @Adapter() and specify 'implementation'`);
|
|
110
117
|
}
|
|
111
118
|
return {
|
|
112
119
|
module: this,
|
|
113
|
-
imports: [
|
|
120
|
+
imports: [
|
|
121
|
+
...decoratorImports,
|
|
122
|
+
...instance.imports(),
|
|
123
|
+
...options.imports ?? []
|
|
124
|
+
],
|
|
114
125
|
providers: [
|
|
115
126
|
implementation,
|
|
116
127
|
{ provide: token, useExisting: implementation },
|
|
117
|
-
...
|
|
128
|
+
...decoratorProviders,
|
|
129
|
+
...instance.extraProviders({})
|
|
118
130
|
],
|
|
119
131
|
exports: [token],
|
|
120
132
|
__provides: token
|
|
@@ -124,10 +136,16 @@ class Adapter {
|
|
|
124
136
|
// src/core/decorators.ts
|
|
125
137
|
var import_reflect_metadata2 = require("reflect-metadata");
|
|
126
138
|
var import_common = require("@nestjs/common");
|
|
127
|
-
function
|
|
139
|
+
function Adapter(config) {
|
|
128
140
|
return (target) => {
|
|
129
|
-
Reflect.defineMetadata(PORT_TOKEN_METADATA, config.
|
|
141
|
+
Reflect.defineMetadata(PORT_TOKEN_METADATA, config.portToken, target);
|
|
130
142
|
Reflect.defineMetadata(PORT_IMPLEMENTATION_METADATA, config.implementation, target);
|
|
143
|
+
if (config.imports) {
|
|
144
|
+
Reflect.defineMetadata(ADAPTER_IMPORTS_METADATA, config.imports, target);
|
|
145
|
+
}
|
|
146
|
+
if (config.providers) {
|
|
147
|
+
Reflect.defineMetadata(ADAPTER_PROVIDERS_METADATA, config.providers, target);
|
|
148
|
+
}
|
|
131
149
|
};
|
|
132
150
|
}
|
|
133
151
|
function InjectPort(token) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nest-hex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A tiny NestJS-native library for building pluggable adapters (Ports & Adapters / Hexagonal) using class-based Dynamic Modules, with great DX and strong type safety.",
|
|
5
5
|
"homepage": "https://github.com/LiorVainer/nest-hex#readme",
|
|
6
6
|
"bugs": {
|