runtypex 0.1.13 → 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/README.md +112 -278
- package/dist/cjs/core/emitArrayOrTuple.d.ts +6 -0
- package/dist/cjs/core/emitArrayOrTuple.js +40 -0
- package/dist/cjs/core/emitLiteralOrEnum.d.ts +4 -0
- package/dist/cjs/core/emitLiteralOrEnum.js +47 -0
- package/dist/cjs/core/emitMapperFromSpec.d.ts +32 -0
- package/dist/cjs/core/emitMapperFromSpec.js +199 -0
- package/dist/cjs/core/emitObject.d.ts +6 -0
- package/dist/cjs/core/emitObject.js +30 -0
- package/dist/cjs/core/emitPrimitive.d.ts +6 -0
- package/dist/cjs/core/emitPrimitive.js +29 -0
- package/dist/cjs/core/emitUnionOrIntersection.d.ts +6 -0
- package/dist/cjs/core/emitUnionOrIntersection.js +15 -0
- package/dist/cjs/core/index.d.ts +17 -0
- package/dist/cjs/core/index.js +41 -0
- package/dist/cjs/core/path.d.ts +9 -0
- package/dist/cjs/core/path.js +42 -0
- package/dist/cjs/generator/generate-jsdoc.d.ts +13 -0
- package/dist/cjs/generator/generate-jsdoc.js +69 -0
- package/dist/cjs/generator/index.d.ts +1 -0
- package/dist/cjs/generator/index.js +17 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/mapper/index.d.ts +1 -0
- package/dist/cjs/mapper/index.js +17 -0
- package/dist/cjs/runtime/index.d.ts +2 -0
- package/dist/cjs/runtime/index.js +18 -0
- package/dist/cjs/runtime/mapper.d.ts +71 -0
- package/dist/cjs/runtime/mapper.js +79 -0
- package/dist/cjs/runtime/validate.d.ts +11 -0
- package/dist/cjs/runtime/validate.js +18 -0
- package/dist/cjs/transformer/helper.d.ts +2 -0
- package/dist/cjs/transformer/helper.js +63 -0
- package/dist/cjs/transformer/index.d.ts +3 -0
- package/dist/cjs/transformer/index.js +12 -0
- package/dist/cjs/transformer/ts-transformer.d.ts +29 -0
- package/dist/cjs/transformer/ts-transformer.js +109 -0
- package/dist/cjs/transformer/vite-plugin.d.ts +18 -0
- package/dist/cjs/transformer/vite-plugin.js +72 -0
- package/dist/esm/core/emitArrayOrTuple.d.ts +1 -1
- package/dist/esm/core/emitLiteralOrEnum.d.ts +1 -1
- package/dist/esm/core/emitMapperFromSpec.d.ts +32 -0
- package/dist/esm/core/emitMapperFromSpec.js +189 -0
- package/dist/esm/core/emitObject.d.ts +1 -1
- package/dist/esm/core/emitObject.js +4 -2
- package/dist/esm/core/emitPrimitive.d.ts +1 -1
- package/dist/esm/core/emitUnionOrIntersection.d.ts +1 -1
- package/dist/esm/core/path.d.ts +9 -0
- package/dist/esm/core/path.js +36 -0
- package/dist/esm/generator/generate-jsdoc.d.ts +13 -0
- package/dist/esm/generator/generate-jsdoc.js +63 -0
- package/dist/esm/generator/index.d.ts +1 -0
- package/dist/esm/generator/index.js +1 -0
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/mapper/index.d.ts +1 -0
- package/dist/esm/mapper/index.js +1 -0
- package/dist/esm/runtime/index.d.ts +2 -0
- package/dist/esm/runtime/index.js +2 -0
- package/dist/esm/runtime/mapper.d.ts +71 -0
- package/dist/esm/runtime/mapper.js +71 -0
- package/dist/esm/transformer/index.d.ts +3 -0
- package/dist/esm/transformer/index.js +3 -0
- package/dist/esm/transformer/ts-transformer.d.ts +8 -4
- package/dist/esm/transformer/ts-transformer.js +51 -10
- package/dist/esm/transformer/vite-plugin.js +7 -32
- package/docs/build-integrations.md +89 -0
- package/docs/jsdoc-generation.md +88 -0
- package/docs/mapper.md +104 -0
- package/docs/mapping-policy.md +78 -0
- package/docs/runtime-validation.md +84 -0
- package/package.json +76 -36
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { emitGuardFromType } from "../core/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { emitMapperFromSpec } from "../core/emitMapperFromSpec.js";
|
|
4
4
|
/**
|
|
5
5
|
* 🧩 tsTransformer
|
|
6
6
|
* TypeScript custom transformer (BEFORE) factory.
|
|
@@ -22,7 +22,6 @@ import { resolveTypeByName } from "./helper.js";
|
|
|
22
22
|
* ✅ Optionally removed in production builds
|
|
23
23
|
*/
|
|
24
24
|
export default function tsTransformer(options) {
|
|
25
|
-
const { program } = options;
|
|
26
25
|
const checker = options.program.getTypeChecker();
|
|
27
26
|
const removeInProd = !!options.removeInProd;
|
|
28
27
|
const prod = process.env.NODE_ENV === "production";
|
|
@@ -30,13 +29,8 @@ export default function tsTransformer(options) {
|
|
|
30
29
|
const visit = (node) => {
|
|
31
30
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
32
31
|
const name = node.expression.text;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const typeNode = node.typeArguments[0];
|
|
36
|
-
const typeName = typeNode.getText();
|
|
37
|
-
const type = resolveTypeByName(program, node.getSourceFile(), checker, typeName);
|
|
38
|
-
if (!type)
|
|
39
|
-
return node;
|
|
32
|
+
if ((name === "makeValidate" || name === "makeAssert") && node.typeArguments?.length) {
|
|
33
|
+
const type = checker.getTypeFromTypeNode(node.typeArguments[0]);
|
|
40
34
|
const isRemovedInProd = removeInProd && prod;
|
|
41
35
|
switch (name) {
|
|
42
36
|
case "makeValidate":
|
|
@@ -45,18 +39,65 @@ export default function tsTransformer(options) {
|
|
|
45
39
|
return _emitMakeAssert(checker, type, isRemovedInProd);
|
|
46
40
|
}
|
|
47
41
|
}
|
|
42
|
+
// makeMapper<TDto, TDomain>(spec) becomes an inline validating mapper.
|
|
43
|
+
if (name === "makeMapper" && node.typeArguments?.length === 2 && node.arguments[0]) {
|
|
44
|
+
const mapperCallOptions = _readMapperCallOptions(node.arguments[1]);
|
|
45
|
+
const mapper = emitMapperFromSpec({
|
|
46
|
+
checker,
|
|
47
|
+
dtoType: checker.getTypeFromTypeNode(node.typeArguments[0]),
|
|
48
|
+
domainType: checker.getTypeFromTypeNode(node.typeArguments[1]),
|
|
49
|
+
specNode: node.arguments[0],
|
|
50
|
+
sourceFile: node.getSourceFile(),
|
|
51
|
+
options: {
|
|
52
|
+
validateDto: !(removeInProd && prod) && options.validateDto !== false,
|
|
53
|
+
validateDomain: !(removeInProd && prod) && options.validateDomain !== false,
|
|
54
|
+
mappingPolicy: mapperCallOptions.policy,
|
|
55
|
+
policyMode: mapperCallOptions.policyMode,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (mapper)
|
|
59
|
+
return ts.factory.createIdentifier(mapper);
|
|
60
|
+
}
|
|
48
61
|
}
|
|
49
62
|
return ts.visitEachChild(node, visit, context);
|
|
50
63
|
};
|
|
51
64
|
return (sf) => ts.visitNode(sf, visit);
|
|
52
65
|
};
|
|
53
66
|
}
|
|
67
|
+
function _readMapperCallOptions(node) {
|
|
68
|
+
if (!node)
|
|
69
|
+
return {};
|
|
70
|
+
const expr = ts.isAsExpression(node) || ts.isParenthesizedExpression(node) ? node.expression : node;
|
|
71
|
+
if (!ts.isObjectLiteralExpression(expr))
|
|
72
|
+
return {};
|
|
73
|
+
return {
|
|
74
|
+
policy: _readExpressionProperty(expr, "policy"),
|
|
75
|
+
policyMode: _readPolicyMode(expr),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function _readExpressionProperty(object, name) {
|
|
79
|
+
for (const item of object.properties) {
|
|
80
|
+
if (ts.isPropertyAssignment(item) && ts.isIdentifier(item.name) && item.name.text === name) {
|
|
81
|
+
return item.initializer;
|
|
82
|
+
}
|
|
83
|
+
if (ts.isShorthandPropertyAssignment(item) && item.name.text === name) {
|
|
84
|
+
return item.name;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
function _readPolicyMode(object) {
|
|
90
|
+
const mode = _readExpressionProperty(object, "policyMode");
|
|
91
|
+
return mode && ts.isStringLiteral(mode) && (mode.text === "warn" || mode.text === "error") ? mode.text : undefined;
|
|
92
|
+
}
|
|
54
93
|
function _emitMakeValidate(checker, type, isRemovedInProd) {
|
|
55
94
|
const guard = isRemovedInProd ? "((_)=>true)" : emitGuardFromType(checker, type);
|
|
56
95
|
return ts.factory.createIdentifier(guard);
|
|
57
96
|
}
|
|
58
97
|
function _emitMakeAssert(checker, type, isRemovedInProd) {
|
|
59
|
-
|
|
98
|
+
if (isRemovedInProd)
|
|
99
|
+
return ts.factory.createIdentifier("((_)=>{})");
|
|
100
|
+
const guard = emitGuardFromType(checker, type);
|
|
60
101
|
const txt = `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
|
|
61
102
|
return ts.factory.createIdentifier(txt);
|
|
62
103
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import { resolveTypeByName } from "./helper.js";
|
|
3
|
+
import tsTransformer from "./ts-transformer.js";
|
|
5
4
|
/**
|
|
6
5
|
* 🧩 vitePluginRuntypex
|
|
7
6
|
* A Vite plugin that performs build-time type → runtime validation transformation.
|
|
@@ -18,24 +17,21 @@ import { resolveTypeByName } from "./helper.js";
|
|
|
18
17
|
*/
|
|
19
18
|
export default function vitePluginRuntypex(options) {
|
|
20
19
|
const removeInProd = !!options?.removeInProd;
|
|
21
|
-
const prod = process.env.NODE_ENV === "production";
|
|
22
20
|
return {
|
|
23
21
|
name: "vite-plugin-runtypex",
|
|
24
22
|
enforce: "pre",
|
|
25
23
|
transform(code, id) {
|
|
26
24
|
const isTS = id.endsWith(".ts") || id.endsWith(".tsx");
|
|
27
|
-
const isTargetFunction = /make(?:Validate|Assert)</.test(code);
|
|
25
|
+
const isTargetFunction = /make(?:Validate|Assert|Mapper)</.test(code);
|
|
28
26
|
if (!isTS || !isTargetFunction)
|
|
29
27
|
return;
|
|
30
|
-
const { program
|
|
28
|
+
const { program } = _createProgramFor(id);
|
|
31
29
|
const sf = program.getSourceFile(id);
|
|
32
30
|
if (!sf)
|
|
33
31
|
return;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// ② makeValidate<T>()
|
|
38
|
-
mutated = mutated.replace(/makeValidate<\s*([^>]+)\s*>\s*\(\s*\)/g, (_m, typeName) => _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd }) ?? _m);
|
|
32
|
+
const result = ts.transform(sf, [tsTransformer({ program, removeInProd })]);
|
|
33
|
+
const mutated = ts.createPrinter().printFile(result.transformed[0]);
|
|
34
|
+
result.dispose();
|
|
39
35
|
return mutated === code ? null : { code: mutated, map: null };
|
|
40
36
|
},
|
|
41
37
|
};
|
|
@@ -50,8 +46,7 @@ function _createProgramFor(file) {
|
|
|
50
46
|
throw new Error(ts.flattenDiagnosticMessageText(cfg.error.messageText, "\n"));
|
|
51
47
|
const parsed = ts.parseJsonConfigFileContent(cfg.config, ts.sys, path.dirname(tsconfig));
|
|
52
48
|
const program = ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
|
|
53
|
-
|
|
54
|
-
return { program, checker };
|
|
49
|
+
return { program };
|
|
55
50
|
}
|
|
56
51
|
function _findNearestTsconfig(start) {
|
|
57
52
|
let dir = start;
|
|
@@ -69,23 +64,3 @@ function _findNearestTsconfig(start) {
|
|
|
69
64
|
return fallback;
|
|
70
65
|
throw new Error("tsconfig.json not found");
|
|
71
66
|
}
|
|
72
|
-
// ──────────────────────────────────────────────
|
|
73
|
-
// ② Emit Helpers
|
|
74
|
-
// ──────────────────────────────────────────────
|
|
75
|
-
function _emitMakeValidate({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
76
|
-
if (removeInProd && prod)
|
|
77
|
-
return `((_)=>true)`;
|
|
78
|
-
const type = resolveTypeByName(program, sf, checker, typeName.trim());
|
|
79
|
-
if (!type)
|
|
80
|
-
return null;
|
|
81
|
-
return emitGuardFromType(checker, type);
|
|
82
|
-
}
|
|
83
|
-
function _emitMakeAssert({ program, checker, sf, typeName, prod, removeInProd, }) {
|
|
84
|
-
if (removeInProd && prod)
|
|
85
|
-
return `((_)=>{})`;
|
|
86
|
-
const type = resolveTypeByName(program, sf, checker, typeName.trim());
|
|
87
|
-
if (!type)
|
|
88
|
-
return null;
|
|
89
|
-
const guard = emitGuardFromType(checker, type);
|
|
90
|
-
return `(function(){const G=${guard};return(i)=>{if(!G(i))throw new TypeError("[runtypex] Validation failed.");};})()`;
|
|
91
|
-
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Build Integrations
|
|
2
|
+
|
|
3
|
+
`runtypex` relies on the TypeScript compiler API, so its full value comes from
|
|
4
|
+
running the transformer during build.
|
|
5
|
+
|
|
6
|
+
## Vite
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// vite.config.ts
|
|
10
|
+
import { defineConfig } from "vite";
|
|
11
|
+
import { vitePlugin as runtypex } from "runtypex";
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [runtypex()],
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The Vite plugin scans TypeScript files for:
|
|
19
|
+
|
|
20
|
+
- `makeValidate<T>()`
|
|
21
|
+
- `makeAssert<T>()`
|
|
22
|
+
- `makeMapper<TDto, TDomain>()`
|
|
23
|
+
|
|
24
|
+
When a target call is found, the plugin creates a TypeScript program for the
|
|
25
|
+
nearest `tsconfig.json`, runs the transformer, and returns the transformed code
|
|
26
|
+
to Vite.
|
|
27
|
+
|
|
28
|
+
## ts-loader
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// webpack.config.js
|
|
32
|
+
const { tsTransformer } = require("runtypex");
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
module: {
|
|
36
|
+
rules: [
|
|
37
|
+
{
|
|
38
|
+
test: /\.tsx?$/,
|
|
39
|
+
loader: "ts-loader",
|
|
40
|
+
options: {
|
|
41
|
+
getCustomTransformers: (program) => ({
|
|
42
|
+
before: [tsTransformer({ program })],
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Transformer Options
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
tsTransformer({
|
|
55
|
+
program,
|
|
56
|
+
removeInProd: true,
|
|
57
|
+
validateDto: true,
|
|
58
|
+
validateDomain: true,
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Option | Default | Description |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `program` | required | TypeScript program used to resolve types. |
|
|
65
|
+
| `removeInProd` | `false` | Replaces generated validation with no-op functions in production. |
|
|
66
|
+
| `validateDto` | `true` | Enables DTO input validation for generated mappers. |
|
|
67
|
+
| `validateDomain` | `true` | Enables domain output validation for generated mappers. |
|
|
68
|
+
|
|
69
|
+
## Package Entry Points
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { makeValidate } from "runtypex";
|
|
73
|
+
import { makeMapper } from "runtypex/mapper";
|
|
74
|
+
import { generateJSDocFromSpec } from "runtypex/generator";
|
|
75
|
+
import { tsTransformer } from "runtypex/transformer";
|
|
76
|
+
import { vitePlugin } from "runtypex/transformer/vite-plugin";
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The package exports ESM and CommonJS builds from `dist/esm` and `dist/cjs`.
|
|
80
|
+
|
|
81
|
+
## Local Verification
|
|
82
|
+
|
|
83
|
+
Useful verification commands:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm run build
|
|
87
|
+
npx jest --runInBand --watchman=false
|
|
88
|
+
npm run test:esm
|
|
89
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# JSDoc Generation
|
|
2
|
+
|
|
3
|
+
JSDoc generation creates interface documentation from mapper metadata. It helps
|
|
4
|
+
editors show where a domain field came from and which DTO or database field it
|
|
5
|
+
represents.
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { generateJSDocFromSpec } from "runtypex/generator";
|
|
11
|
+
|
|
12
|
+
const source = generateJSDocFromSpec({
|
|
13
|
+
checker,
|
|
14
|
+
dtoType,
|
|
15
|
+
domainType,
|
|
16
|
+
specNode,
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This API is intended for build tooling that already has access to the TypeScript
|
|
21
|
+
program, checker, DTO type, domain type, and mapper spec node.
|
|
22
|
+
|
|
23
|
+
## Metadata Fields
|
|
24
|
+
|
|
25
|
+
Mapper metadata can include:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
source("user_id", {
|
|
29
|
+
db: "users.user_id",
|
|
30
|
+
description: "User id",
|
|
31
|
+
dtoDescription: "Identifier returned by the user API.",
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Field meanings:
|
|
36
|
+
|
|
37
|
+
| Field | Meaning |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `description` | Domain field description. Usually used as the first JSDoc sentence. |
|
|
40
|
+
| `dtoDescription` | Optional explanation attached to the DTO path line. |
|
|
41
|
+
| `db` | Optional database table and column reference. |
|
|
42
|
+
|
|
43
|
+
## Generated Output
|
|
44
|
+
|
|
45
|
+
Given this domain field:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
id: source("user_id", {
|
|
49
|
+
db: "users.user_id",
|
|
50
|
+
description: "User id",
|
|
51
|
+
dtoDescription: "Identifier returned by the user API.",
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
the generated documentation can look like this:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
/**
|
|
59
|
+
* User id
|
|
60
|
+
*
|
|
61
|
+
* DTO: UserDto.user_id Identifier returned by the user API.
|
|
62
|
+
* DTO type: string
|
|
63
|
+
* DB: users.user_id
|
|
64
|
+
* Domain type: string
|
|
65
|
+
*/
|
|
66
|
+
id: string;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Policy Integration
|
|
70
|
+
|
|
71
|
+
JSDoc generation can also receive mapper policy options. This lets documentation
|
|
72
|
+
generation fail or warn when the spec violates canonical DTO path naming:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
generateJSDocFromSpec({
|
|
76
|
+
checker,
|
|
77
|
+
dtoType,
|
|
78
|
+
domainType,
|
|
79
|
+
specNode,
|
|
80
|
+
options: {
|
|
81
|
+
policy,
|
|
82
|
+
policyMode: "error",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use this when generated docs should reflect the same naming rules as runtime and
|
|
88
|
+
build-time mapper generation.
|
package/docs/mapper.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Mapper
|
|
2
|
+
|
|
3
|
+
The mapper feature converts DTO objects into domain objects with a typed mapping
|
|
4
|
+
spec. It is useful when API or database field names do not match the names used
|
|
5
|
+
inside application code.
|
|
6
|
+
|
|
7
|
+
## Basic Example
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineMap, makeMapper, source, transform } from "runtypex/mapper";
|
|
11
|
+
|
|
12
|
+
interface UserDto {
|
|
13
|
+
user_id: string;
|
|
14
|
+
profile: { name: string };
|
|
15
|
+
status: "ACTIVE" | "INACTIVE";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface User {
|
|
19
|
+
id: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
isActive: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const userMap = defineMap<UserDto, User>()({
|
|
25
|
+
id: source("user_id"),
|
|
26
|
+
displayName: source("profile.name"),
|
|
27
|
+
isActive: transform("status", (value) => value === "ACTIVE"),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const toUser = makeMapper<UserDto, User>(userMap);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Type-Level Guarantees
|
|
34
|
+
|
|
35
|
+
`defineMap<TDto, TDomain>()` checks the mapping spec at compile time:
|
|
36
|
+
|
|
37
|
+
- every domain field must be present in the mapping spec
|
|
38
|
+
- every `source()` or `transform()` path must exist on the DTO type
|
|
39
|
+
- transform callbacks can return the final domain field value
|
|
40
|
+
|
|
41
|
+
This keeps DTO changes visible at compile time instead of letting a rename fail
|
|
42
|
+
silently at runtime.
|
|
43
|
+
|
|
44
|
+
## Runtime Behavior
|
|
45
|
+
|
|
46
|
+
Without the transformer, `makeMapper()` interprets the mapping spec at runtime:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const user = toUser(dto);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For each domain key, the mapper:
|
|
53
|
+
|
|
54
|
+
1. reads the DTO value from the configured path
|
|
55
|
+
2. applies a default when the source value is missing and a default exists
|
|
56
|
+
3. runs the transform callback when one is provided
|
|
57
|
+
4. writes the result to the domain output object
|
|
58
|
+
|
|
59
|
+
## Transformer Behavior
|
|
60
|
+
|
|
61
|
+
With the transformer enabled, this call:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const toUser = makeMapper<UserDto, User>(userMap);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
is replaced with an inline mapper function. The generated function can validate
|
|
68
|
+
the DTO input before mapping and validate the domain output after mapping.
|
|
69
|
+
|
|
70
|
+
This gives you one mapping declaration while still allowing build-time optimized
|
|
71
|
+
runtime code.
|
|
72
|
+
|
|
73
|
+
## Metadata
|
|
74
|
+
|
|
75
|
+
Mapping rules can include metadata:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
id: source("user_id", {
|
|
79
|
+
db: "users.user_id",
|
|
80
|
+
description: "User id",
|
|
81
|
+
dtoDescription: "Identifier returned by the user API.",
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Metadata is not required for mapping. It is mainly used by JSDoc generation and
|
|
86
|
+
documentation tooling.
|
|
87
|
+
|
|
88
|
+
## Typed Helpers
|
|
89
|
+
|
|
90
|
+
Use `mapperHelpers<TDto>()` when helper callbacks need DTO-aware typing:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { mapperHelpers } from "runtypex/mapper";
|
|
94
|
+
|
|
95
|
+
const h = mapperHelpers<UserDto>();
|
|
96
|
+
|
|
97
|
+
const userMap = defineMap<UserDto, User>()({
|
|
98
|
+
id: h.source("user_id"),
|
|
99
|
+
displayName: h.source("profile.name"),
|
|
100
|
+
isActive: h.transform("status", (value, dto) => {
|
|
101
|
+
return dto.status === "ACTIVE";
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Mapping Policy
|
|
2
|
+
|
|
3
|
+
Mapping policy keeps DTO path to domain field naming consistent across mappers.
|
|
4
|
+
It is useful when the same DTO field appears in multiple domain shapes.
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
Without a policy, different mappers can rename the same DTO path differently:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const userMap = defineMap<UserDto, User>()({
|
|
12
|
+
userId: source("user_id"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const auditMap = defineMap<UserDto, AuditUser>()({
|
|
16
|
+
realMemberID: source("user_id"),
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Both mappings are technically valid, but they make the domain language
|
|
21
|
+
inconsistent.
|
|
22
|
+
|
|
23
|
+
## Policy Declaration
|
|
24
|
+
|
|
25
|
+
Declare the canonical name once:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { defineMappingPolicy, source } from "runtypex/mapper";
|
|
29
|
+
|
|
30
|
+
const userPolicy = defineMappingPolicy<UserDto>()({
|
|
31
|
+
userId: source("user_id"),
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then pass the policy into a mapper:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const toAuditUser = makeMapper<UserDto, AuditUser>(auditMap, {
|
|
39
|
+
policy: userPolicy,
|
|
40
|
+
policyMode: "error",
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Modes
|
|
45
|
+
|
|
46
|
+
`policyMode` controls how violations are handled:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
policyMode: "warn"; // default, logs a warning
|
|
50
|
+
policyMode: "error"; // throws an error
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use `"warn"` while introducing a policy into existing code. Use `"error"` when
|
|
54
|
+
the naming convention should be enforced.
|
|
55
|
+
|
|
56
|
+
## Runtime And Transformer Checks
|
|
57
|
+
|
|
58
|
+
Policy validation runs in both paths:
|
|
59
|
+
|
|
60
|
+
- runtime `makeMapper()` fallback
|
|
61
|
+
- build-time `makeMapper<TDto, TDomain>()` transformer emission
|
|
62
|
+
|
|
63
|
+
That means the policy protects code whether or not the transformer is currently
|
|
64
|
+
configured.
|
|
65
|
+
|
|
66
|
+
## Duplicate Policy Entries
|
|
67
|
+
|
|
68
|
+
The policy itself must not map the same DTO path to multiple domain names:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const invalidPolicy = defineMappingPolicy<UserDto>()({
|
|
72
|
+
userId: source("user_id"),
|
|
73
|
+
realMemberID: source("user_id"),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This is treated as a policy violation because there is no single canonical
|
|
78
|
+
domain name for `user_id`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Runtime Validation
|
|
2
|
+
|
|
3
|
+
Runtime validation is the core `runtypex` feature. It turns TypeScript types into
|
|
4
|
+
runtime guard functions during build.
|
|
5
|
+
|
|
6
|
+
## APIs
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { makeAssert, makeValidate } from "runtypex";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`makeValidate<T>()` returns a predicate:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const isUser = makeValidate<User>();
|
|
16
|
+
|
|
17
|
+
if (isUser(input)) {
|
|
18
|
+
input.id;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`makeAssert<T>()` returns an assertion function:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const assertUser = makeAssert<User>();
|
|
26
|
+
|
|
27
|
+
assertUser(input);
|
|
28
|
+
input.id;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How It Works
|
|
32
|
+
|
|
33
|
+
You write this:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
interface User {
|
|
37
|
+
id: number;
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isUser = makeValidate<User>();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The transformer reads the `User` type through the TypeScript type checker and
|
|
45
|
+
replaces the call with generated JavaScript:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const isUser = (v) =>
|
|
49
|
+
typeof v === "object" &&
|
|
50
|
+
v !== null &&
|
|
51
|
+
typeof v.id === "number" &&
|
|
52
|
+
typeof v.name === "string";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No runtime reflection is required. The generated code is plain JavaScript.
|
|
56
|
+
|
|
57
|
+
## Production Removal
|
|
58
|
+
|
|
59
|
+
When `removeInProd: true` is enabled and `NODE_ENV` is `production`,
|
|
60
|
+
validators are replaced with no-op equivalents:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
makeValidate<T>(); // (_) => true
|
|
64
|
+
makeAssert<T>(); // (_) => {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use this only when runtime validation is a development-time safety net and not a
|
|
68
|
+
production boundary.
|
|
69
|
+
|
|
70
|
+
## Supported Shape Examples
|
|
71
|
+
|
|
72
|
+
The current emitter covers common TypeScript shapes:
|
|
73
|
+
|
|
74
|
+
- primitives
|
|
75
|
+
- object and interface properties
|
|
76
|
+
- optional properties
|
|
77
|
+
- arrays
|
|
78
|
+
- tuples
|
|
79
|
+
- unions
|
|
80
|
+
- intersections
|
|
81
|
+
- literal types
|
|
82
|
+
- enums
|
|
83
|
+
|
|
84
|
+
See the emitter tests for exact behavior around edge cases.
|