nesties 1.1.21 → 1.1.22

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 CHANGED
@@ -492,7 +492,7 @@ By composing multiple middlewares (dictionaries, database lookups, remote APIs),
492
492
 
493
493
  ### 8. ParamResolver
494
494
 
495
- `ParamResolver` and `CombinedParamResolver` provide a small, composable abstraction over headers and query parameters. They are used internally by `TokenGuard`, the i18n utilities, and can also be used directly in controllers, pipes, and guards.
495
+ `ParamResolver` provide a small, composable abstraction over headers and query parameters. They are used internally by `TokenGuard`, the i18n utilities, and can also be used directly in controllers, pipes, and guards.
496
496
 
497
497
  #### Static header / query resolvers
498
498
 
@@ -635,6 +635,119 @@ When used as a decorator, the combined resolver:
635
635
  - Returns a typed object where each key corresponds to the original resolver
636
636
  - Emits merged Swagger metadata for all headers / queries involved
637
637
 
638
+ #### Transforming resolved values with `TransformParamResolver`
639
+
640
+ Sometimes reading a header or query parameter is only the first step. You often want to **normalize**, **validate**, or **enrich** that value before it reaches your handler—especially when the transformation needs access to request-scoped dependencies via `ModuleRef`.
641
+
642
+ `TransformParamResolver` is a thin wrapper around any `ParamResolverBase<T>` that applies a function `T -> U`:
643
+
644
+ - It **reuses** the base resolver’s extraction logic (header/query/dynamic).
645
+ - It runs a **transform function** that can be sync or async.
646
+ - It receives `(value, moduleRef, req)` so you can:
647
+ - normalize formats (`'zh-hant' -> 'zh-Hant'`)
648
+ - parse primitives (`string -> number/boolean/date`)
649
+ - validate and throw `HttpException` / `BlankReturnMessageDto`
650
+ - hydrate values (e.g., `token -> user`) by resolving services from DI
651
+ - Swagger metadata is **inherited** from the base resolver, so the API contract stays accurate and not duplicated.
652
+
653
+ ##### Basic example: normalize a header value
654
+
655
+ ```ts
656
+ import { Controller, Get } from '@nestjs/common';
657
+ import { ParamResolver, TransformParamResolver } from 'nesties';
658
+
659
+ const rawLang = new ParamResolver({
660
+ paramType: 'header',
661
+ paramName: 'accept-language',
662
+ });
663
+
664
+ const normalizedLang = new TransformParamResolver(
665
+ rawLang,
666
+ (value) => {
667
+ if (!value) return undefined;
668
+ const v = value.split(',')[0]?.trim() ?? value;
669
+ const lower = v.toLowerCase();
670
+ if (lower === 'zh-hant' || lower === 'zh-tw') return 'zh-Hant';
671
+ if (lower === 'en-us') return 'en-US';
672
+ return v;
673
+ },
674
+ );
675
+
676
+ const Lang = normalizedLang.toParamDecorator();
677
+
678
+ @Controller()
679
+ export class LocaleController {
680
+ @Get('lang')
681
+ getLang(@Lang() lang: string | undefined) {
682
+ return { lang };
683
+ }
684
+ }
685
+ ```
686
+
687
+ ##### Advanced example: resolve a request-scoped service inside the transform
688
+
689
+ Because `TransformParamResolver` receives `ModuleRef` and `req`, you can resolve request-scoped providers using `ContextIdFactory.getByRequest(req)`.
690
+
691
+ ```ts
692
+ import { Injectable, Scope } from '@nestjs/common';
693
+ import { ContextIdFactory, ModuleRef } from '@nestjs/core';
694
+ import { ParamResolver, TransformParamResolver } from 'nesties';
695
+
696
+ @Injectable({ scope: Scope.REQUEST })
697
+ class LocaleService {
698
+ normalize(input?: string) {
699
+ if (!input) return undefined;
700
+ const lower = input.toLowerCase();
701
+ if (lower === 'zh-hant') return 'zh-Hant';
702
+ return input;
703
+ }
704
+ }
705
+
706
+ const rawLocale = new ParamResolver({
707
+ paramType: 'query',
708
+ paramName: 'locale',
709
+ });
710
+
711
+ const localeWithService = new TransformParamResolver(
712
+ rawLocale,
713
+ async (value, ref: ModuleRef, req) => {
714
+ const ctxId = ContextIdFactory.getByRequest(req);
715
+ const svc = await ref.resolve(LocaleService, ctxId, { strict: false });
716
+ return svc.normalize(value);
717
+ },
718
+ );
719
+ ```
720
+
721
+ ##### Composing multiple transforms (nesting)
722
+
723
+ `TransformParamResolver` can be used as the base resolver of another `TransformParamResolver`, forming a pipeline of transformations.
724
+
725
+ ```ts
726
+ import { TransformParamResolver } from 'nesties';
727
+
728
+ // rawLocale: ParamResolverBase<string | undefined>
729
+
730
+ // Step 1: normalize
731
+ const normalized = new TransformParamResolver(rawLocale, (v) =>
732
+ v ? v.trim() : undefined,
733
+ );
734
+
735
+ // Step 2: validate / coerce
736
+ const validated = new TransformParamResolver(normalized, (v) => {
737
+ if (!v) return 'en-US';
738
+ return v;
739
+ });
740
+
741
+ // validated resolves to string
742
+ const Locale = validated.toParamDecorator();
743
+ ```
744
+
745
+ ##### Notes
746
+
747
+ - `TransformParamResolver` does **not** change where the parameter comes from—only how the resolved value is shaped.
748
+ - Swagger decorators are inherited from the base resolver, so documentation remains consistent even when you compose multiple transforms.
749
+ - For multi-field inputs, you can first use `CombinedParamResolver`, then transform the combined object into a richer type (e.g., `{ lang, token } -> { locale, user }`).
750
+
638
751
  #### Request-scoped providers from resolvers
