zodbridge 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 typescriptmapper contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,361 @@
1
+ # zodbridge
2
+
3
+ [![CI](https://github.com/khanhx/zodmapper/actions/workflows/ci.yml/badge.svg)](https://github.com/khanhx/zodmapper/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/khanhx/zodmapper/branch/main/graph/badge.svg)](https://codecov.io/gh/khanhx/zodmapper)
5
+ [![npm version](https://img.shields.io/npm/v/zodbridge.svg)](https://www.npmjs.com/package/zodbridge)
6
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
7
+
8
+ Schema-first, **decorator-free**, **Zod 4**-driven mapping toolkit for the
9
+ DB-entity ↔ response-DTO boundary. No `reflect-metadata`, no class decorators —
10
+ your Zod schema is the single source of truth.
11
+
12
+ Four composable pillars, each available as its own sub-export:
13
+
14
+ 1. **Two-way mapper** (`createMap`) — entity → DTO with auto-reversible renames.
15
+ 2. **Serialize / deserialize** — JSON-safe codec (Date ↔ ISO, BigInt ↔ string, Map ↔ entries).
16
+ 3. **Resolver graph** (`createResolver`) — dependency-aware, memoizing, cycle-guarded lazy field resolution over an injected fetch adapter.
17
+ 4. **Async mapping** (`forwardAsync`) — resolver-backed fields + a dynamic `select` set, validated with async Zod.
18
+
19
+ ```bash
20
+ npm install zodbridge zod
21
+ ```
22
+
23
+ > **Zod 4 required.** `zod` is a peer dependency pinned to `^4`. The codec
24
+ > dispatches on Zod 4's internal `_zod.def.type`; **Zod 3 is not supported.**
25
+
26
+ TypeScript ≥ 5.0 is required for the `const` type parameter `createMap` uses to
27
+ preserve bare rule literals.
28
+
29
+ ## Contents
30
+
31
+ - [1. Two-way mapper — `createMap`](#1-two-way-mapper--createmap)
32
+ - [Built-in key case conversion](#built-in-key-case-conversion)
33
+ - [2. Serialize / deserialize](#2-serialize--deserialize)
34
+ - [3. Resolver graph — `createResolver`](#3-resolver-graph--createresolver)
35
+ - [Schema-driven resolvers](#schema-driven-resolvers-validated-output)
36
+ - [4. Async mapping — `forwardAsync`](#4-async-mapping--forwardasync)
37
+ - [Map ops & resolver extras](#map-ops--resolver-extras)
38
+ - [Security](#security)
39
+ - [API surface](#api-surface)
40
+ - [Examples](#examples)
41
+ - [Contributing](#contributing)
42
+
43
+ ---
44
+
45
+ ## 1. Two-way mapper — `createMap`
46
+
47
+ The dest Zod schema defines the DTO shape. Rules describe how each dest field is
48
+ produced from the source entity:
49
+
50
+ - `'sourceKey'` — string **rename** (auto-reversible, dot-paths supported).
51
+ - `(source) => value` — **computed** function (one-way).
52
+ - `{ to, from }` — explicit **both-directions** (reversible).
53
+
54
+ ```ts
55
+ import { createMap } from "zodbridge";
56
+ import { z } from "zod";
57
+
58
+ const UserDto = z.object({
59
+ id: z.number(),
60
+ fullName: z.string(),
61
+ createdAt: z.string(),
62
+ });
63
+
64
+ const userMap = createMap(UserDto, {
65
+ fullName: (u: { first: string; last: string }) => `${u.first} ${u.last}`,
66
+ createdAt: "created_at", // rename
67
+ });
68
+
69
+ const dto = userMap.forward({ id: 1, first: "Ada", last: "Lovelace", created_at: "2024-01-01" });
70
+ // → { id: 1, fullName: "Ada Lovelace", createdAt: "2024-01-01" } (Zod-validated)
71
+
72
+ userMap.reverse(dto);
73
+ // → { created_at: "2024-01-01" } (only reversible-mapped source fields)
74
+ ```
75
+
76
+ `reverse` emits **only the mapped fields**; a one-way function rule throws
77
+ `OneWayRuleError`. Use a `{ to, from }` pair to make a transform reversible.
78
+
79
+ ### Built-in key case conversion
80
+
81
+ Every mapper instance also has `toSnakeCase` / `toCamelCase` — they recursively
82
+ re-case **all** property keys of a value to the target case, no matter the input
83
+ casing (snake, camel, Pascal, kebab, SCREAMING_SNAKE all normalize). Only keys
84
+ change; values pass through. Mixed-casing objects are converted per key — a key
85
+ that can't be tokenized is left as-is.
86
+
87
+ ```ts
88
+ userMap.toSnakeCase({ firstName: 1, CreatedAt: 2, "kebab-key": 3 });
89
+ // → { first_name: 1, created_at: 2, kebab_key: 3 }
90
+
91
+ userMap.toCamelCase({ first_name: 1, post_tags: [{ tag_name: "x" }] });
92
+ // → { firstName: 1, postTags: [{ tagName: "x" }] } (recurses objects + arrays)
93
+ ```
94
+
95
+ The same functions are exported standalone (and per-key variants too):
96
+
97
+ ```ts
98
+ import { toSnakeCase, toCamelCase, toSnakeKey, toCamelKey } from "zodbridge";
99
+ ```
100
+
101
+ Notes:
102
+ - **Collisions:** if two source keys normalize to the same target (e.g. `userId`
103
+ and `userID` → `user_id`), the last one in iteration order wins.
104
+ - **Safety:** a source key that normalizes to `constructor` or `prototype` is
105
+ dropped (never emitted) to prevent prototype pollution.
106
+ - **Idempotency:** `toSnakeCase` is idempotent. `toCamelCase` is too, except for
107
+ keys that produce adjacent single-letter words (`a-b` → `aBCD` → `aBcd`) — an
108
+ unusual shape for real DTO keys.
109
+
110
+ ---
111
+
112
+ ## 2. Serialize / deserialize
113
+
114
+ JSON-safe round-trip driven by the schema. `serialize` converts rich values to
115
+ JSON-safe plain values; `deserialize` rebuilds them and validates with Zod.
116
+
117
+ ```ts
118
+ import { serialize, deserialize } from "zodbridge/serialize";
119
+ import { z } from "zod";
120
+
121
+ const Event = z.object({ id: z.bigint(), at: z.date(), tally: z.map(z.string(), z.bigint()) });
122
+
123
+ const wire = serialize({ id: 42n, at: new Date(), tally: new Map([["a", 1n]]) }, Event);
124
+ JSON.stringify(wire); // safe — no Date/BigInt/Map left
125
+
126
+ deserialize(wire, Event); // → original rich values, Zod-validated
127
+ ```
128
+
129
+ `deserialize` is **sync-only**; a schema with async refinements throws
130
+ `AsyncSchemaError`. Malformed wire input surfaces a typed `CodecError` (never a
131
+ raw throw), and BigInt-string length / Map-entry count are bounded against DoS.
132
+
133
+ ---
134
+
135
+ ## 3. Resolver graph — `createResolver`
136
+
137
+ Declarative, dependency-aware field resolution. Each field's resolver may read
138
+ the seed, call the injected `adapter`, depend on other fields (`resolve`), or
139
+ fall back to alternate sources (`fallback`). Results are **memoized** (one fetch
140
+ per field, even under `Promise.all`), and cycles are guarded.
141
+
142
+ ```ts
143
+ import { createResolver } from "zodbridge/resolver";
144
+
145
+ interface Fields { orgId: string; org: { id: string; plan: string }; plan: string }
146
+ interface Api { getOrg: (id: string) => Promise<{ id: string; plan: string }> }
147
+
148
+ // your injected data source (DB client, HTTP API, etc.)
149
+ const api: Api = {
150
+ getOrg: async (id) => ({ id, plan: "pro" }),
151
+ };
152
+
153
+ const ctx = createResolver<Fields, Api>({
154
+ adapter: api,
155
+ seed: { orgId: "o1" },
156
+ resolvers: {
157
+ org: (c) => c.adapter.getOrg(c.get("orgId") ?? ""),
158
+ plan: async (c) => (await c.resolve("org"))?.plan,
159
+ },
160
+ });
161
+
162
+ await ctx.resolve("plan"); // → "pro" (fetches org once, derives plan)
163
+ await ctx.resolveMany("org", "plan"); // { org, plan } — org NOT re-fetched
164
+ ```
165
+
166
+ - `get(field)` — synchronous peek of seed + already-resolved values (no I/O).
167
+ - `path(field, [..keys])` — pure nested peek (never resolves, never fetches).
168
+ - `fallback([deps])` — resolve untouched deps, then retry the calling field.
169
+
170
+ A resolver that **returns `undefined`** caches `undefined` (forgiving absence).
171
+ A resolver that **throws** rejects and is **not cached** — auth/DB failures stay
172
+ loud and a later retry can succeed.
173
+
174
+ **Cycles never hang.** Mutually dependent fields are safe: if `conversation`'s
175
+ resolver does `await ctx.resolve("reservation")` and `reservation`'s does
176
+ `await ctx.resolve("conversation")`, a re-entry of a field already being resolved
177
+ in the same chain returns `undefined` (so a `strategies` candidate falls through
178
+ to its next route) — it never deadlocks. Concurrent
179
+ `Promise.all` over a shared dependency is unaffected and still dedups to one fetch.
180
+
181
+ ### Schema-driven resolvers (validated output)
182
+
183
+ Instead of a hand-written `Fields` type, derive the resolvable fields and their
184
+ schemas from `createMap` instances. Each resolver's **output is validated** against
185
+ its map's schema (unknown keys stripped) before caching — bad adapter data and
186
+ over-exposure are caught at the boundary. Pass scalar dependency fields (with no
187
+ DTO) via an optional `fields` object.
188
+
189
+ ```ts
190
+ import { createResolver } from "zodbridge/resolver";
191
+ import { createMap } from "zodbridge";
192
+ import { z } from "zod";
193
+
194
+ const userMap = createMap(z.object({ id: z.string(), name: z.string() }));
195
+ const orgMap = createMap(z.object({ orgId: z.string(), tier: z.number() }));
196
+
197
+ const api = {
198
+ getOrg: async (orgId: string) => ({ orgId, tier: 2 }),
199
+ getUser: async (orgId: string) => ({ id: `u-${orgId}`, name: "Ada" }),
200
+ };
201
+
202
+ const ctx = createResolver({
203
+ adapter: api,
204
+ maps: { user: userMap, org: orgMap }, // validated DTO fields
205
+ fields: z.object({ orgId: z.string() }), // scalar deps (no DTO)
206
+ seed: { orgId: "o1" },
207
+ resolvers: {
208
+ org: (c) => c.adapter.getOrg(c.get("orgId") ?? ""), // typed as orgMap's DTO
209
+ user: async (c) => {
210
+ const org = await c.resolve("org"); // chaining
211
+ return c.adapter.getUser(org?.orgId ?? ""); // typed as userMap's DTO
212
+ },
213
+ },
214
+ });
215
+
216
+ await ctx.resolve("user"); // → { id: "u-o1", name: "Ada" } (validated, or rejects)
217
+ ```
218
+
219
+ - Resolver output is parsed; a value that fails its schema **rejects and evicts**
220
+ (same loud-failure policy as a thrown resolver).
221
+ - **Seed is not validated** — it is trusted caller input (allows partial/scalar seed).
222
+ - The hand-written `createResolver<Fields, Adapter>(...)` signature still works
223
+ unchanged (no runtime validation when no schema is attached).
224
+
225
+ ---
226
+
227
+ ## 4. Async mapping — `forwardAsync`
228
+
229
+ Combine the mapper and the resolver. The `fromResolver` rule pulls a field from
230
+ the resolver graph; an optional `select` set restricts which fields (and which
231
+ resolver calls) run.
232
+
233
+ ```ts
234
+ import { createMap, fromResolver } from "zodbridge";
235
+ import { forwardAsync } from "zodbridge/async";
236
+ import { createResolver } from "zodbridge/resolver";
237
+ import { z } from "zod";
238
+
239
+ const PostDto = z.object({
240
+ id: z.string(),
241
+ author: z.object({ id: z.string(), name: z.string() }),
242
+ });
243
+
244
+ const postMap = createMap(PostDto, { id: "post_id", author: fromResolver("author") });
245
+
246
+ // the resolver that backs the `fromResolver("author")` rule
247
+ const resolver = createResolver<{ author: { id: string; name: string } }, object>({
248
+ adapter: {},
249
+ resolvers: { author: async () => ({ id: "a1", name: "Ada" }) },
250
+ });
251
+
252
+ await forwardAsync(postMap, { post_id: "p1" }, { resolver });
253
+ // → { id: "p1", author: { id: "a1", name: "Ada" } } (author resolved + validated)
254
+
255
+ await forwardAsync(postMap, { post_id: "p1" }, { resolver, select: ["id"] });
256
+ // → { id: "p1" } — author's resolver never fires
257
+ ```
258
+
259
+ Validation uses `safeParseAsync`, so field-level async `.refine` runs. A partial
260
+ `select` validates against a sub-schema derived from the raw `.shape` (Zod 4's
261
+ `.pick()/.partial()` throw on refined schemas, so they are never called).
262
+ Object-level cross-field refines run only on a **full** select. If any
263
+ `fromResolver` resolver throws, the whole `forwardAsync` promise rejects.
264
+
265
+ ---
266
+
267
+ ## Security
268
+
269
+ - **`select` is NOT an authorization boundary.** It is a performance/shape knob.
270
+ Enforce field-level authorization *before* passing any untrusted selection.
271
+ - **Over-exposure.** Resolver-backed fields are parsed through their own dest
272
+ sub-schema, and Zod objects `.strip()` unknown keys by default — so extra
273
+ adapter-row columns (`passwordHash`, …) are dropped. **`z.any()`,
274
+ `.passthrough()`, and `z.record()` DISABLE this** and leak the value verbatim.
275
+ Use closed object schemas for response DTOs.
276
+ - **Resolver errors propagate.** Your adapter must enforce authorization and
277
+ throw on denial; the resolver will not swallow it.
278
+ - **Prototype pollution.** The dot-path helpers reject `__proto__`, `prototype`,
279
+ and `constructor` segments, so mapping untrusted data cannot poison
280
+ `Object.prototype`.
281
+ - **DoS guards.** `deserialize` bounds BigInt-string length and Map-entry count
282
+ before transforming attacker-controlled input.
283
+
284
+ ---
285
+
286
+ ## API surface
287
+
288
+ | Import path | Exports |
289
+ |-----------------------------|---------|
290
+ | `zodbridge` | `createMap`, `fromResolver`, `withDefault`, `forwardMany`, `safeForward`, `pick`, `omit`, `compose`, `toSnakeCase`/`toCamelCase`/`toKebabCase`/`toPascalCase`/`toConstantCase` (+ per-key), `serialize`, `deserialize`, `deserializeAsync`, errors, `VERSION` |
291
+ | `zodbridge/serialize`| `serialize`, `deserialize`, `deserializeAsync`, `CodecError`, `AsyncSchemaError`, leaf transforms (incl. `setToArray`) |
292
+ | `zodbridge/resolver` | `createResolver`, `strategies`, `SKIP`, `GraphContext`, types |
293
+ | `zodbridge/async` | `forwardAsync`, `normalizeSelect`, `UnknownSelectKeyError` |
294
+
295
+ ### Map ops & resolver extras
296
+
297
+ ```ts
298
+ import { createMap, compose, pick, withDefault } from "zodbridge";
299
+ import { createResolver, strategies, SKIP } from "zodbridge/resolver";
300
+
301
+ const UserDto = z.object({ id: z.number(), name: z.string(), role: z.string() });
302
+ const userMap = createMap(UserDto, {
303
+ name: "full_name",
304
+ role: withDefault("member", "user_role"), // default when source missing
305
+ });
306
+
307
+ userMap.forwardMany([{ id: 1, full_name: "Ada" }]); // batch -> validated DTO[]
308
+ userMap.safeForward({ id: "bad" }); // { success: false, error }
309
+ pick(userMap, ["id", "name"]); // sub-DTO view (new map)
310
+
311
+ // resolver: try the fastest source first; first DEFINED value wins.
312
+ // A candidate skips by returning `undefined` or the `SKIP` sentinel — `null`
313
+ // is a winning value (return `SKIP` to skip when the value would be null).
314
+ createResolver({
315
+ adapter,
316
+ resolvers: {
317
+ conversation: strategies(
318
+ (c) => c.adapter.byReservation(c.get("reservationId")), // undefined -> skip
319
+ (c) => c.adapter.byConversationId(c.get("conversationId")) ?? SKIP, // skip even if null
320
+ ),
321
+ },
322
+ });
323
+ // force a fresh fetch (e.g. after a write):
324
+ await ctx.refresh("org");
325
+
326
+ // attach a resolver graph to the map itself:
327
+ const m = createMap(schema, rules, { resolvers: { org: (c) => c.adapter.getOrg() } });
328
+ const ctx = m.toResolver(adapter, seed); // validated against the map's schema
329
+ ```
330
+
331
+ Ships dual ESM + CJS with `.d.ts` for every entry. Zero runtime dependencies.
332
+
333
+ ## Examples
334
+
335
+ Runnable, self-contained use-cases live in [`examples/`](./examples):
336
+
337
+ | # | Example | Shows |
338
+ |---|---------|-------|
339
+ | 01 | basic mapping | `forward`/`reverse`, rename + computed + pair rules |
340
+ | 02 | case conversion | `toSnakeCase`/`toCamelCase`, value preservation |
341
+ | 03 | serialize/deserialize | Date/BigInt/Map/Set JSON round-trip |
342
+ | 04 | resolver graph | memoized resolution + `refresh` |
343
+ | 05 | smart resolve | `strategies()` fastest-first multi-source |
344
+ | 06 | async + select | `forwardAsync` with dynamic `select` |
345
+ | 07 | map ops & views | batch/safe, `withDefault`, `pick`/`omit`/`compose`, `map.toResolver` |
346
+
347
+ ```bash
348
+ npm run example examples/04-resolver-graph.ts # run one (tsx)
349
+ npm run verify:examples # typecheck them all
350
+ ```
351
+
352
+ ## Contributing
353
+
354
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). In short: TDD, keep the **100% coverage
355
+ gate** green, add a [changeset](https://github.com/changesets/changesets), and
356
+ keep changes additive. Security issues → [SECURITY.md](./SECURITY.md) (private
357
+ reporting, not public issues).
358
+
359
+ ## License
360
+
361
+ [MIT](./LICENSE)
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ // src/async/forwardAsync.ts
6
+
7
+ // src/map/rules.ts
8
+ function isStringRule(rule) {
9
+ return typeof rule === "string";
10
+ }
11
+ function isFromResolverRule(rule) {
12
+ return typeof rule === "object" && rule !== null && rule.__kind === "fromResolver";
13
+ }
14
+ function isPairRule(rule) {
15
+ return typeof rule === "object" && rule !== null && typeof rule.to === "function" && typeof rule.from === "function";
16
+ }
17
+ function isDefaultRule(rule) {
18
+ return typeof rule === "object" && rule !== null && rule.__kind === "default";
19
+ }
20
+ function isFnRule(rule) {
21
+ return typeof rule === "function";
22
+ }
23
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
24
+ function isForbidden(key) {
25
+ return FORBIDDEN_KEYS.has(key);
26
+ }
27
+ function splitPath(path) {
28
+ return path.split(".");
29
+ }
30
+ function getPath(source, path) {
31
+ let current = source;
32
+ for (const segment of splitPath(path)) {
33
+ if (isForbidden(segment)) return void 0;
34
+ if (current === null || current === void 0) return void 0;
35
+ if (typeof current !== "object") return void 0;
36
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) return void 0;
37
+ current = current[segment];
38
+ }
39
+ return current;
40
+ }
41
+ function setPath(target, path, value) {
42
+ const segments = splitPath(path);
43
+ let current = target;
44
+ for (let i = 0; i < segments.length; i++) {
45
+ const segment = segments[i];
46
+ if (isForbidden(segment)) return;
47
+ if (i === segments.length - 1) {
48
+ current[segment] = value;
49
+ return;
50
+ }
51
+ const next = current[segment];
52
+ if (typeof next !== "object" || next === null) {
53
+ const fresh = {};
54
+ current[segment] = fresh;
55
+ current = fresh;
56
+ } else {
57
+ current = next;
58
+ }
59
+ }
60
+ }
61
+
62
+ // src/map/createMap.ts
63
+ function applyForwardRule(rule, destKey, source) {
64
+ if (rule === void 0) {
65
+ return getPath(source, destKey);
66
+ }
67
+ if (isStringRule(rule)) {
68
+ return getPath(source, rule);
69
+ }
70
+ if (isPairRule(rule)) {
71
+ return rule.to(source);
72
+ }
73
+ if (isDefaultRule(rule)) {
74
+ const read = getPath(source, rule.source ?? destKey);
75
+ return read === void 0 ? rule.value : read;
76
+ }
77
+ if (isFnRule(rule)) {
78
+ return rule(source);
79
+ }
80
+ return void 0;
81
+ }
82
+
83
+ // src/async/forwardAsync.ts
84
+ var UnknownSelectKeyError = class extends Error {
85
+ keys;
86
+ constructor(keys) {
87
+ super(
88
+ `forwardAsync select contains key(s) not in the dest schema: ${keys.join(", ")}`
89
+ );
90
+ this.name = "UnknownSelectKeyError";
91
+ this.keys = keys;
92
+ }
93
+ };
94
+ function normalizeSelect(select, shape) {
95
+ if (select === void 0) return void 0;
96
+ const deduped = Array.from(new Set(select));
97
+ const unknown = deduped.filter(
98
+ (k) => !Object.prototype.hasOwnProperty.call(shape, k)
99
+ );
100
+ if (unknown.length > 0) throw new UnknownSelectKeyError(unknown);
101
+ return deduped;
102
+ }
103
+ async function stripField(value, fieldSchema) {
104
+ const result = await fieldSchema.safeParseAsync(value);
105
+ if (!result.success) return value;
106
+ return result.data;
107
+ }
108
+ async function forwardAsync(map, source, options) {
109
+ const { resolver, select } = options;
110
+ const { schema, rules, shape } = map;
111
+ const selected = normalizeSelect(select, shape);
112
+ const targetKeys = selected ?? Object.keys(shape);
113
+ const assembled = {};
114
+ for (const key of targetKeys) {
115
+ const rule = rules[key];
116
+ if (isFromResolverRule(rule)) {
117
+ const field = rule.field;
118
+ const raw = await resolver.resolve(field);
119
+ if (raw !== void 0) {
120
+ const value2 = await stripField(raw, shape[key]);
121
+ setPath(assembled, key, value2);
122
+ }
123
+ continue;
124
+ }
125
+ const produced = applyForwardRule(rule, key, source);
126
+ const value = produced instanceof Promise ? await produced : produced;
127
+ if (value !== void 0) setPath(assembled, key, value);
128
+ }
129
+ const validator = selected === void 0 ? schema : buildSubSchema(shape, selected);
130
+ const result = await validator.safeParseAsync(assembled);
131
+ if (!result.success) throw result.error;
132
+ return result.data;
133
+ }
134
+ function buildSubSchema(shape, selected) {
135
+ const picked = {};
136
+ for (const key of selected) {
137
+ picked[key] = shape[key];
138
+ }
139
+ return zod.z.object(picked);
140
+ }
141
+
142
+ exports.UnknownSelectKeyError = UnknownSelectKeyError;
143
+ exports.forwardAsync = forwardAsync;
144
+ exports.normalizeSelect = normalizeSelect;
145
+ //# sourceMappingURL=index.cjs.map
146
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/map/rules.ts","../../src/map/createMap.ts","../../src/async/forwardAsync.ts"],"names":["value","z"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Rule kinds for {@link createMap} and the dot-path get/set helpers they use.\n *\n * Rule kinds (by JS type):\n * - `string` — rename: copy `source[ruleValue]` to the dest key. Auto-reversible.\n * - `(source) => value` — computed function, one-way (no reverse unless paired).\n * - `{ to, from }` — explicit both-directions, reversible.\n * - `FromResolver` — value pulled from a resolver graph (async path, Phase 5).\n */\n\n/** A function rule: derive a dest value from the whole source object. */\nexport type FnRule<S, V> = (source: S) => V;\n\n/** A reversible rule with explicit forward (`to`) and inverse (`from`) maps. */\nexport interface PairRule<S, V> {\n to: (source: S) => V;\n from: (value: V) => unknown;\n}\n\n/** Marker for a dest field resolved from a resolver graph (consumed by Phase 5). */\nexport interface FromResolver {\n readonly __kind: \"fromResolver\";\n readonly field: string;\n}\n\n/**\n * Default-value rule: read `source` (a source key, defaults to the dest key)\n * and fall back to `value` when the source is `undefined`. Reverse-safe: the\n * default is dropped on reverse and only the read key round-trips.\n */\nexport interface DefaultRule<V = unknown> {\n readonly __kind: \"default\";\n readonly value: V;\n readonly source?: string;\n}\n\n/** Any rule a dest key may be configured with. */\nexport type Rule<S = any, V = any> =\n | string\n | FnRule<S, V>\n | PairRule<S, V>\n | FromResolver\n | DefaultRule<V>;\n\n/** Build a {@link FromResolver} marker for `field` in the resolver graph. */\nexport function fromResolver(field: string): FromResolver {\n return { __kind: \"fromResolver\", field };\n}\n\n/**\n * Build a {@link DefaultRule}: use `source` (or the dest key) from the entity,\n * falling back to `value` when absent.\n */\nexport function withDefault<V>(value: V, source?: string): DefaultRule<V> {\n return { __kind: \"default\", value, source };\n}\n\n// --- type guards ---\n\nexport function isStringRule(rule: Rule): rule is string {\n return typeof rule === \"string\";\n}\n\nexport function isFromResolverRule(rule: Rule): rule is FromResolver {\n return (\n typeof rule === \"object\" &&\n rule !== null &&\n (rule as FromResolver).__kind === \"fromResolver\"\n );\n}\n\nexport function isPairRule(rule: Rule): rule is PairRule<any, any> {\n return (\n typeof rule === \"object\" &&\n rule !== null &&\n typeof (rule as PairRule<any, any>).to === \"function\" &&\n typeof (rule as PairRule<any, any>).from === \"function\"\n );\n}\n\nexport function isDefaultRule(rule: Rule): rule is DefaultRule {\n return (\n typeof rule === \"object\" &&\n rule !== null &&\n (rule as DefaultRule).__kind === \"default\"\n );\n}\n\nexport function isFnRule(rule: Rule): rule is FnRule<any, any> {\n return typeof rule === \"function\";\n}\n\n// --- prototype-pollution-safe dot-path helpers ---\n\n/** Path segments that, if written, could poison `Object.prototype`. */\nconst FORBIDDEN_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\n\nfunction isForbidden(key: string): boolean {\n return FORBIDDEN_KEYS.has(key);\n}\n\n/** Split a dot-path into segments. A plain key (no dot) yields a single segment. */\nexport function splitPath(path: string): string[] {\n return path.split(\".\");\n}\n\n/**\n * Read a nested value by dot-path. Returns `undefined` if any segment is\n * missing or if a segment is a forbidden prototype key (read is skipped, not\n * served from the prototype chain).\n */\nexport function getPath(source: unknown, path: string): unknown {\n let current: unknown = source;\n for (const segment of splitPath(path)) {\n if (isForbidden(segment)) return undefined;\n if (current === null || current === undefined) return undefined;\n if (typeof current !== \"object\") return undefined;\n if (!Object.prototype.hasOwnProperty.call(current, segment)) return undefined;\n current = (current as Record<string, unknown>)[segment];\n }\n return current;\n}\n\n/**\n * Write a nested value by dot-path, auto-vivifying intermediate objects.\n * Forbidden prototype keys at any segment are skipped entirely, so a hostile\n * `__proto__.isAdmin` path can never mutate `Object.prototype`.\n */\nexport function setPath(target: Record<string, unknown>, path: string, value: unknown): void {\n const segments = splitPath(path);\n let current: Record<string, unknown> = target;\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i] as string;\n if (isForbidden(segment)) return;\n if (i === segments.length - 1) {\n current[segment] = value;\n return;\n }\n const next = current[segment];\n if (typeof next !== \"object\" || next === null) {\n const fresh: Record<string, unknown> = {};\n current[segment] = fresh;\n current = fresh;\n } else {\n current = next as Record<string, unknown>;\n }\n }\n}\n","/**\n * `createMap(destSchema, rules)` — synchronous, Zod-validated entity -> DTO\n * mapper. The dest Zod schema is the shape source of truth. Simple string\n * renames and `{ to, from }` pairs auto-reverse; computed function rules are\n * one-way unless paired.\n */\nimport type { z } from \"zod\";\nimport { createResolver } from \"../resolver/createResolver.js\";\nimport type { ResolverContext, ResolverRegistry } from \"../resolver/context.js\";\nimport {\n type CamelCased,\n type SnakeCased,\n toCamelCase,\n toSnakeCase,\n} from \"./case-convert.js\";\nimport {\n type SafeResult,\n forwardMany as forwardManyOp,\n reverseMany as reverseManyOp,\n safeForward as safeForwardOp,\n} from \"./map-ops.js\";\nimport {\n type DefaultRule,\n type FromResolver,\n type Rule,\n getPath,\n isDefaultRule,\n isFnRule,\n isFromResolverRule,\n isPairRule,\n isStringRule,\n setPath,\n} from \"./rules.js\";\n\n/**\n * Resolver graph declared on a map: each field of the dest DTO `Dest` may have\n * a resolver typed against `Dest` and the adapter `TAdapter`.\n */\nexport type MapResolvers<Dest = Record<string, unknown>, TAdapter = unknown> =\n ResolverRegistry<Dest, TAdapter>;\n\n/** Options for {@link createMap}, parameterized by the dest DTO and adapter type. */\nexport interface CreateMapOptions<Dest = Record<string, unknown>, TAdapter = unknown> {\n /**\n * Resolver graph for this map's fields, against the map's own dest schema.\n * Enables `map.toResolver(adapter)` and resolver-backed `forwardAsync` without\n * a separate `createResolver` call. Each resolver's output is typed against the\n * DTO field it produces. NOTE: `ctx.adapter` is typed as the concrete adapter\n * only at the `toResolver(adapter)` call site — inside these declarations it is\n * `unknown` (the adapter is not known at `createMap` time); cast it if needed.\n */\n resolvers?: MapResolvers<Dest, TAdapter>;\n}\n\n/** Thrown when `reverse` meets a one-way function rule that has no inverse. */\nexport class OneWayRuleError extends Error {\n readonly key: string;\n constructor(key: string) {\n super(\n `Cannot reverse dest key \"${key}\": it is mapped by a one-way function ` +\n `rule. Use a { to, from } pair rule to make it reversible.`,\n );\n this.name = \"OneWayRuleError\";\n this.key = key;\n }\n}\n\n/** Thrown when `toResolver` is called on a map that declared no resolvers. */\nexport class MapHasNoResolversError extends Error {\n constructor() {\n super(\n \"createMap.toResolver requires a resolver graph: pass \" +\n \"`createMap(schema, rules, { resolvers })`.\",\n );\n this.name = \"MapHasNoResolversError\";\n }\n}\n\n/** Thrown at build time when two rules reverse onto the same source key. */\nexport class ReverseKeyCollisionError extends Error {\n constructor(sourceKey: string, a: string, b: string) {\n super(\n `Reverse-key collision: dest keys \"${a}\" and \"${b}\" both map back to ` +\n `source key \"${sourceKey}\". Disambiguate one of them.`,\n );\n this.name = \"ReverseKeyCollisionError\";\n }\n}\n\ntype AnyZodObject = z.ZodObject<any>;\n\n/** Rule map: each dest key may carry a rule; unmapped keys are copied by name. */\nexport type RuleMap<Dest> = Partial<Record<keyof Dest & string, Rule>>;\n\nexport interface TypeMapper<S extends AnyZodObject, R extends RuleMap<z.infer<S>>> {\n /** Build a validated DTO from a source entity (sync path). */\n forward(source: Record<string, unknown>): z.infer<S>;\n /** Forward every item in a list, returning the validated DTO array. */\n forwardMany(sources: Array<Record<string, unknown>>): Array<z.infer<S>>;\n /** Forward without throwing: `{ success, data }` or `{ success: false, error }`. */\n safeForward(source: Record<string, unknown>): SafeResult<z.infer<S>>;\n /** Invert reversible rules; returns only mapped source fields. */\n reverse(dto: z.infer<S>): Partial<Record<string, unknown>>;\n /** Reverse every DTO in a list. */\n reverseMany(dtos: Array<z.infer<S>>): Array<Partial<Record<string, unknown>>>;\n /**\n * Recursively re-case ALL property keys of `value` to `snake_case`,\n * regardless of their original casing. Keys only; values pass through.\n */\n toSnakeCase<T>(value: T): SnakeCased<T>;\n /**\n * Recursively re-case ALL property keys of `value` to `camelCase`,\n * regardless of their original casing. Keys only; values pass through.\n */\n toCamelCase<T>(value: T): CamelCased<T>;\n /**\n * Build a resolver graph from THIS map's schema fields plus the resolver\n * graph declared in `createMap`'s options. The resolver validates each field's\n * output against the matching dest sub-schema (over-exposure protection).\n * Throws if no `resolvers` were declared on the map.\n */\n toResolver<TAdapter>(\n adapter: TAdapter,\n seed?: Partial<z.infer<S>>,\n ): ResolverContext<z.infer<S>, TAdapter>;\n /** The dest schema (shape source of truth). */\n readonly schema: S;\n /** Configured rules, frozen. */\n readonly rules: R;\n /** Raw `.shape` of the dest object — used by the async path (Phase 5). */\n readonly shape: Record<string, z.ZodType>;\n /** Resolver graph declared on this map (if any), for `toResolver`/forwardAsync. */\n readonly resolvers?: MapResolvers<z.infer<S>>;\n}\n\n/** Apply a single forward rule for `destKey`, returning the mapped value. */\nexport function applyForwardRule(\n rule: Rule | undefined,\n destKey: string,\n source: Record<string, unknown>,\n): unknown {\n if (rule === undefined) {\n // No rule: copy by matching dest key from source (dot-path aware).\n return getPath(source, destKey);\n }\n if (isStringRule(rule)) {\n return getPath(source, rule);\n }\n if (isPairRule(rule)) {\n return rule.to(source);\n }\n if (isDefaultRule(rule)) {\n const read = getPath(source, rule.source ?? destKey);\n return read === undefined ? rule.value : read;\n }\n if (isFnRule(rule)) {\n return rule(source);\n }\n // FromResolver markers are resolved only on the async path; sync forward\n // treats them as absent (the field is filled by forwardAsync).\n return undefined;\n}\n\n/**\n * Build a two-way mapper. The `const` type parameter on `R` preserves bare\n * object-literal rule values (string-literal renames) without the caller\n * writing `as const` (requires TypeScript >= 5.0).\n */\nexport function createMap<\n S extends AnyZodObject,\n const R extends RuleMap<z.infer<S>>,\n>(\n schema: S,\n rules: R = {} as R,\n options: CreateMapOptions<z.infer<S>> = {},\n): TypeMapper<S, R> {\n const shape = schema.shape as Record<string, z.ZodType>;\n const destKeys = Object.keys(shape);\n const mapResolvers = options.resolvers;\n\n // Build-time reverse-collision detection: any two reversible rules whose\n // inverse targets the same source key are a configuration error.\n const reverseTargets = new Map<string, string>();\n for (const destKey of Object.keys(rules) as Array<keyof R & string>) {\n const rule = rules[destKey] as Rule | undefined;\n if (rule !== undefined && isStringRule(rule)) {\n const existing = reverseTargets.get(rule);\n if (existing) throw new ReverseKeyCollisionError(rule, existing, destKey);\n reverseTargets.set(rule, destKey);\n }\n }\n\n function forward(source: Record<string, unknown>): z.infer<S> {\n const out: Record<string, unknown> = {};\n for (const destKey of destKeys) {\n const rule = rules[destKey as keyof R & string] as Rule | undefined;\n if (isFromResolverRule(rule as Rule)) continue; // async-only\n const value = applyForwardRule(rule, destKey, source);\n if (value !== undefined) setPath(out, destKey, value);\n }\n return schema.parse(out) as z.infer<S>;\n }\n\n function reverse(dto: z.infer<S>): Partial<Record<string, unknown>> {\n const out: Record<string, unknown> = {};\n const dtoObj = dto as Record<string, unknown>;\n for (const destKey of Object.keys(rules) as Array<keyof R & string>) {\n const rule = rules[destKey] as Rule | undefined;\n if (rule === undefined) continue;\n const destValue = getPath(dtoObj, destKey);\n if (isStringRule(rule)) {\n setPath(out, rule, destValue);\n } else if (isPairRule(rule)) {\n setPath(out, destKey, rule.from(destValue));\n } else if (isDefaultRule(rule)) {\n // Round-trip the read key; the default itself is forward-only.\n setPath(out, (rule as DefaultRule).source ?? destKey, destValue);\n } else if (isFromResolverRule(rule as Rule)) {\n // Resolver-backed fields have no source inverse; skip.\n continue;\n } else {\n // One-way function rule, no inverse.\n throw new OneWayRuleError(destKey);\n }\n }\n return out;\n }\n\n type Dest = z.infer<S>;\n\n function toResolver<TAdapter>(\n adapter: TAdapter,\n seed?: Partial<Dest>,\n ): ResolverContext<Dest, TAdapter> {\n if (!mapResolvers) {\n throw new MapHasNoResolversError();\n }\n // Per-field schemas from this map's own shape (over-exposure protection).\n const schemas: Record<string, z.ZodType> = {};\n for (const key of Object.keys(shape)) {\n schemas[key] = shape[key] as z.ZodType;\n }\n return createResolver<Dest, TAdapter>({\n adapter,\n seed,\n resolvers: mapResolvers as ResolverRegistry<Dest, TAdapter>,\n schemas: schemas as Record<keyof Dest, z.ZodType>,\n });\n }\n\n const mapper: TypeMapper<S, R> = {\n forward,\n reverse,\n forwardMany: (sources) => forwardManyOp(mapper, sources) as Array<Dest>,\n reverseMany: (dtos) => reverseManyOp(mapper, dtos),\n safeForward: (source) => safeForwardOp(mapper, source) as SafeResult<Dest>,\n toSnakeCase,\n toCamelCase,\n toResolver,\n schema,\n rules: Object.freeze({ ...rules }) as R,\n shape,\n resolvers: mapResolvers,\n };\n return mapper;\n}\n\nexport type { FromResolver };\n","/**\n * `forwardAsync` — the async mapping path. Consumes a resolver graph (Phase 4)\n * for `fromResolver` fields, honors a dynamic `select` set so only requested\n * fields (and their resolver deps) do I/O, and validates with Zod\n * `safeParseAsync` (so async `.refine` runs).\n *\n * Zod 4 constraint: `.pick()`/`.partial()` THROW on object schemas containing\n * refinements, which is exactly the async-refined shape this targets. So a\n * partial `select` is validated against a sub-schema built from the RAW\n * `.shape` (`z.object(pickedShape)`), never by calling `.pick()` on the refined\n * wrapper. Field-level refines on selected fields still run; object-level\n * cross-field refines run only on a full select (documented limitation).\n */\nimport { z } from \"zod\";\nimport { applyForwardRule } from \"../map/createMap.js\";\nimport {\n type FromResolver,\n type Rule,\n isFromResolverRule,\n setPath,\n} from \"../map/rules.js\";\nimport type { ResolverContext } from \"../resolver/context.js\";\n\n/** Thrown when `select` names a key absent from the dest schema. */\nexport class UnknownSelectKeyError extends Error {\n readonly keys: string[];\n constructor(keys: string[]) {\n super(\n `forwardAsync select contains key(s) not in the dest schema: ${keys.join(\", \")}`,\n );\n this.name = \"UnknownSelectKeyError\";\n this.keys = keys;\n }\n}\n\ntype AnyZodObject = z.ZodObject<any>;\n\ninterface ForwardAsyncMap<S extends AnyZodObject> {\n readonly schema: S;\n readonly rules: Record<string, Rule | undefined>;\n readonly shape: Record<string, z.ZodType>;\n}\n\nexport interface ForwardAsyncOptions<Fields, TAdapter> {\n resolver: ResolverContext<Fields, TAdapter>;\n /** Only these dest keys are computed; unselected resolvers never fire. */\n select?: string[];\n /** Opaque extra passed to sync function rules via the source (unused hook). */\n context?: unknown;\n}\n\n/**\n * Validate and de-duplicate a `select` list against the dest shape.\n * Unknown keys -> typed {@link UnknownSelectKeyError} (never a raw Zod trace).\n */\nexport function normalizeSelect(\n select: string[] | undefined,\n shape: Record<string, z.ZodType>,\n): string[] | undefined {\n if (select === undefined) return undefined;\n const deduped = Array.from(new Set(select));\n const unknown = deduped.filter(\n (k) => !Object.prototype.hasOwnProperty.call(shape, k),\n );\n if (unknown.length > 0) throw new UnknownSelectKeyError(unknown);\n return deduped;\n}\n\n/**\n * Strip a resolver-produced value through its own dest field sub-schema so\n * unknown adapter-row keys (e.g. `passwordHash`) are dropped. NOTE: a field\n * typed `z.any()`/`.passthrough()`/`z.record()` disables this — those leak the\n * value verbatim by design (documented in the README security section).\n */\nasync function stripField(value: unknown, fieldSchema: z.ZodType): Promise<unknown> {\n const result = await fieldSchema.safeParseAsync(value);\n if (!result.success) return value; // let the full validation surface the error\n return result.data;\n}\n\n/**\n * Build a validated DTO asynchronously. `fromResolver` fields pull from the\n * resolver; a resolver throw rejects the whole promise (fail-fast). Returns the\n * full DTO, or a `Partial<Dto>` when `select` is given.\n */\nexport async function forwardAsync<S extends AnyZodObject, Fields, TAdapter>(\n map: ForwardAsyncMap<S>,\n source: Record<string, unknown>,\n options: ForwardAsyncOptions<Fields, TAdapter>,\n): Promise<Partial<z.infer<S>>> {\n const { resolver, select } = options;\n const { schema, rules, shape } = map;\n\n const selected = normalizeSelect(select, shape);\n const targetKeys = selected ?? Object.keys(shape);\n\n // Fields resolve sequentially (await per key). The resolver's promise-cache\n // (Phase 4) makes Promise.all safe too, but sequential keeps resolver\n // invocations non-overlapping and is the documented default.\n const assembled: Record<string, unknown> = {};\n for (const key of targetKeys) {\n const rule = rules[key];\n if (isFromResolverRule(rule as Rule)) {\n const field = (rule as FromResolver).field;\n // A resolver throw propagates out of forwardAsync (fail-fast).\n const raw = await resolver.resolve(field as keyof Fields);\n if (raw !== undefined) {\n // `key` is always a key of `shape` (it came from shape keys or a\n // select list validated against shape), so its field schema exists.\n const value = await stripField(raw, shape[key] as z.ZodType);\n setPath(assembled, key, value);\n }\n continue;\n }\n // Sync/computed rules reuse the Phase 3 primitive; an async function rule\n // returns a promise, which we await.\n const produced = applyForwardRule(rule, key, source);\n const value = produced instanceof Promise ? await produced : produced;\n if (value !== undefined) setPath(assembled, key, value);\n }\n\n // Validate. Full select -> the original schema (all refines run). Partial\n // select -> a sub-schema built from raw .shape (avoids .pick on refined).\n const validator =\n selected === undefined ? schema : buildSubSchema(shape, selected);\n const result = await validator.safeParseAsync(assembled);\n if (!result.success) throw result.error;\n return result.data as Partial<z.infer<S>>;\n}\n\n/**\n * Construct a partial-select validator from the dest schema's raw `.shape`,\n * via `z.object(pickedShape)`. Refined wrappers are never `.pick()`-ed (which\n * throws in Zod 4); object-level cross-field refines are intentionally omitted\n * here and run only on a full select.\n */\nfunction buildSubSchema(\n shape: Record<string, z.ZodType>,\n selected: string[],\n): z.ZodType {\n const picked: Record<string, z.ZodType> = {};\n for (const key of selected) {\n // `selected` was validated against `shape` by normalizeSelect, so each\n // key is present.\n picked[key] = shape[key] as z.ZodType;\n }\n return z.object(picked);\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import { R as Rule } from '../rules-msHkZDR8.cjs';
3
+ import { R as ResolverContext } from '../context-B0f9mQWu.cjs';
4
+
5
+ /**
6
+ * `forwardAsync` — the async mapping path. Consumes a resolver graph (Phase 4)
7
+ * for `fromResolver` fields, honors a dynamic `select` set so only requested
8
+ * fields (and their resolver deps) do I/O, and validates with Zod
9
+ * `safeParseAsync` (so async `.refine` runs).
10
+ *
11
+ * Zod 4 constraint: `.pick()`/`.partial()` THROW on object schemas containing
12
+ * refinements, which is exactly the async-refined shape this targets. So a
13
+ * partial `select` is validated against a sub-schema built from the RAW
14
+ * `.shape` (`z.object(pickedShape)`), never by calling `.pick()` on the refined
15
+ * wrapper. Field-level refines on selected fields still run; object-level
16
+ * cross-field refines run only on a full select (documented limitation).
17
+ */
18
+
19
+ /** Thrown when `select` names a key absent from the dest schema. */
20
+ declare class UnknownSelectKeyError extends Error {
21
+ readonly keys: string[];
22
+ constructor(keys: string[]);
23
+ }
24
+ type AnyZodObject = z.ZodObject<any>;
25
+ interface ForwardAsyncMap<S extends AnyZodObject> {
26
+ readonly schema: S;
27
+ readonly rules: Record<string, Rule | undefined>;
28
+ readonly shape: Record<string, z.ZodType>;
29
+ }
30
+ interface ForwardAsyncOptions<Fields, TAdapter> {
31
+ resolver: ResolverContext<Fields, TAdapter>;
32
+ /** Only these dest keys are computed; unselected resolvers never fire. */
33
+ select?: string[];
34
+ /** Opaque extra passed to sync function rules via the source (unused hook). */
35
+ context?: unknown;
36
+ }
37
+ /**
38
+ * Validate and de-duplicate a `select` list against the dest shape.
39
+ * Unknown keys -> typed {@link UnknownSelectKeyError} (never a raw Zod trace).
40
+ */
41
+ declare function normalizeSelect(select: string[] | undefined, shape: Record<string, z.ZodType>): string[] | undefined;
42
+ /**
43
+ * Build a validated DTO asynchronously. `fromResolver` fields pull from the
44
+ * resolver; a resolver throw rejects the whole promise (fail-fast). Returns the
45
+ * full DTO, or a `Partial<Dto>` when `select` is given.
46
+ */
47
+ declare function forwardAsync<S extends AnyZodObject, Fields, TAdapter>(map: ForwardAsyncMap<S>, source: Record<string, unknown>, options: ForwardAsyncOptions<Fields, TAdapter>): Promise<Partial<z.infer<S>>>;
48
+
49
+ export { type ForwardAsyncOptions, UnknownSelectKeyError, forwardAsync, normalizeSelect };