nesties 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +250 -39
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -98,8 +98,6 @@ class ExampleController {
98
98
  }
99
99
  ```
100
100
 
101
- ## Usage
102
-
103
101
  ### 4. Return Message DTOs
104
102
 
105
103
  Nesties provides a set of DTOs for consistent API response structures, and it also includes a utility function `ReturnMessageDto` to generate DTOs dynamically based on the provided class type.
@@ -153,67 +151,280 @@ This approach automatically creates a DTO structure with the properties of `User
153
151
 
154
152
  ### 5. Token Guard
155
153
 
156
- Nesties includes a `TokenGuard` class that validates server tokens from the request headers. This can be used with the `RequireToken` decorator for routes requiring token validation.
157
-
158
- ```typescript
159
- import { RequireToken } from 'nesties';
160
-
161
- @Controller('secure')
162
- export class SecureController {
163
- @Get()
164
- @RequireToken()
165
- secureEndpoint() {
166
- // This endpoint requires a token
167
- }
168
- }
169
- ```
154
+ `TokenGuard` validates a single server token” before invoking a controller method. By default it reads `SERVER_TOKEN` from `ConfigService` and compares it with the `x-server-token` header, returning a `401` when they differ.
170
155
 
171
- #### How to Use `TokenGuard`
156
+ #### Quick start (defaults only)
172
157
 
173
- 1. **Set the `SERVER_TOKEN` in the Configuration**
174
-
175
- In your Nest.js configuration, make sure to set up the `SERVER_TOKEN` using the `@nestjs/config` package.
158
+ 1. **Load the config module**
176
159
 
177
160
  ```typescript
178
161
  import { ConfigModule } from '@nestjs/config';
179
162
 
180
163
  @Module({
181
- imports: [ConfigModule.forRoot()],
164
+ imports: [ConfigModule.forRoot()],
182
165
  })
183
166
  export class AppModule {}
184
167
  ```
185
168
 
186
- In your environment file (`.env`), define your token:
169
+ 2. **Set the secret**
187
170
 
188
171
  ```
189
172
  SERVER_TOKEN=your-secure-token
190
173
  ```
191
174
 
192
- 2. **Token Validation with `TokenGuard`**
193
-
194
- `TokenGuard` checks the request headers for a token called `x-server-token`. If this token matches the one defined in your configuration, the request is allowed to proceed. If the token is missing or incorrect, a `401 Unauthorized` error is thrown.
195
-
196
- This approach is ideal for simple token-based authentication for APIs. It provides a lightweight method to protect routes without implementing a full OAuth or JWT-based system.
197
-
198
- 3. **Use `RequireToken` Decorator**
199
-
200
- Apply the `RequireToken` decorator to your controller methods to enforce token validation:
175
+ 3. **Decorate the route**
201
176
 
202
177
  ```typescript
203
178
  import { Controller, Get } from '@nestjs/common';
204
179
  import { RequireToken } from 'nesties';
205
180
 
206
- @Controller('api')
207
- export class ApiController {
208
- @Get('protected')
209
- @RequireToken()
210
- protectedRoute() {
211
- return { message: 'This is a protected route' };
212
- }
181
+ @Controller('secure')
182
+ export class SecureController {
183
+ @Get()
184
+ @RequireToken() // expects x-server-token to match SERVER_TOKEN
185
+ secureEndpoint() {
186
+ return { message: 'Valid server token supplied' };
187
+ }
213
188
  }
214
189
  ```
215
190
 