639
752
 
640
753
  Sometimes you want to treat the resolved value itself as an injectable request-scoped provider. You can derive such a provider from any `ParamResolver` or `CombinedParamResolver` using `toRequestScopedProvider()`:
package/dist/index.cjs CHANGED
@@ -1166,6 +1166,7 @@ __export(index_exports, {
1166
1166
  ReturnMessageDto: () => ReturnMessageDto,
1167
1167
  StringReturnMessageDto: () => StringReturnMessageDto,
1168
1168
  TokenGuard: () => TokenGuard,
1169
+ TransformParamResolver: () => TransformParamResolver,
1169
1170
  abortableToken: () => abortableToken,
1170
1171
  createAbortableProvider: () => createAbortableProvider,
1171
1172
  createI18n: () => createI18n,
@@ -1445,6 +1446,20 @@ var createProvider = (options, factory) => {
1445
1446
  // src/utility/resolver-swagger-map.ts
1446
1447
  var ResolverSwaggerMap = /* @__PURE__ */ new Map();
1447
1448
 
1449
+ // src/utility/uniq-by.ts
1450
+ var uniqBy = (arr, fn) => {
1451
+ const seen = /* @__PURE__ */ new Set();
1452
+ const result = [];
1453
+ for (const item of arr) {
1454
+ const key = fn(item);
1455
+ if (!seen.has(key)) {
1456
+ seen.add(key);
1457
+ result.push(item);
1458
+ }
1459
+ }
1460
+ return result;
1461
+ };
1462
+
1448
1463
  // src/resolver.ts
1449
1464
  var ParamResolverCopiedFieldsFromSwagger = [
1450
1465
  "required",
@@ -1510,8 +1525,22 @@ ParamResolverPipe = __decorateClass([
1510
1525
  ], ParamResolverPipe);
1511
1526
  var usedParamResolverTokens = /* @__PURE__ */ new Set();
1512
1527
  var ParamResolverBase = class {
1528
+ constructor() {
1529
+ this.extraSwagger = [];
1530
+ }
1531
+ addExtraDecorator(dec, token = Math.random().toString(36)) {
1532
+ this.extraSwagger.push({
1533
+ token,
1534
+ swagger: dec
1535
+ });
1536
+ return this;
1537
+ }
1538
+ // for override
1539
+ toSwaggerInfo(extras = []) {
1540
+ return uniqBy([...this.extraSwagger, ...extras], (info) => info.token);
1541
+ }
1513
1542
  toApiPropertyDecorator(extras = {}) {
1514
- const swaggerInfo = this.toSwaggerInfo();
1543
+ const swaggerInfo = uniqBy(this.toSwaggerInfo(), (info) => info.token);
1515
1544
  return (extras2 = {}) => MergeClassOrMethodDecorators(
1516
1545
  swaggerInfo.map((info) => info.swagger({ ...extras, ...extras2 }))
1517
1546
  );
@@ -1600,7 +1629,7 @@ var ParamResolver = class extends ParamResolverBase {
1600
1629
  const suffix = this.info ? `${this.info.paramType.toUpperCase()}_${this.info.paramName}` : `DYNAMIC`;
1601
1630
  return `ParamResolver_${suffix}`;
1602
1631
  }
1603
- toSwaggerInfo() {
1632
+ toSwaggerInfo(extras = []) {
1604
1633
  const swagger = (extras2 = {}) => {
1605
1634
  if (this.info) {
1606
1635
  const paramType = this.info.paramType;
@@ -1621,7 +1650,7 @@ var ParamResolver = class extends ParamResolverBase {
1621
1650
  return () => {
1622
1651
  };
1623
1652
  };
1624
- return [
1653
+ return super.toSwaggerInfo([
1625
1654
  {
1626
1655
  swagger,
1627
1656
  token: this.toString()
@@ -1631,8 +1660,9 @@ var ParamResolver = class extends ParamResolverBase {
1631
1660
  swagger: () => ApiError(400, "Invalid request parameters"),
1632
1661
  token: `__missing_required_400__`
1633
1662
  }
1634
- ] : []
1635
- ];
1663
+ ] : [],
1664
+ ...extras
1665
+ ]);
1636
1666
  }
1637
1667
  };
1638
1668
  var CombinedParamResolver = class extends ParamResolverBase {
@@ -1656,10 +1686,28 @@ var CombinedParamResolver = class extends ParamResolverBase {
1656
1686
  const suffix = Object.entries(this.resolvers).map(([key, resolver]) => `${key.toString()}_${resolver.toString()}`).join("__");
1657
1687
  return `CombinedParamResolver_${suffix}`;
1658
1688
  }
1659
- toSwaggerInfo() {
1660
- return Object.values(this.resolvers).flatMap(
1689
+ toSwaggerInfo(extras = []) {
1690
+ const combined = Object.values(this.resolvers).flatMap(
1661
1691
  (resolver) => resolver.toSwaggerInfo()
1662
1692
  );
1693
+ return super.toSwaggerInfo([...combined, ...extras]);
1694
+ }
1695
+ };
1696
+ var TransformParamResolver = class extends ParamResolverBase {
1697
+ constructor(baseResolver, transformFn) {
1698
+ super();
1699
+ this.baseResolver = baseResolver;
1700
+ this.transformFn = transformFn;
1701
+ }
1702
+ async resolve(req, ref) {
1703
+ const baseValue = await this.baseResolver.resolve(req, ref);
1704
+ return this.transformFn(baseValue, ref, req);
1705
+ }
1706
+ toString() {
1707
+ return `Transform_${this.baseResolver.toString()}`;
1708
+ }
1709
+ toSwaggerInfo(extras = []) {
1710
+ return this.baseResolver.toSwaggerInfo(extras);
1663
1711
  }
1664
1712
  };
1665
1713
  var getParamResolver = (input) => {
@@ -2113,6 +2161,7 @@ var I18nLookupMiddleware = (0, import_nfkit3.createI18nLookupMiddleware)();
2113
2161
  ReturnMessageDto,
2114
2162
  StringReturnMessageDto,
2115
2163
  TokenGuard,
2164
+ TransformParamResolver,
2116
2165
  abortableToken,
2117
2166
  createAbortableProvider,
2118
2167
  createI18n,