nesties 1.1.20 → 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 +114 -1
- package/dist/index.cjs +73 -9
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +72 -9
- package/dist/index.mjs.map +3 -3
- package/dist/src/resolver.d.ts +16 -5
- package/dist/src/utility/uniq-by.d.ts +1 -0
- package/package.json +1 -1
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`
|
|
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
|
);
|
|
@@ -1570,6 +1599,15 @@ var ParamResolver = class extends ParamResolverBase {
|
|
|
1570
1599
|
}
|
|
1571
1600
|
}
|
|
1572
1601
|
}
|
|
1602
|
+
handleResolveValue(v) {
|
|
1603
|
+
if (this.info?.required && v == null) {
|
|
1604
|
+
throw new BlankReturnMessageDto(
|
|
1605
|
+
400,
|
|
1606
|
+
`Required parameter '${this.info.paramName}' in ${this.info.paramType} is missing`
|
|
1607
|
+
).toException();
|
|
1608
|
+
}
|
|
1609
|
+
return v;
|
|
1610
|
+
}
|
|
1573
1611
|
resolve(req, ref) {
|
|
1574
1612
|
if (this.info) {
|
|
1575
1613
|
if (this.info.paramType === "header") {
|
|
@@ -1577,10 +1615,10 @@ var ParamResolver = class extends ParamResolverBase {
|
|
|
1577
1615
|
let raw = getHeader(req, name);
|
|
1578
1616
|
if (name === "accept-language")
|
|
1579
1617
|
raw = pickPrimaryFromAcceptLanguage(raw);
|
|
1580
|
-
return raw;
|
|
1618
|
+
return this.handleResolveValue(raw);
|
|
1581
1619
|
}
|
|
1582
1620
|
if (this.info.paramType === "query") {
|
|
1583
|
-
return getQueryValue(req, this.info.paramName);
|
|
1621
|
+
return this.handleResolveValue(getQueryValue(req, this.info.paramName));
|
|
1584
1622
|
}
|
|
1585
1623
|
throw new Error(`Unsupported paramType: ${this.info.paramType}`);
|
|
1586
1624
|
} else if (this.dynamic) {
|
|
@@ -1591,7 +1629,7 @@ var ParamResolver = class extends ParamResolverBase {
|
|
|
1591
1629
|
const suffix = this.info ? `${this.info.paramType.toUpperCase()}_${this.info.paramName}` : `DYNAMIC`;
|
|
1592
1630
|
return `ParamResolver_${suffix}`;
|
|
1593
1631
|
}
|
|
1594
|
-
toSwaggerInfo() {
|
|
1632
|
+
toSwaggerInfo(extras = []) {
|
|
1595
1633
|
const swagger = (extras2 = {}) => {
|
|
1596
1634
|
if (this.info) {
|
|
1597
1635
|
const paramType = this.info.paramType;
|
|
@@ -1612,12 +1650,19 @@ var ParamResolver = class extends ParamResolverBase {
|
|
|
1612
1650
|
return () => {
|
|
1613
1651
|
};
|
|
1614
1652
|
};
|
|
1615
|
-
return [
|
|
1653
|
+
return super.toSwaggerInfo([
|
|
1616
1654
|
{
|
|
1617
1655
|
swagger,
|
|
1618
1656
|
token: this.toString()
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1657
|
+
},
|
|
1658
|
+
...this.info?.required ? [
|
|
1659
|
+
{
|
|
1660
|
+
swagger: () => ApiError(400, "Invalid request parameters"),
|
|
1661
|
+
token: `__missing_required_400__`
|
|
1662
|
+
}
|
|
1663
|
+
] : [],
|
|
1664
|
+
...extras
|
|
1665
|
+
]);
|
|
1621
1666
|
}
|
|
1622
1667
|
};
|
|
1623
1668
|
var CombinedParamResolver = class extends ParamResolverBase {
|
|
@@ -1641,10 +1686,28 @@ var CombinedParamResolver = class extends ParamResolverBase {
|
|
|
1641
1686
|
const suffix = Object.entries(this.resolvers).map(([key, resolver]) => `${key.toString()}_${resolver.toString()}`).join("__");
|
|
1642
1687
|
return `CombinedParamResolver_${suffix}`;
|
|
1643
1688
|
}
|
|
1644
|
-
toSwaggerInfo() {
|
|
1645
|
-
|
|
1689
|
+
toSwaggerInfo(extras = []) {
|
|
1690
|
+
const combined = Object.values(this.resolvers).flatMap(
|
|
1646
1691
|
(resolver) => resolver.toSwaggerInfo()
|
|
1647
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);
|
|
1648
1711
|
}
|
|
1649
1712
|
};
|
|
1650
1713
|
var getParamResolver = (input) => {
|
|
@@ -2098,6 +2161,7 @@ var I18nLookupMiddleware = (0, import_nfkit3.createI18nLookupMiddleware)();
|
|
|
2098
2161
|
ReturnMessageDto,
|
|
2099
2162
|
StringReturnMessageDto,
|
|
2100
2163
|
TokenGuard,
|
|
2164
|
+
TransformParamResolver,
|
|
2101
2165
|
abortableToken,
|
|
2102
2166
|
createAbortableProvider,
|
|
2103
2167
|
createI18n,
|