@shadow-library/fastify 1.4.0 → 1.5.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/README.md +387 -0
- package/cjs/decorators/http-input.decorator.d.ts +2 -3
- package/cjs/decorators/http-output.decorator.d.ts +3 -0
- package/cjs/decorators/http-output.decorator.js +0 -4
- package/cjs/decorators/index.d.ts +1 -0
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/transform.decorator.d.ts +27 -0
- package/cjs/decorators/transform.decorator.js +17 -0
- package/cjs/interfaces/server-metadata.interface.d.ts +2 -2
- package/cjs/module/data-transformers.d.ts +14 -0
- package/cjs/module/data-transformers.js +19 -0
- package/cjs/module/fastify-module.interface.d.ts +5 -0
- package/cjs/module/fastify-router.d.ts +17 -5
- package/cjs/module/fastify-router.js +136 -30
- package/esm/decorators/http-input.decorator.d.ts +2 -3
- package/esm/decorators/http-output.decorator.d.ts +3 -0
- package/esm/decorators/http-output.decorator.js +0 -4
- package/esm/decorators/index.d.ts +1 -0
- package/esm/decorators/index.js +1 -0
- package/esm/decorators/transform.decorator.d.ts +27 -0
- package/esm/decorators/transform.decorator.js +14 -0
- package/esm/interfaces/server-metadata.interface.d.ts +2 -2
- package/esm/module/data-transformers.d.ts +14 -0
- package/esm/module/data-transformers.js +16 -0
- package/esm/module/fastify-module.interface.d.ts +5 -0
- package/esm/module/fastify-router.d.ts +17 -5
- package/esm/module/fastify-router.js +136 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -952,6 +952,393 @@ class CreateProductDto {
|
|
|
952
952
|
}
|
|
953
953
|
```
|
|
954
954
|
|
|
955
|
+
## Data Transformation
|
|
956
|
+
|
|
957
|
+
The `@Transform` decorator enables automatic data transformation at two key points in the request-response lifecycle:
|
|
958
|
+
|
|
959
|
+
1. **After Validation (Request)**: Transforms incoming data (body, query, params) after AJV validation but before it reaches your route handler. This allows validation to run against the original data type while your handler receives the transformed type.
|
|
960
|
+
|
|
961
|
+
2. **Before Serialization (Response)**: Transforms outgoing response data before it's serialized and sent to the client. This allows you to work with one data type internally while presenting a different format to API consumers.
|
|
962
|
+
|
|
963
|
+
### How It Works
|
|
964
|
+
|
|
965
|
+
The `@Transform` decorator works in conjunction with `@Field` from `@shadow-library/class-schema`. The `@Field` decorator defines the **source/validation type** (the type used for JSON Schema validation), while `@Transform` converts the data to the **target type** (the type your code actually works with or returns to the client).
|
|
966
|
+
|
|
967
|
+
```mermaid
|
|
968
|
+
flowchart LR
|
|
969
|
+
subgraph responseFlow["Response Flow"]
|
|
970
|
+
Response["Response (JSON)"]
|
|
971
|
+
Serialization["Serialization (JSON)"]
|
|
972
|
+
ResTransform["Transform (Pre Serialization)"]
|
|
973
|
+
ResHandler["Route Handler (Returns Data)"]
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
subgraph requestFlow["Request Flow"]
|
|
977
|
+
ReqHandler["Route Handler (Receives Transformed Data)"]
|
|
978
|
+
ReqTransform["Transform (Post Validation)"]
|
|
979
|
+
Validation["Validation (AJV)"]
|
|
980
|
+
rawReq["Raw Request (JSON)"]
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
rawReq --> Validation
|
|
984
|
+
Validation --> ReqTransform
|
|
985
|
+
ReqTransform --> ReqHandler
|
|
986
|
+
ResHandler --> ResTransform
|
|
987
|
+
ResTransform --> Serialization
|
|
988
|
+
Serialization --> Response
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
### Basic Usage
|
|
992
|
+
|
|
993
|
+
The `@Transform` decorator accepts either a transformer name (string) or an options object:
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
// Single transformer - applies to both input and output
|
|
997
|
+
@Transform('string:trim')
|
|
998
|
+
|
|
999
|
+
// Options object - specify different transformers for input and output
|
|
1000
|
+
@Transform({ input: 'int:parse', output: 'int:stringify' })
|
|
1001
|
+
|
|
1002
|
+
// Options object - specify only input or output
|
|
1003
|
+
@Transform({ input: 'string:trim' }) // Only transforms input
|
|
1004
|
+
@Transform({ output: 'email:normalize' }) // Only transforms output
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
#### Example
|
|
1008
|
+
|
|
1009
|
+
```typescript
|
|
1010
|
+
import { Schema, Field } from '@shadow-library/class-schema';
|
|
1011
|
+
import { Transform } from '@shadow-library/fastify';
|
|
1012
|
+
|
|
1013
|
+
@Schema()
|
|
1014
|
+
class CreateUserDto {
|
|
1015
|
+
@Field(() => String)
|
|
1016
|
+
@Transform('string:trim')
|
|
1017
|
+
name: string;
|
|
1018
|
+
|
|
1019
|
+
@Field(() => String, { format: 'email' })
|
|
1020
|
+
@Transform('email:normalize')
|
|
1021
|
+
email: string;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
@HttpController('/users')
|
|
1025
|
+
export class UserController {
|
|
1026
|
+
@Post()
|
|
1027
|
+
async createUser(@Body() userData: CreateUserDto) {
|
|
1028
|
+
// userData.name is automatically trimmed
|
|
1029
|
+
// userData.email is normalized (trimmed and lowercased)
|
|
1030
|
+
return this.userService.create(userData);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### Built-in Transformers
|
|
1036
|
+
|
|
1037
|
+
The following transformers are available out of the box:
|
|
1038
|
+
|
|
1039
|
+
| Transformer | Input Type | Output Type | Description |
|
|
1040
|
+
| ----------------- | ---------- | ----------- | ------------------------------------------ |
|
|
1041
|
+
| `email:normalize` | `string` | `string` | Trims whitespace and converts to lowercase |
|
|
1042
|
+
| `string:trim` | `string` | `string` | Removes leading and trailing whitespace |
|
|
1043
|
+
| `int:parse` | `string` | `number` | Parses string to integer (base 10) |
|
|
1044
|
+
| `float:parse` | `string` | `number` | Parses string to floating-point number |
|
|
1045
|
+
| `bigint:parse` | `string` | `bigint` | Parses string to BigInt |
|
|
1046
|
+
|
|
1047
|
+
### Request Transformation Examples
|
|
1048
|
+
|
|
1049
|
+
#### Normalizing User Input
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
@Schema()
|
|
1053
|
+
class SignUpDto {
|
|
1054
|
+
@Field(() => String)
|
|
1055
|
+
@Transform('string:trim')
|
|
1056
|
+
username: string;
|
|
1057
|
+
|
|
1058
|
+
@Field(() => String, { format: 'email' })
|
|
1059
|
+
@Transform('email:normalize')
|
|
1060
|
+
email: string;
|
|
1061
|
+
|
|
1062
|
+
@Field(() => String)
|
|
1063
|
+
password: string;
|
|
1064
|
+
}
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
#### Parsing Query Parameters
|
|
1068
|
+
|
|
1069
|
+
Query parameters are always received as strings from the HTTP layer. Use `@Transform` to convert them to the appropriate type while keeping `@Field` for validation:
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
@Schema()
|
|
1073
|
+
class PaginationQuery {
|
|
1074
|
+
// @Field defines the validation type (string from query params)
|
|
1075
|
+
// @Transform converts the validated string to a number for your handler
|
|
1076
|
+
@Field(() => String, { pattern: '^[0-9]+$' })
|
|
1077
|
+
@Transform('int:parse')
|
|
1078
|
+
page: number;
|
|
1079
|
+
|
|
1080
|
+
@Field(() => String, { pattern: '^[0-9]+$' })
|
|
1081
|
+
@Transform('int:parse')
|
|
1082
|
+
limit: number;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
@HttpController('/products')
|
|
1086
|
+
export class ProductController {
|
|
1087
|
+
@Get()
|
|
1088
|
+
async getProducts(@Query() query: PaginationQuery) {
|
|
1089
|
+
// query.page and query.limit are numbers here, not strings
|
|
1090
|
+
// Validation ensured they were numeric strings before transformation
|
|
1091
|
+
return this.productService.findAll(query.page, query.limit);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
#### Working with Large Numbers
|
|
1097
|
+
|
|
1098
|
+
JavaScript numbers lose precision beyond `Number.MAX_SAFE_INTEGER`. Use BigInt for large numbers while transmitting as strings:
|
|
1099
|
+
|
|
1100
|
+
```typescript
|
|
1101
|
+
@Schema()
|
|
1102
|
+
class TransactionDto {
|
|
1103
|
+
// Validated as a string (safe for JSON transmission)
|
|
1104
|
+
// Transformed to BigInt for precise arithmetic in your handler
|
|
1105
|
+
@Field(() => String, { pattern: '^-?[0-9]+$' })
|
|
1106
|
+
@Transform('bigint:parse')
|
|
1107
|
+
amount: bigint;
|
|
1108
|
+
|
|
1109
|
+
@Field(() => String)
|
|
1110
|
+
recipientId: string;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
@HttpController('/transactions')
|
|
1114
|
+
export class TransactionController {
|
|
1115
|
+
@Post()
|
|
1116
|
+
async createTransaction(@Body() dto: TransactionDto) {
|
|
1117
|
+
// dto.amount is a BigInt - safe for precise calculations
|
|
1118
|
+
const fee = dto.amount / 100n; // BigInt arithmetic
|
|
1119
|
+
return this.transactionService.process(dto);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### Response Transformation Examples
|
|
1125
|
+
|
|
1126
|
+
Transformers also work on response schemas, allowing you to convert internal data types to API-friendly formats.
|
|
1127
|
+
|
|
1128
|
+
#### Currency Formatting
|
|
1129
|
+
|
|
1130
|
+
Store and compute with integers (cents) to avoid floating-point errors, but display as decimal strings for the API:
|
|
1131
|
+
|
|
1132
|
+
```typescript
|
|
1133
|
+
// Define a custom transformer for cents to decimal conversion
|
|
1134
|
+
declare module '@shadow-library/fastify' {
|
|
1135
|
+
export interface CustomTransformers {
|
|
1136
|
+
'currency:format': (value: number) => string;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Register the transformer in your module config
|
|
1141
|
+
FastifyModule.forRoot({
|
|
1142
|
+
controllers: [ProductController],
|
|
1143
|
+
transformers: {
|
|
1144
|
+
'currency:format': (cents: number) => (cents / 100).toFixed(2),
|
|
1145
|
+
},
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
// Use in your response DTO
|
|
1149
|
+
@Schema()
|
|
1150
|
+
class ProductResponse {
|
|
1151
|
+
@Field(() => Number)
|
|
1152
|
+
id: number;
|
|
1153
|
+
|
|
1154
|
+
@Field(() => String)
|
|
1155
|
+
name: string;
|
|
1156
|
+
|
|
1157
|
+
// Internally stored as integer (cents), transformed to "19.99" format for API response
|
|
1158
|
+
@Field(() => String)
|
|
1159
|
+
@Transform('currency:format')
|
|
1160
|
+
price: number; // TypeScript type is what is used internally
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
@HttpController('/products')
|
|
1164
|
+
export class ProductController {
|
|
1165
|
+
@Get('/:id')
|
|
1166
|
+
@RespondFor(200, ProductResponse)
|
|
1167
|
+
async getProduct(@Params() params: { id: string }): Promise<ProductResponse> {
|
|
1168
|
+
const product = await this.productService.findById(params.id);
|
|
1169
|
+
// product.price = 1999 (integer cents from database)
|
|
1170
|
+
return product;
|
|
1171
|
+
// Response: { id: 1, name: "Widget", price: "19.99" }
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
#### Date Formatting
|
|
1177
|
+
|
|
1178
|
+
```typescript
|
|
1179
|
+
declare module '@shadow-library/fastify' {
|
|
1180
|
+
export interface CustomTransformers {
|
|
1181
|
+
'date:iso': (value: Date) => string;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
FastifyModule.forRoot({
|
|
1186
|
+
controllers: [EventController],
|
|
1187
|
+
transformers: {
|
|
1188
|
+
'date:iso': (date: Date) => date.toISOString(),
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
@Schema()
|
|
1193
|
+
class EventResponse {
|
|
1194
|
+
@Field(() => String)
|
|
1195
|
+
name: string;
|
|
1196
|
+
|
|
1197
|
+
// Date object internally, ISO string in API response
|
|
1198
|
+
@Field(() => String)
|
|
1199
|
+
@Transform('date:iso')
|
|
1200
|
+
startDate: Date;
|
|
1201
|
+
}
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
### Different Input and Output Transformers
|
|
1205
|
+
|
|
1206
|
+
Sometimes you need different transformations for incoming requests vs outgoing responses. The `@Transform` decorator supports separate `input` and `output` options:
|
|
1207
|
+
|
|
1208
|
+
```typescript
|
|
1209
|
+
declare module '@shadow-library/fastify' {
|
|
1210
|
+
export interface CustomTransformers {
|
|
1211
|
+
'cents:parse': (value: string) => number;
|
|
1212
|
+
'cents:format': (value: number) => string;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
FastifyModule.forRoot({
|
|
1217
|
+
controllers: [TransactionController],
|
|
1218
|
+
transformers: {
|
|
1219
|
+
'cents:parse': (value: string) => Math.round(parseFloat(value) * 100),
|
|
1220
|
+
'cents:format': (cents: number) => (cents / 100).toFixed(2),
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
@Schema()
|
|
1225
|
+
class TransactionDto {
|
|
1226
|
+
@Field(() => String)
|
|
1227
|
+
@Transform({ input: 'cents:parse', output: 'cents:format' })
|
|
1228
|
+
amount: number;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
@HttpController('/transactions')
|
|
1232
|
+
export class TransactionController {
|
|
1233
|
+
@Post()
|
|
1234
|
+
@RespondFor(201, TransactionDto)
|
|
1235
|
+
async createTransaction(@Body() dto: TransactionDto): Promise<TransactionDto> {
|
|
1236
|
+
// Request: { "amount": "19.99" }
|
|
1237
|
+
// dto.amount = 1999 (parsed to cents)
|
|
1238
|
+
|
|
1239
|
+
// Your business logic works with cents (integers)
|
|
1240
|
+
const saved = await this.transactionService.save(dto);
|
|
1241
|
+
|
|
1242
|
+
return saved;
|
|
1243
|
+
// Response: { "amount": "19.99" } (formatted back to decimal string)
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
#### Input-Only or Output-Only Transformations
|
|
1249
|
+
|
|
1250
|
+
You can also specify only input or output transformation:
|
|
1251
|
+
|
|
1252
|
+
```typescript
|
|
1253
|
+
@Schema()
|
|
1254
|
+
class SearchQuery {
|
|
1255
|
+
// Only transform input - sanitize search terms
|
|
1256
|
+
@Field(() => String)
|
|
1257
|
+
@Transform({ input: 'string:trim' })
|
|
1258
|
+
query: string;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
@Schema()
|
|
1262
|
+
class UserResponse {
|
|
1263
|
+
@Field(() => String)
|
|
1264
|
+
name: string;
|
|
1265
|
+
|
|
1266
|
+
// Only transform output - mask email for privacy
|
|
1267
|
+
@Field(() => String)
|
|
1268
|
+
@Transform({ output: 'email:mask' })
|
|
1269
|
+
email: string;
|
|
1270
|
+
}
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
### Extending with Custom Transformers
|
|
1274
|
+
|
|
1275
|
+
#### Step 1: Declare the Transformer Types
|
|
1276
|
+
|
|
1277
|
+
Extend the `CustomTransformers` interface for type-safe transformer names:
|
|
1278
|
+
|
|
1279
|
+
```typescript
|
|
1280
|
+
declare module '@shadow-library/fastify' {
|
|
1281
|
+
export interface CustomTransformers {
|
|
1282
|
+
'phone:normalize': (value: string) => string;
|
|
1283
|
+
'currency:format': (value: number) => string;
|
|
1284
|
+
'date:parse': (value: string) => Date;
|
|
1285
|
+
'boolean:parse': (value: string) => boolean;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
#### Step 2: Register Transformers in Module Config
|
|
1291
|
+
|
|
1292
|
+
```typescript
|
|
1293
|
+
@Module({
|
|
1294
|
+
imports: [
|
|
1295
|
+
FastifyModule.forRoot({
|
|
1296
|
+
controllers: [UserController, ProductController],
|
|
1297
|
+
transformers: {
|
|
1298
|
+
'phone:normalize': (phone: string) => phone.replace(/[^0-9+]/g, ''),
|
|
1299
|
+
'currency:format': (cents: number) => (cents / 100).toFixed(2),
|
|
1300
|
+
'date:parse': (dateStr: string) => new Date(dateStr),
|
|
1301
|
+
'boolean:parse': (value: string) => value === 'true' || value === '1',
|
|
1302
|
+
},
|
|
1303
|
+
}),
|
|
1304
|
+
],
|
|
1305
|
+
})
|
|
1306
|
+
export class AppModule {}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
#### Step 3: Use in Your Schemas
|
|
1310
|
+
|
|
1311
|
+
```typescript
|
|
1312
|
+
@Schema()
|
|
1313
|
+
class UserDto {
|
|
1314
|
+
@Field(() => String)
|
|
1315
|
+
@Transform('phone:normalize')
|
|
1316
|
+
phone: string;
|
|
1317
|
+
|
|
1318
|
+
@Field(() => String)
|
|
1319
|
+
@Transform('boolean:parse')
|
|
1320
|
+
isActive: boolean;
|
|
1321
|
+
}
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
### Best Practices
|
|
1325
|
+
|
|
1326
|
+
1. **Validate First, Transform After**: The `@Field` decorator defines what's valid input. `@Transform` converts valid input to your preferred type.
|
|
1327
|
+
|
|
1328
|
+
2. **Use Strings for Query/Params Validation**: Query parameters and URL params are always strings. Validate them as strings, then transform:
|
|
1329
|
+
|
|
1330
|
+
```typescript
|
|
1331
|
+
@Field(() => String, { pattern: '^[0-9]+$' }) // Validate as numeric string
|
|
1332
|
+
@Transform('int:parse') // Convert to number
|
|
1333
|
+
id: number;
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
3. **Avoid Floating-Point for Currency**: Store monetary values as integers (cents) and use transformers for display formatting.
|
|
1337
|
+
|
|
1338
|
+
4. **Keep Transformers Pure**: Transformers should be pure functions without side effects.
|
|
1339
|
+
|
|
1340
|
+
5. **Handle Edge Cases**: Ensure your custom transformers handle edge cases like empty strings, null values, etc.
|
|
1341
|
+
|
|
955
1342
|
## Response Serialization
|
|
956
1343
|
|
|
957
1344
|
Define response schemas for automatic serialization and documentation:
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { JSONSchema } from '@shadow-library/class-schema';
|
|
2
|
-
import { Class } from 'type-fest';
|
|
1
|
+
import { JSONSchema, SchemaClass } from '@shadow-library/class-schema';
|
|
3
2
|
/**
|
|
4
3
|
* Defining types
|
|
5
4
|
*/
|
|
@@ -10,7 +9,7 @@ export declare enum RouteInputType {
|
|
|
10
9
|
REQUEST = "request",
|
|
11
10
|
RESPONSE = "response"
|
|
12
11
|
}
|
|
13
|
-
export type RouteInputSchemas = Partial<Record<'body' | 'params' | 'query', JSONSchema |
|
|
12
|
+
export type RouteInputSchemas = Partial<Record<'body' | 'params' | 'query', JSONSchema | SchemaClass>>;
|
|
14
13
|
/**
|
|
15
14
|
* Declaring the constants
|
|
16
15
|
*/
|
|
@@ -13,6 +13,9 @@ export interface DynamicRender<T extends JsonObject> {
|
|
|
13
13
|
template: string;
|
|
14
14
|
data: T;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Declaring the constants
|
|
18
|
+
*/
|
|
16
19
|
export declare const HttpStatus: (status: number) => MethodDecorator;
|
|
17
20
|
export declare const Header: (name: string, value: string | (() => string)) => MethodDecorator;
|
|
18
21
|
export declare const Redirect: (redirect: string, status?: number) => MethodDecorator;
|
|
@@ -6,11 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.Render = exports.Redirect = exports.Header = exports.HttpStatus = void 0;
|
|
7
7
|
exports.RespondFor = RespondFor;
|
|
8
8
|
const app_1 = require("@shadow-library/app");
|
|
9
|
-
const class_schema_1 = require("@shadow-library/class-schema");
|
|
10
9
|
/**
|
|
11
10
|
* Declaring the constants
|
|
12
11
|
*/
|
|
13
|
-
const isClass = (schema) => schema.toString().startsWith('class ');
|
|
14
12
|
const HttpStatus = (status) => (0, app_1.Route)({ status });
|
|
15
13
|
exports.HttpStatus = HttpStatus;
|
|
16
14
|
const Header = (name, value) => (0, app_1.Route)({ headers: { [name]: value } });
|
|
@@ -20,7 +18,5 @@ exports.Redirect = Redirect;
|
|
|
20
18
|
const Render = (render) => (0, app_1.Route)({ render: render ?? true });
|
|
21
19
|
exports.Render = Render;
|
|
22
20
|
function RespondFor(statusCode, schema) {
|
|
23
|
-
if (isClass(schema))
|
|
24
|
-
schema = class_schema_1.ClassSchema.generate(schema);
|
|
25
21
|
return (0, app_1.Route)({ schemas: { response: { [statusCode]: schema } } });
|
|
26
22
|
}
|
package/cjs/decorators/index.js
CHANGED
|
@@ -20,4 +20,5 @@ __exportStar(require("./http-output.decorator.js"), exports);
|
|
|
20
20
|
__exportStar(require("./http-route.decorator.js"), exports);
|
|
21
21
|
__exportStar(require("./middleware.decorator.js"), exports);
|
|
22
22
|
__exportStar(require("./sensitive.decorator.js"), exports);
|
|
23
|
+
__exportStar(require("./transform.decorator.js"), exports);
|
|
23
24
|
__exportStar(require("./version.decorator.js"), exports);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importing npm packages
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Importing user defined packages
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Defining types
|
|
9
|
+
*/
|
|
10
|
+
export interface TransformOptions {
|
|
11
|
+
input?: TransformTypes;
|
|
12
|
+
output?: TransformTypes;
|
|
13
|
+
}
|
|
14
|
+
export interface CustomTransformers {
|
|
15
|
+
}
|
|
16
|
+
export interface InbuiltTransformers {
|
|
17
|
+
'email:normalize': (value: string) => string;
|
|
18
|
+
'string:trim': (value: string) => string;
|
|
19
|
+
'int:parse': (value: string) => number;
|
|
20
|
+
'float:parse': (value: string) => number;
|
|
21
|
+
'bigint:parse': (value: string) => bigint;
|
|
22
|
+
}
|
|
23
|
+
export type TransformTypes = keyof CustomTransformers | keyof InbuiltTransformers;
|
|
24
|
+
/**
|
|
25
|
+
* Declaring the constants
|
|
26
|
+
*/
|
|
27
|
+
export declare function Transform(type: TransformTypes | TransformOptions): PropertyDecorator;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Importing npm packages
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Transform = Transform;
|
|
7
|
+
const class_schema_1 = require("@shadow-library/class-schema");
|
|
8
|
+
/**
|
|
9
|
+
* Declaring the constants
|
|
10
|
+
*/
|
|
11
|
+
function Transform(type) {
|
|
12
|
+
const options = typeof type === 'string' ? { input: type, output: type } : type;
|
|
13
|
+
return (target, propertyKey) => {
|
|
14
|
+
const decorator = (0, class_schema_1.FieldMetadata)({ 'x-fastify': { transform: options } });
|
|
15
|
+
decorator(target, propertyKey);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Importing npm packages
|
|
3
3
|
*/
|
|
4
4
|
import { RouteMetadata } from '@shadow-library/app';
|
|
5
|
-
import { JSONSchema } from '@shadow-library/class-schema';
|
|
5
|
+
import { JSONSchema, SchemaClass } from '@shadow-library/class-schema';
|
|
6
6
|
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
|
|
7
7
|
/**
|
|
8
8
|
* Importing user defined packages
|
|
@@ -18,7 +18,7 @@ declare module '@shadow-library/app' {
|
|
|
18
18
|
path?: string;
|
|
19
19
|
version?: number;
|
|
20
20
|
schemas?: RouteInputSchemas & {
|
|
21
|
-
response?: Record<number | string, JSONSchema>;
|
|
21
|
+
response?: Record<number | string, JSONSchema | SchemaClass>;
|
|
22
22
|
};
|
|
23
23
|
rawBody?: boolean;
|
|
24
24
|
silentValidation?: boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importing npm packages
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Importing user defined packages
|
|
6
|
+
*/
|
|
7
|
+
import { InbuiltTransformers } from '../decorators/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Defining types
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Declaring the constants
|
|
13
|
+
*/
|
|
14
|
+
export declare const INBUILT_TRANSFORMERS: InbuiltTransformers;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Importing npm packages
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.INBUILT_TRANSFORMERS = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Defining types
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Declaring the constants
|
|
12
|
+
*/
|
|
13
|
+
exports.INBUILT_TRANSFORMERS = {
|
|
14
|
+
'email:normalize': value => value.trim().toLowerCase(),
|
|
15
|
+
'string:trim': value => value.trim(),
|
|
16
|
+
'int:parse': value => parseInt(value, 10),
|
|
17
|
+
'float:parse': value => parseFloat(value),
|
|
18
|
+
'bigint:parse': value => BigInt(value),
|
|
19
|
+
};
|
|
@@ -8,6 +8,7 @@ import { Promisable } from 'type-fest';
|
|
|
8
8
|
/**
|
|
9
9
|
* Importing user defined packages
|
|
10
10
|
*/
|
|
11
|
+
import { CustomTransformers } from '../decorators/index.js';
|
|
11
12
|
import { ErrorHandler } from '../interfaces/index.js';
|
|
12
13
|
import { ContextService } from '../services/index.js';
|
|
13
14
|
/**
|
|
@@ -59,6 +60,10 @@ export interface FastifyConfig extends FastifyServerOptions {
|
|
|
59
60
|
* The global route prefix for all routes in the Fastify instance
|
|
60
61
|
*/
|
|
61
62
|
routePrefix?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Object defining custom transformers for request and response data transformation
|
|
65
|
+
*/
|
|
66
|
+
transformers?: Record<keyof CustomTransformers, (value: any) => any>;
|
|
62
67
|
}
|
|
63
68
|
export interface FastifyModuleOptions extends Partial<FastifyConfig> {
|
|
64
69
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ControllerRouteMetadata, Router } from '@shadow-library/app';
|
|
2
|
+
import { Transformer } from '@shadow-library/class-schema';
|
|
2
3
|
import { type FastifyInstance } from 'fastify';
|
|
3
4
|
import { Chain as MockRequestChain, InjectOptions as MockRequestOptions, Response as MockResponse } from 'light-my-request';
|
|
4
5
|
import { JsonObject, JsonValue } from 'type-fest';
|
|
@@ -49,12 +50,15 @@ export interface ChildRouteRequest {
|
|
|
49
50
|
params: Record<string, string>;
|
|
50
51
|
query: Record<string, string>;
|
|
51
52
|
}
|
|
53
|
+
interface Transformers {
|
|
54
|
+
body?: Transformer;
|
|
55
|
+
query?: Transformer;
|
|
56
|
+
params?: Transformer;
|
|
57
|
+
response?: Record<string, Transformer>;
|
|
58
|
+
}
|
|
52
59
|
interface RouteArtifacts {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
maskQuery?(query: object): object;
|
|
56
|
-
maskParams?(params: object): object;
|
|
57
|
-
};
|
|
60
|
+
masks: Partial<Record<'body' | 'query' | 'params', Transformer>>;
|
|
61
|
+
transformers: Transformers;
|
|
58
62
|
}
|
|
59
63
|
export declare class FastifyRouter extends Router {
|
|
60
64
|
private readonly config;
|
|
@@ -64,17 +68,25 @@ export declare class FastifyRouter extends Router {
|
|
|
64
68
|
private readonly logger;
|
|
65
69
|
private readonly cachedDynamicMiddlewares;
|
|
66
70
|
private readonly childRouter;
|
|
71
|
+
private readonly transformers;
|
|
67
72
|
private readonly sensitiveTransformer;
|
|
73
|
+
private readonly inputDataTransformer;
|
|
74
|
+
private readonly outputDataTransformer;
|
|
68
75
|
constructor(config: FastifyConfig, instance: FastifyInstance, context: ContextService);
|
|
69
76
|
getInstance(): FastifyInstance;
|
|
77
|
+
private isTransformable;
|
|
70
78
|
private joinPaths;
|
|
79
|
+
private addRouteHandler;
|
|
71
80
|
private registerRawBody;
|
|
72
81
|
private maskField;
|
|
82
|
+
private generateDataTransformer;
|
|
73
83
|
private getRequestLogger;
|
|
74
84
|
private parseControllers;
|
|
75
85
|
private getStatusCode;
|
|
76
86
|
private generateRouteHandler;
|
|
77
87
|
private getMiddlewareHandler;
|
|
88
|
+
private transformResponseHandler;
|
|
89
|
+
private transformRequestHandler;
|
|
78
90
|
register(controllers: ControllerRouteMetadata[]): Promise<void>;
|
|
79
91
|
start(): Promise<void>;
|
|
80
92
|
stop(): Promise<void>;
|