216
- In this example, the `protectedRoute` method will only be accessible if the request includes the correct `x-server-token` header.
191
+ `RequireToken()` installs `TokenGuard`, generates the Swagger header metadata automatically, and documents the `401` response. If `SERVER_TOKEN` is empty (e.g., local dev) the guard becomes a no-op so you can disable it without touching code.
192
+
193
+ #### Advanced configuration
194
+
195
+ When you need to override the defaults, pass options into `RequireToken`:
196
+
197
+ - `resolver` (default: `{ paramType: 'header', paramName: 'x-server-token' }`): where to read the **client** token from. Accepts any `ResolverDual`, so query/header resolvers are all supported.
198
+ - `tokenSource` (default: `'SERVER_TOKEN'`): how to read the **server** token. Provide another config key or an async resolver `(ctx, moduleRef) => Promise<string>` for dynamic sources.
199
+ - `errorCode` (default: `401`): HTTP status when tokens do not match.
200
+
201
+ ```typescript
202
+ @Controller('api')
203
+ export class ApiController {
204
+ @Get('protected')
205
+ @RequireToken({
206
+ resolver: { paramType: 'query', paramName: 'token' },
207
+ tokenSource: 'INTERNAL_TOKEN',
208
+ errorCode: 498,
209
+ })
210
+ fetch() {
211
+ return { data: 'guarded' };
212
+ }
213
+ }
214
+ ```
215
+
216
+ Multi-tenant secrets are just another `tokenSource` resolver:
217
+
218
+ ```typescript
219
+ import { ConfigService } from '@nestjs/config';
220
+ import { createResolver } from 'nesties';
221
+
222
+ const headerResolver = { paramType: 'header', paramName: 'x-tenant-token' };
223
+
224
+ @RequireToken({
225
+ resolver: headerResolver,
226
+ tokenSource: async (ctx, moduleRef) => {
227
+ const tenantId = await createResolver({
228
+ paramType: 'header',
229
+ paramName: 'x-tenant-id',
230
+ })(ctx, moduleRef);
231
+ const config = moduleRef.get(ConfigService);
232
+ return config.get<string>(`TENANT_${tenantId}_TOKEN`);
233
+ },
234
+ })
235
+ ```
236
+
237
+ `TokenGuard` only throws when both values exist and differ, so clearing the config value temporarily disables the guard without a code change.
238
+
239
+ ### 6. AbortableModule
240
+
241
+ Use `AbortableModule` when you want long‑running providers to respect the lifetime of the HTTP request. The module exposes a request‑scoped `AbortSignal` and wraps existing providers with [`nfkit`](https://www.npmjs.com/package/nfkit)'s `abortable` helper so that work can be canceled automatically when the client disconnects.
242
+
243
+ ```typescript
244
+ import { AbortableModule, InjectAbortable } from 'nesties';
245
+
246
+ @Module({
247
+ imports: [
248
+ AbortableModule.forRoot(), // registers the request-level AbortSignal
249
+ AbortableModule.forFeature([DemoService]), // wrap DemoService with an abortable proxy
250
+ ],
251
+ })
252
+ export class DemoModule {
253
+ constructor(@InjectAbortable() private readonly demo: DemoService) {}
254
+
255
+ getData() {
256
+ return this.demo.expensiveCall(); // aborts when request ends
257
+ }
258
+ }
259
+ ```
260
+
261
+ - `AbortableModule.forRoot()` should be added once (typically in `AppModule`) to expose the shared `AbortSignal`.
262
+ - `AbortableModule.forFeature([Token], { abortableOptions })` registers one or more providers that will be resolved per request and automatically wrapped in an abortable proxy.
263
+ - `@InjectAbortable()` can infer the token type automatically, or accept an explicit injection token, and `InjectAbortSignal()` gives direct access to the `AbortSignal` if you need to manage cancellation manually.
264
+
265
+ #### Injecting `AbortSignal` with `@nestjs/axios`
266
+
267
+ ```typescript
268
+ import { HttpModule, HttpService } from '@nestjs/axios';
269
+ import {
270
+ AbortableModule,
271
+ InjectAbortable,
272
+ InjectAbortSignal,
273
+ } from 'nesties';
274
+
275
+ @Module({
276
+ imports: [
277
+ HttpModule,
278
+ AbortableModule.forRoot(),
279
+ AbortableModule.forFeature([HttpService]),
280
+ ],
281
+ })
282
+ export class WeatherModule {
283
+ constructor(
284
+ @InjectAbortable() private readonly http: HttpService,
285
+ @InjectAbortSignal() private readonly abortSignal: AbortSignal,
286
+ ) {}
287
+
288
+ async fetchForecast() {
289
+ const { data } = await this.http.axiosRef.get(
290
+ 'https://api.example.com/weather',
291
+ { signal: this.abortSignal },
292
+ );
293
+ return data;
294
+ }
295
+ }
296
+ ```
297
+
298
+ The wrapped `HttpService` observes the same abort signal as the request, so in‑flight HTTP calls will be canceled as soon as the client disconnects or Nest aborts the request scope.
299
+
300
+ ### 7. I18nModule
301
+
302
+ Nesties also ships an opinionated but flexible internationalization module. The typical workflow is to call `createI18n` to obtain `I18nModule` plus the `UseI18n` decorator, register locale lookup middleware (e.g., `I18nLookupMiddleware`), and then return DTOs that contain placeholders like `#{key}`—the interceptor installed by `@UseI18n()` will translate those placeholders automatically before the response leaves the server.
303
+
304
+ ```typescript
305
+ import {
306
+ createI18n,
307
+ I18nService,
308
+ GenericReturnMessageDto,
309
+ I18nLookupMiddleware,
310
+ } from 'nesties';
311
+
312
+ const { I18nModule, UseI18n } = createI18n({
313
+ locales: ['en-US', 'zh-CN'],
314
+ defaultLocale: 'en-US',
315
+ });
316
+
317
+ @Module({
318
+ imports: [I18nModule],
319
+ })
320
+ export class AppModule {
321
+ constructor(private readonly i18n: I18nService) {
322
+ this.i18n.middleware(
323
+ I18nLookupMiddleware({
324
+ 'en-US': { bar: 'Nesties' },
325
+ 'zh-CN': { bar: '奈斯提' },
326
+ }),
327
+ );
328
+ }
329
+ }
330
+
331
+ @Controller()
332
+ @UseI18n()
333
+ export class GreetingController {
334
+ @Get()
335
+ async greet() {
336
+ return new GenericReturnMessageDto(200, 'OK', {
337
+ greeting: 'Hello #{bar}',
338
+ });
339
+ }
340
+ }
341
+
342
+ #### `@PutLocale()` Per-handler Overrides
343
+
344
+ `@PutLocale()` lets you override how the locale is resolved for a specific handler or parameter. Pass a custom resolver (any shape supported by `ResolverDual`) when you want to read the locale from a query param, body field, or even headers different from the global resolver.
345
+
346
+ ```typescript
347
+ import { GenericReturnMessageDto, PutLocale } from 'nesties';
348
+
349
+ @Controller('reports')
350
+ @UseI18n()
351
+ export class ReportController {
352
+ @Get()
353
+ async summary(
354
+ @PutLocale({ paramType: 'query', paramName: 'locale' }) locale: string,
355
+ ) {
356
+ // locale now respects ?locale=...
357
+ return new GenericReturnMessageDto(200, 'OK', {
358
+ summary: 'report.summary',
359
+ });
360
+ }
361
+ }
362
+ ```
363
+
364
+ #### Custom Middleware with TypeORM
365
+
366
+ You can register any number of middlewares that resolve placeholders. The example below queries a TypeORM repository to fetch translations stored in a database and falls back to the next middleware when no record is found.
367
+
368
+ ```typescript
369
+ import { ExecutionContext, Injectable } from '@nestjs/common';
370
+ import { InjectRepository } from '@nestjs/typeorm';
371
+ import {
372
+ Repository,
373
+ Entity,
374
+ Column,
375
+ PrimaryGeneratedColumn,
376
+ } from 'typeorm';
377
+ import { I18nService } from 'nesties';
378
+
379
+ @Entity()
380
+ export class Translation {
381
+ @PrimaryGeneratedColumn()
382
+ id: number;
383
+
384
+ @Column()
385
+ locale: string;
386
+
387
+ @Column()
388
+ key: string;
389
+
390
+ @Column()
391
+ value: string;
392
+ }
393
+
394
+ @Injectable()
395
+ export class TranslationMiddleware {
396
+ constructor(
397
+ private readonly i18n: I18nService,
398
+ @InjectRepository(Translation)
399
+ private readonly repo: Repository<Translation>,
400
+ ) {
401
+ this.i18n.middleware(this.lookupFromDatabase.bind(this));
402
+ }
403
+
404
+ private async lookupFromDatabase(
405
+ locale: string,
406
+ key: string,
407
+ next: () => Promise<string | undefined>,
408
+ ctx?: ExecutionContext,
409
+ ) {
410
+ const found = await this.repo.findOne({ where: { locale, key } });
411
+ if (found) {
412
+ return found.value;
413
+ }
414
+ return next();
415
+ }
416
+ }
417
+ ```
418
+
419
+ Register `TranslationMiddleware` in any module that also imports `TypeOrmModule.forFeature([Translation])` so the service is instantiated and its middleware is attached to `I18nService`.
420
+
421
+ By composing multiple middlewares (dictionaries, database lookups, remote APIs), you can build a tiered fallback chain that covers every translation source you need.
422
+
423
+ - `createI18n` returns both a configured module (`I18nModule`) and a decorator (`UseI18n`) that adds the bundled interceptor and Swagger metadata describing the locale resolver.
424
+ - `UseI18n` wires the interceptor that walks the returned DTO (e.g., `GenericReturnMessageDto`) and replaces every string that contains placeholders (`Hello #{key}`) using the locale detected from the incoming request.
425
+ - `I18nService.middleware` lets you register middlewares such as `I18nLookupMiddleware` for dictionary lookups, database resolvers, or remote translation APIs.
426
+ - `LocalePipe`/`PutLocale` provide ergonomic access to the resolved locale inside route handlers, and you can override the resolver per parameter when necessary.
427
+ - `I18nService.translate` and `translateString` remain available for advanced manual flows (generating strings outside of interceptor scope, building static assets, etc.).
217
428
 
218
429
  ## DTO Classes
219
430
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nesties",
3
3
  "description": "Nest.js utilities",
4
- "version": "1.1.8",
4
+ "version": "1.1.10",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "https://github.com/purerosefallen/nesties.git"
27
+ "url": "git+https://github.com/purerosefallen/nesties.git"
28
28
  },
29
29
  "author": "Nanahira <nanahira@momobako.com>",
30
30
  "license": "MIT",