nest-hex 0.2.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/LICENSE +21 -0
- package/README.md +425 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +137 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 LiorVainer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# nest-hex
|
|
2
|
+
|
|
3
|
+
> A tiny, **class-based**, **NestJS-native** helper library for building **pluggable adapters** following the Ports & Adapters (Hexagonal Architecture) pattern with minimal boilerplate and great developer experience.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
Building NestJS applications with the Ports & Adapters pattern involves repetitive boilerplate:
|
|
10
|
+
|
|
11
|
+
- Registering concrete implementation classes
|
|
12
|
+
- Aliasing port tokens to implementations (`useExisting`)
|
|
13
|
+
- Exporting only the port token (not provider objects)
|
|
14
|
+
- Supporting both `register()` and `registerAsync()` patterns
|
|
15
|
+
- Keeping the app responsible for configuration (no `process.env` in libraries)
|
|
16
|
+
|
|
17
|
+
This library provides base classes and decorators to eliminate this boilerplate while maintaining compile-time type safety.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- ๐ฏ **Declarative**: Declare port tokens and implementations once using `@Port({ token, implementation })`
|
|
22
|
+
- ๐๏ธ **Class-based**: Use standard NestJS dynamic modules, no function factories required
|
|
23
|
+
- ๐ **Type-safe**: `AdapterModule<TToken>` carries compile-time proof of which token it provides
|
|
24
|
+
- โก **Zero runtime overhead**: Uses TypeScript decorators and metadata, minimal abstraction
|
|
25
|
+
- ๐ฆ **Tiny**: Core library is under 1KB minified
|
|
26
|
+
- ๐งช **Testable**: Easily mock adapters for testing
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install nest-hex
|
|
32
|
+
# or
|
|
33
|
+
yarn add nest-hex
|
|
34
|
+
# or
|
|
35
|
+
pnpm add nest-hex
|
|
36
|
+
# or
|
|
37
|
+
bun add nest-hex
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Peer Dependencies
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @nestjs/common @nestjs/core reflect-metadata
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
### 1. Define a Port (Domain Interface)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// storage.port.ts
|
|
52
|
+
export const STORAGE_PORT = Symbol('STORAGE_PORT');
|
|
53
|
+
|
|
54
|
+
export interface StoragePort {
|
|
55
|
+
upload(file: Buffer, key: string): Promise<{ url: string }>;
|
|
56
|
+
download(key: string): Promise<Buffer>;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Create an Adapter (Infrastructure Implementation)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// s3.adapter.ts
|
|
64
|
+
import { Injectable } from '@nestjs/common';
|
|
65
|
+
import { Adapter, Port } from 'nest-hex';
|
|
66
|
+
import { STORAGE_PORT, type StoragePort } from './storage.port';
|
|
67
|
+
|
|
68
|
+
// Implementation service
|
|
69
|
+
@Injectable()
|
|
70
|
+
class S3StorageService implements StoragePort {
|
|
71
|
+
async upload(file: Buffer, key: string) {
|
|
72
|
+
// AWS S3 upload logic here
|
|
73
|
+
return { url: `https://s3.amazonaws.com/bucket/${key}` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async download(key: string) {
|
|
77
|
+
// AWS S3 download logic here
|
|
78
|
+
return Buffer.from('file contents');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Adapter configuration
|
|
83
|
+
interface S3Options {
|
|
84
|
+
bucket: string;
|
|
85
|
+
region: string;
|
|
86
|
+
accessKeyId?: string;
|
|
87
|
+
secretAccessKey?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Adapter module - single decorator declares everything!
|
|
91
|
+
@Port({
|
|
92
|
+
token: STORAGE_PORT,
|
|
93
|
+
implementation: S3StorageService,
|
|
94
|
+
})
|
|
95
|
+
export class S3Adapter extends Adapter<S3Options> {}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Create a Port Module (Domain Service)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// storage.module.ts
|
|
102
|
+
import { Injectable, Module } from '@nestjs/common';
|
|
103
|
+
import { InjectPort, PortModule } from 'nest-hex';
|
|
104
|
+
import { STORAGE_PORT, type StoragePort } from './storage.port';
|
|
105
|
+
|
|
106
|
+
// Domain service that uses the port
|
|
107
|
+
@Injectable()
|
|
108
|
+
export class StorageService {
|
|
109
|
+
constructor(
|
|
110
|
+
@InjectPort(STORAGE_PORT)
|
|
111
|
+
private readonly storage: StoragePort,
|
|
112
|
+
) {}
|
|
113
|
+
|
|
114
|
+
async uploadUserAvatar(userId: string, image: Buffer) {
|
|
115
|
+
const key = `avatars/${userId}.jpg`;
|
|
116
|
+
return this.storage.upload(image, key);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async downloadUserAvatar(userId: string) {
|
|
120
|
+
const key = `avatars/${userId}.jpg`;
|
|
121
|
+
return this.storage.download(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Port module that accepts any adapter
|
|
126
|
+
@Module({})
|
|
127
|
+
export class StorageModule extends PortModule<typeof StorageService> {}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 4. Wire It Up in Your App
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// app.module.ts
|
|
134
|
+
import { Module } from '@nestjs/common';
|
|
135
|
+
import { StorageModule } from './storage/storage.module';
|
|
136
|
+
import S3Adapter from './storage/adapters/s3.adapter';
|
|
137
|
+
|
|
138
|
+
@Module({
|
|
139
|
+
imports: [
|
|
140
|
+
StorageModule.register({
|
|
141
|
+
adapter: S3Adapter.register({
|
|
142
|
+
bucket: 'my-app-uploads',
|
|
143
|
+
region: 'us-east-1',
|
|
144
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
145
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
146
|
+
}),
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
})
|
|
150
|
+
export class AppModule {}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
That's it! You now have a fully type-safe, pluggable storage adapter. ๐
|
|
154
|
+
|
|
155
|
+
## Key Benefits
|
|
156
|
+
|
|
157
|
+
### Before (Manual Boilerplate)
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// Lots of manual wiring...
|
|
161
|
+
@Module({})
|
|
162
|
+
export class S3StorageModule {
|
|
163
|
+
static register(options: S3Options): DynamicModule {
|
|
164
|
+
return {
|
|
165
|
+
module: S3StorageModule,
|
|
166
|
+
providers: [
|
|
167
|
+
S3StorageService,
|
|
168
|
+
{ provide: STORAGE_PORT, useExisting: S3StorageService },
|
|
169
|
+
// More boilerplate...
|
|
170
|
+
],
|
|
171
|
+
exports: [STORAGE_PORT],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### After (With nest-hex)
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Clean and declarative!
|
|
181
|
+
@Port({
|
|
182
|
+
token: STORAGE_PORT,
|
|
183
|
+
implementation: S3StorageService,
|
|
184
|
+
})
|
|
185
|
+
export class S3Adapter extends Adapter<S3Options> {}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Advanced Usage
|
|
189
|
+
|
|
190
|
+
### Async Registration with Dependency Injection
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
194
|
+
|
|
195
|
+
@Module({
|
|
196
|
+
imports: [
|
|
197
|
+
StorageModule.register({
|
|
198
|
+
adapter: S3Adapter.registerAsync({
|
|
199
|
+
imports: [ConfigModule],
|
|
200
|
+
inject: [ConfigService],
|
|
201
|
+
useFactory: (config: ConfigService) => ({
|
|
202
|
+
bucket: config.get('S3_BUCKET'),
|
|
203
|
+
region: config.get('AWS_REGION'),
|
|
204
|
+
accessKeyId: config.get('AWS_ACCESS_KEY_ID'),
|
|
205
|
+
secretAccessKey: config.get('AWS_SECRET_ACCESS_KEY'),
|
|
206
|
+
}),
|
|
207
|
+
}),
|
|
208
|
+
}),
|
|
209
|
+
],
|
|
210
|
+
})
|
|
211
|
+
export class AppModule {}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Custom Imports and Extra Ports
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
@Port({
|
|
218
|
+
token: HTTP_CLIENT_PORT,
|
|
219
|
+
implementation: AxiosHttpClient,
|
|
220
|
+
})
|
|
221
|
+
class AxiosAdapterClass extends Adapter<AxiosOptions> {
|
|
222
|
+
protected override imports(options: AxiosOptions) {
|
|
223
|
+
return [
|
|
224
|
+
HttpModule.register({
|
|
225
|
+
baseURL: options.baseUrl,
|
|
226
|
+
timeout: options.timeout,
|
|
227
|
+
}),
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
protected override extraPoviders(options: AxiosOptions) {
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
provide: 'HTTP_CLIENT_CONFIG',
|
|
235
|
+
useValue: options,
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Swapping Adapters
|
|
243
|
+
|
|
244
|
+
The beauty of the Ports & Adapters pattern is that you can easily swap implementations:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// Development: Use filesystem storage
|
|
248
|
+
import FilesystemAdapter from './storage/adapters/filesystem.adapter';
|
|
249
|
+
|
|
250
|
+
// Production: Use AWS S3
|
|
251
|
+
import S3Adapter from './storage/adapters/s3.adapter';
|
|
252
|
+
|
|
253
|
+
const adapter = process.env.NODE_ENV === 'production'
|
|
254
|
+
? S3Adapter.register({ bucket: 'prod-bucket', region: 'us-east-1' })
|
|
255
|
+
: FilesystemAdapter.register({ basePath: './uploads' });
|
|
256
|
+
|
|
257
|
+
@Module({
|
|
258
|
+
imports: [
|
|
259
|
+
StorageModule.register({ adapter }),
|
|
260
|
+
],
|
|
261
|
+
})
|
|
262
|
+
export class AppModule {}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Testing with Mock Adapters
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { Adapter, Port } from 'nest-hex';
|
|
269
|
+
|
|
270
|
+
class MockStorageService {
|
|
271
|
+
async upload(file: Buffer, key: string) {
|
|
272
|
+
return { url: `mock://storage/${key}` };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async download(key: string) {
|
|
276
|
+
return Buffer.from('mock file contents');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@Port({
|
|
281
|
+
token: STORAGE_PORT,
|
|
282
|
+
implementation: MockStorageService,
|
|
283
|
+
})
|
|
284
|
+
export class MockStorageAdapter extends Adapter<void> {}
|
|
285
|
+
|
|
286
|
+
// Use in tests
|
|
287
|
+
const module = await Test.createTestingModule({
|
|
288
|
+
imports: [
|
|
289
|
+
StorageModule.register({
|
|
290
|
+
adapter: MockStorageAdapter.register(undefined),
|
|
291
|
+
}),
|
|
292
|
+
],
|
|
293
|
+
}).compile();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## API Reference
|
|
297
|
+
|
|
298
|
+
### Core Classes
|
|
299
|
+
|
|
300
|
+
#### `Adapter<TOptions>`
|
|
301
|
+
|
|
302
|
+
Abstract base class for building adapter modules.
|
|
303
|
+
|
|
304
|
+
**Methods:**
|
|
305
|
+
- `static register<TToken, TOptions>(options: TOptions): AdapterModule<TToken>` - Synchronous registration
|
|
306
|
+
- `static registerAsync<TToken, TOptions>(config: AsyncConfig): AdapterModule<TToken>` - Async registration with DI
|
|
307
|
+
|
|
308
|
+
**Protected Hooks:**
|
|
309
|
+
- `protected imports(options?: TOptions): unknown[]` - Override to import other NestJS modules
|
|
310
|
+
- `protected extraPoviders(options: TOptions): Port[]` - Override to add additional providers
|
|
311
|
+
|
|
312
|
+
#### `PortModule<TService>`
|
|
313
|
+
|
|
314
|
+
Abstract base class for building port modules that consume adapters.
|
|
315
|
+
|
|
316
|
+
**Methods:**
|
|
317
|
+
- `static register<TToken>({ adapter }: { adapter?: AdapterModule<TToken> }): DynamicModule`
|
|
318
|
+
|
|
319
|
+
### Decorators
|
|
320
|
+
|
|
321
|
+
#### `@Port({ token, implementation })`
|
|
322
|
+
|
|
323
|
+
Class decorator that declares which port token an adapter provides and its implementation class.
|
|
324
|
+
|
|
325
|
+
**Parameters:**
|
|
326
|
+
- `token: TToken` - The port token (usually a Symbol)
|
|
327
|
+
- `implementation: Type<unknown>` - The concrete implementation class
|
|
328
|
+
|
|
329
|
+
**Example:**
|
|
330
|
+
```typescript
|
|
331
|
+
@Port({
|
|
332
|
+
token: STORAGE_PORT,
|
|
333
|
+
implementation: S3StorageService,
|
|
334
|
+
})
|
|
335
|
+
class S3Adapter extends Adapter<S3Options> {}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### `@InjectPort(token)`
|
|
339
|
+
|
|
340
|
+
Parameter decorator for injecting a port token into service constructors.
|
|
341
|
+
|
|
342
|
+
**Example:**
|
|
343
|
+
```typescript
|
|
344
|
+
constructor(
|
|
345
|
+
@InjectPort(STORAGE_PORT)
|
|
346
|
+
private readonly storage: StoragePort,
|
|
347
|
+
) {}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Types
|
|
351
|
+
|
|
352
|
+
#### `AdapterModule<TToken>`
|
|
353
|
+
|
|
354
|
+
A DynamicModule that carries compile-time proof it provides `TToken`.
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
type AdapterModule<TToken> = DynamicModule & {
|
|
358
|
+
__provides: TToken;
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Best Practices
|
|
363
|
+
|
|
364
|
+
### โ
Do's
|
|
365
|
+
|
|
366
|
+
- **Export port tokens, not provider objects**
|
|
367
|
+
```typescript
|
|
368
|
+
exports: [STORAGE_PORT] // โ
Correct
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- **Keep configuration in the app layer**
|
|
372
|
+
```typescript
|
|
373
|
+
// โ
Good: App provides config
|
|
374
|
+
S3Adapter.register({
|
|
375
|
+
bucket: process.env.S3_BUCKET,
|
|
376
|
+
})
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
- **Use `@InjectPort` for clarity**
|
|
380
|
+
```typescript
|
|
381
|
+
@InjectPort(STORAGE_PORT) // โ
Clear intent
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
- **Create small, focused adapters**
|
|
385
|
+
- One adapter = one infrastructure concern
|
|
386
|
+
|
|
387
|
+
### โ Don'ts
|
|
388
|
+
|
|
389
|
+
- **Don't export provider objects**
|
|
390
|
+
```typescript
|
|
391
|
+
exports: [{ provide: STORAGE_PORT, useExisting: S3Service }] // โ Wrong
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- **Don't use `process.env` in adapters**
|
|
395
|
+
```typescript
|
|
396
|
+
// โ Bad: Config hard-coded in adapter
|
|
397
|
+
class S3Adapter {
|
|
398
|
+
bucket = process.env.S3_BUCKET;
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- **Don't mix domain logic with adapters**
|
|
403
|
+
- Adapters = infrastructure only
|
|
404
|
+
- Domain logic = port modules/services
|
|
405
|
+
|
|
406
|
+
## Examples
|
|
407
|
+
|
|
408
|
+
See the [`examples/`](./examples) directory for complete working examples:
|
|
409
|
+
|
|
410
|
+
- **Object Storage** - S3 adapter with file upload/download
|
|
411
|
+
- **Currency Rates** - HTTP API adapter with rate conversion
|
|
412
|
+
- **Basic Examples** - Decorator usage patterns
|
|
413
|
+
|
|
414
|
+
## Documentation
|
|
415
|
+
|
|
416
|
+
- ๐ [Full Specification](./spec/spec.md) - Complete implementation guide with AWS S3 and HTTP API examples
|
|
417
|
+
- ๐ง [API Reference](#api-reference) - Detailed API documentation
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
MIT
|
|
422
|
+
|
|
423
|
+
## Contributing
|
|
424
|
+
|
|
425
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Provider } from "@nestjs/common";
|
|
2
|
+
import { DynamicModule } from "@nestjs/common";
|
|
3
|
+
/**
|
|
4
|
+
* A DynamicModule that carries a compile-time proof it provides `TToken`.
|
|
5
|
+
*
|
|
6
|
+
* The `__provides` field is used only for TypeScript structural typing to ensure
|
|
7
|
+
* that feature modules receive adapters that provide the correct token type.
|
|
8
|
+
* This enables compile-time type safety without runtime overhead.
|
|
9
|
+
*
|
|
10
|
+
* @template TToken - The token type that this adapter module provides
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const s3Module: AdapterModule<typeof STORAGE_TOKEN> = S3Adapter.register(options);
|
|
15
|
+
* // TypeScript ensures s3Module provides STORAGE_TOKEN
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
type AdapterModule<TToken> = DynamicModule & {
|
|
19
|
+
__provides: TToken;
|
|
20
|
+
};
|
|
21
|
+
type PortConfig<
|
|
22
|
+
Token,
|
|
23
|
+
Port
|
|
24
|
+
> = {
|
|
25
|
+
token: Token;
|
|
26
|
+
port: Port;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Abstract base class for building NestJS adapter modules following the Ports & Adapters pattern.
|
|
30
|
+
*
|
|
31
|
+
* Adapters are dynamic modules that provide a port token and hide infrastructure details.
|
|
32
|
+
* This base class automatically handles provider registration, token aliasing, and exports
|
|
33
|
+
* by reading metadata from the @Port decorator.
|
|
34
|
+
*
|
|
35
|
+
* @template TOptions - The options type for configuring this adapter
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* @Port({
|
|
40
|
+
* token: STORAGE_PORT,
|
|
41
|
+
* implementation: S3Service
|
|
42
|
+
* })
|
|
43
|
+
* class S3Adapter extends Adapter<S3Options> {
|
|
44
|
+
* protected override imports(options: S3Options) {
|
|
45
|
+
* return []; // Optional: import other modules
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* protected override extraPoviders(options: S3Options) {
|
|
49
|
+
* return []; // Optional: additional providers
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare class Adapter<TOptions> {
|
|
55
|
+
/**
|
|
56
|
+
* Optional hook to import other NestJS modules.
|
|
57
|
+
* Override this method to add module dependencies.
|
|
58
|
+
*
|
|
59
|
+
* @param _options - The adapter configuration options
|
|
60
|
+
* @returns Array of modules to import
|
|
61
|
+
*/
|
|
62
|
+
protected imports(_options?: TOptions): unknown[];
|
|
63
|
+
/**
|
|
64
|
+
* Optional hook to provide additional providers.
|
|
65
|
+
* Override this method to add helper services, factories, or initialization logic.
|
|
66
|
+
*
|
|
67
|
+
* @param _options - The adapter configuration options
|
|
68
|
+
* @returns Array of additional providers
|
|
69
|
+
*/
|
|
70
|
+
protected extraPoviders(_options: TOptions): Provider[];
|
|
71
|
+
/**
|
|
72
|
+
* Synchronous registration method.
|
|
73
|
+
* Creates a dynamic module with the adapter's port token and implementation.
|
|
74
|
+
*
|
|
75
|
+
* @param options - The adapter configuration options
|
|
76
|
+
* @returns An AdapterModule with compile-time token proof
|
|
77
|
+
* @throws Error if @Port decorator is missing or incomplete
|
|
78
|
+
*/
|
|
79
|
+
static register<
|
|
80
|
+
TToken,
|
|
81
|
+
TOptions
|
|
82
|
+
>(this: new () => Adapter<TOptions>, options: TOptions): AdapterModule<TToken>;
|
|
83
|
+
/**
|
|
84
|
+
* Asynchronous registration method with dependency injection support.
|
|
85
|
+
* Creates a dynamic module where options are resolved via DI.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Async configuration with factory, imports, and inject
|
|
88
|
+
* @returns An AdapterModule with compile-time token proof
|
|
89
|
+
* @throws Error if @Port decorator is missing or incomplete
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* S3Adapter.registerAsync({
|
|
94
|
+
* imports: [ConfigModule],
|
|
95
|
+
* inject: [ConfigService],
|
|
96
|
+
* useFactory: (config) => config.get('s3'),
|
|
97
|
+
* })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
static registerAsync<
|
|
101
|
+
TToken,
|
|
102
|
+
TOptions
|
|
103
|
+
>(this: new () => Adapter<TOptions>, options: {
|
|
104
|
+
imports?: unknown[];
|
|
105
|
+
inject?: unknown[];
|
|
106
|
+
useFactory: (...args: unknown[]) => TOptions | Promise<TOptions>;
|
|
107
|
+
}): AdapterModule<TToken>;
|
|
108
|
+
}
|
|
109
|
+
import { Type } from "@nestjs/common";
|
|
110
|
+
/**
|
|
111
|
+
* Declares the port configuration for an adapter (token and implementation).
|
|
112
|
+
*
|
|
113
|
+
* This decorator stores both the port token and implementation class in metadata,
|
|
114
|
+
* which is read at runtime by the Adapter base class's register() and registerAsync() methods.
|
|
115
|
+
*
|
|
116
|
+
* @param config - Port configuration object
|
|
117
|
+
* @param config.token - The port token this adapter provides
|
|
118
|
+
* @param config.implementation - The concrete implementation class that provides the port functionality
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* @Port({
|
|
123
|
+
* token: OBJECT_STORAGE_PORT,
|
|
124
|
+
* implementation: S3ObjectStorageService
|
|
125
|
+
* })
|
|
126
|
+
* class S3Adapter extends Adapter<S3Options> {}
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function Port2<C extends PortConfig<any, any>>(config: {
|
|
130
|
+
token: C["token"];
|
|
131
|
+
implementation: Type<C["port"]>;
|
|
132
|
+
}): ClassDecorator;
|
|
133
|
+
/**
|
|
134
|
+
* DX decorator for injecting a port token into service constructors.
|
|
135
|
+
* This is a shorthand for @Inject(token) that provides clearer semantics.
|
|
136
|
+
*
|
|
137
|
+
* @param token - The port token to inject
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* @Injectable()
|
|
142
|
+
* class ObjectStorageService {
|
|
143
|
+
* constructor(
|
|
144
|
+
* @InjectPort(OBJECT_STORAGE_PROVIDER)
|
|
145
|
+
* private readonly storage: ObjectStoragePort,
|
|
146
|
+
* ) {}
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
declare function InjectPort<TToken>(token: TToken): ParameterDecorator;
|
|
151
|
+
import { DynamicModule as DynamicModule2 } from "@nestjs/common";
|
|
152
|
+
declare class PortModule<_TService> {
|
|
153
|
+
/**
|
|
154
|
+
* Registers the port module with an adapter.
|
|
155
|
+
*
|
|
156
|
+
* @param config - Configuration object containing the adapter module
|
|
157
|
+
* @param config.adapter - An adapter module that provides the required port
|
|
158
|
+
* @returns A dynamic module that imports the adapter and provides the service
|
|
159
|
+
*/
|
|
160
|
+
static register<TToken>({ adapter }: {
|
|
161
|
+
adapter?: AdapterModule<TToken>;
|
|
162
|
+
}): DynamicModule2;
|
|
163
|
+
}
|
|
164
|
+
export { PortModule, PortConfig, Port2 as Port, InjectPort, AdapterModule, Adapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
var import_node_module = require("node:module");
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
|
+
var __toCommonJS = (from) => {
|
|
8
|
+
var entry = __moduleCache.get(from), desc;
|
|
9
|
+
if (entry)
|
|
10
|
+
return entry;
|
|
11
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
13
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
14
|
+
get: () => from[key],
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
}));
|
|
17
|
+
__moduleCache.set(from, entry);
|
|
18
|
+
return entry;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
30
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
31
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
32
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
33
|
+
else
|
|
34
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
35
|
+
if (d = decorators[i])
|
|
36
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
37
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/index.ts
|
|
41
|
+
var exports_src = {};
|
|
42
|
+
__export(exports_src, {
|
|
43
|
+
PortModule: () => PortModule,
|
|
44
|
+
Port: () => Port,
|
|
45
|
+
InjectPort: () => InjectPort,
|
|
46
|
+
Adapter: () => Adapter
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(exports_src);
|
|
49
|
+
|
|
50
|
+
// src/core/adapter.base.ts
|
|
51
|
+
var import_reflect_metadata = require("reflect-metadata");
|
|
52
|
+
|
|
53
|
+
// src/core/constants.ts
|
|
54
|
+
var PORT_TOKEN_METADATA = Symbol("PORT_TOKEN_METADATA");
|
|
55
|
+
var PORT_IMPLEMENTATION_METADATA = Symbol("PORT_IMPLEMENTATION_METADATA");
|
|
56
|
+
|
|
57
|
+
// src/core/adapter.base.ts
|
|
58
|
+
class Adapter {
|
|
59
|
+
imports(_options) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
extraPoviders(_options) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
static register(options) {
|
|
66
|
+
const instance = new this;
|
|
67
|
+
const token = Reflect.getMetadata(PORT_TOKEN_METADATA, this);
|
|
68
|
+
const implementation = Reflect.getMetadata(PORT_IMPLEMENTATION_METADATA, this);
|
|
69
|
+
if (!token) {
|
|
70
|
+
throw new Error(`${this.name} must be decorated with @Port() and specify 'token'`);
|
|
71
|
+
}
|
|
72
|
+
if (!implementation) {
|
|
73
|
+
throw new Error(`${this.name} must be decorated with @Port() and specify 'implementation'`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
module: this,
|
|
77
|
+
imports: instance.imports(options),
|
|
78
|
+
providers: [
|
|
79
|
+
implementation,
|
|
80
|
+
{ provide: token, useExisting: implementation },
|
|
81
|
+
...instance.extraPoviders(options)
|
|
82
|
+
],
|
|
83
|
+
exports: [token],
|
|
84
|
+
__provides: token
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
static registerAsync(options) {
|
|
88
|
+
const instance = new this;
|
|
89
|
+
const token = Reflect.getMetadata(PORT_TOKEN_METADATA, this);
|
|
90
|
+
const implementation = Reflect.getMetadata(PORT_IMPLEMENTATION_METADATA, this);
|
|
91
|
+
if (!token) {
|
|
92
|
+
throw new Error(`${this.name} must be decorated with @Port() and specify 'token'`);
|
|
93
|
+
}
|
|
94
|
+
if (!implementation) {
|
|
95
|
+
throw new Error(`${this.name} must be decorated with @Port() and specify 'implementation'`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
module: this,
|
|
99
|
+
imports: [...options.imports ?? [], ...instance.imports()],
|
|
100
|
+
providers: [
|
|
101
|
+
implementation,
|
|
102
|
+
{ provide: token, useExisting: implementation },
|
|
103
|
+
...instance.extraPoviders({})
|
|
104
|
+
],
|
|
105
|
+
exports: [token],
|
|
106
|
+
__provides: token
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// src/core/decorators.ts
|
|
111
|
+
var import_reflect_metadata2 = require("reflect-metadata");
|
|
112
|
+
var import_common = require("@nestjs/common");
|
|
113
|
+
function Port(config) {
|
|
114
|
+
return (target) => {
|
|
115
|
+
Reflect.defineMetadata(PORT_TOKEN_METADATA, config.token, target);
|
|
116
|
+
Reflect.defineMetadata(PORT_IMPLEMENTATION_METADATA, config.implementation, target);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function InjectPort(token) {
|
|
120
|
+
return import_common.Inject(token);
|
|
121
|
+
}
|
|
122
|
+
// src/core/port-module.base.ts
|
|
123
|
+
var import_reflect_metadata3 = require("reflect-metadata");
|
|
124
|
+
var import_common2 = require("@nestjs/common");
|
|
125
|
+
class PortModule {
|
|
126
|
+
static register({
|
|
127
|
+
adapter
|
|
128
|
+
}) {
|
|
129
|
+
return {
|
|
130
|
+
module: PortModule,
|
|
131
|
+
imports: adapter ? [adapter] : []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
PortModule = __legacyDecorateClassTS([
|
|
136
|
+
import_common2.Module({})
|
|
137
|
+
], PortModule);
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nest-hex",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"homepage": "https://github.com/LiorVainer/nest-hex#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/LiorVainer/nest-hex/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"nest-hex": "./dist/cli/bin.js"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/LiorVainer/nest-hex.git"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bunup",
|
|
25
|
+
"dev": "bunup --watch",
|
|
26
|
+
"prepare": "bun simple-git-hooks",
|
|
27
|
+
"cli": "bun run src/cli/bin.ts",
|
|
28
|
+
"cli:init": "bun run src/cli/bin.ts init",
|
|
29
|
+
"cli:generate": "bun run src/cli/bin.ts generate",
|
|
30
|
+
"lint": "biome check .",
|
|
31
|
+
"lint:fix": "biome check --write .",
|
|
32
|
+
"release": "bumpp --commit --push --tag",
|
|
33
|
+
"test": "bun test",
|
|
34
|
+
"test:coverage": "bun test --coverage",
|
|
35
|
+
"test:watch": "bun test --watch",
|
|
36
|
+
"type-check": "tsc --noEmit",
|
|
37
|
+
"type-check:examples": "tsc --noEmit --project examples/tsconfig.json",
|
|
38
|
+
"vibecheck": "npx vibechck ."
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@biomejs/biome": "^2.3.10",
|
|
42
|
+
"@inkjs/ui": "^2.0.0",
|
|
43
|
+
"@nestjs/common": "^11.0.0",
|
|
44
|
+
"@nestjs/testing": "^11.1.11",
|
|
45
|
+
"@types/bun": "^1.3.5",
|
|
46
|
+
"@types/react": "^18.3.12",
|
|
47
|
+
"bumpp": "^10.3.2",
|
|
48
|
+
"bunup": "^0.16.11",
|
|
49
|
+
"commander": "^12.0.0",
|
|
50
|
+
"handlebars": "^4.7.8",
|
|
51
|
+
"ink": "^5.0.1",
|
|
52
|
+
"react": "^18.3.1",
|
|
53
|
+
"reflect-metadata": "^0.2.0",
|
|
54
|
+
"simple-git-hooks": "^2.13.1",
|
|
55
|
+
"typescript": "^5.9.3"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@nestjs/common": ">=10.0.0",
|
|
59
|
+
"reflect-metadata": ">=0.1.13",
|
|
60
|
+
"typescript": ">=4.5.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"typescript": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"main": "./dist/index.js",
|
|
68
|
+
"types": "./dist/index.d.ts",
|
|
69
|
+
"exports": {
|
|
70
|
+
".": {
|
|
71
|
+
"types": "./dist/index.d.ts",
|
|
72
|
+
"require": "./dist/index.js",
|
|
73
|
+
"import": "./dist/index.js"
|
|
74
|
+
},
|
|
75
|
+
"./cli": {
|
|
76
|
+
"types": "./dist/cli/index.d.ts",
|
|
77
|
+
"require": "./dist/cli/index.js",
|
|
78
|
+
"import": "./dist/cli/index.js"
|
|
79
|
+
},
|
|
80
|
+
"./package.json": "./package.json"
|
|
81
|
+
},
|
|
82
|
+
"simple-git-hooks": {
|
|
83
|
+
"pre-commit": "bun run lint:fix && bun run type-check"
|
|
84
|
+
},
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"ts-deepmerge": "^7.0.3"
|
|
87
|
+
}
|
|
88
|
+
}
|