@ayepi/mock 0.1.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 +21 -0
- package/README.md +49 -0
- package/dist/index.cjs +448 -0
- package/dist/index.d.cts +109 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +445 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philip Diffenderfer
|
|
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,49 @@
|
|
|
1
|
+
# @ayepi/mock
|
|
2
|
+
|
|
3
|
+
Generate **schema-valid fake data** from an [`@ayepi/core`](../core) spec. Turn any
|
|
4
|
+
`spec(...)` into a real server whose endpoints return generated bodies that parse against
|
|
5
|
+
their own response schemas — great for frontend dev, demos, and tests before the backend
|
|
6
|
+
exists.
|
|
7
|
+
|
|
8
|
+
- **Deterministic by default** — the PRNG is seeded from `seed + endpoint + JSON(request)`,
|
|
9
|
+
so identical inputs yield identical output and pagination is naturally stable.
|
|
10
|
+
- **Generic over zod** — deeply walks objects, arrays, unions, enums, string formats, and
|
|
11
|
+
wrappers; no per-endpoint fixtures.
|
|
12
|
+
- **Pagination** — array sizes follow a `limit`-style query key (configurable), clamped to
|
|
13
|
+
schema length bounds.
|
|
14
|
+
- **Overrides** — pin values by field name/path or by zod string format.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pnpm add -D @ayepi/mock # peers: @ayepi/core, zod
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { mockServer, generate } from '@ayepi/mock'
|
|
22
|
+
import { z } from 'zod'
|
|
23
|
+
|
|
24
|
+
// one-off value from any schema
|
|
25
|
+
generate(z.object({ id: z.uuid(), name: z.string() }))
|
|
26
|
+
|
|
27
|
+
// a real server with generated, schema-valid responses
|
|
28
|
+
const app = mockServer(api, { seed: 1, arraySize: 5 })
|
|
29
|
+
const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Exports
|
|
33
|
+
|
|
34
|
+
- `generate(schema, opts?, ctx?)` — one fake value from a zod schema.
|
|
35
|
+
- `mockHandlers(spec, opts?)` — a handler bag for `server(spec, [implement(spec).handlers(bag)])`.
|
|
36
|
+
- `mockServer(spec, opts?)` — a real ayepi `Server` with generated responses.
|
|
37
|
+
|
|
38
|
+
See [`ayepi-mock.md`](./ayepi-mock.md) for the full reference (options, seeding,
|
|
39
|
+
determinism, pagination, overrides, supported zod types, and gotchas).
|
|
40
|
+
|
|
41
|
+
## For AI coding agents
|
|
42
|
+
|
|
43
|
+
This package ships dense, machine-oriented reference docs written for **AI coding agents**
|
|
44
|
+
(Claude Code, Cursor, and the like) to understand and drive the package — point your agent at them:
|
|
45
|
+
|
|
46
|
+
- [`ayepi-mock.md`](./ayepi-mock.md)
|
|
47
|
+
|
|
48
|
+
They live next to the source in the [repo](https://github.com/ClickerMonkey/ayepi/tree/main/packages/mock) and are **not** shipped in the npm tarball.
|
|
49
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _ayepi_core = require("@ayepi/core");
|
|
3
|
+
//#region src/rng.ts
|
|
4
|
+
/**
|
|
5
|
+
* # Seeded PRNG
|
|
6
|
+
*
|
|
7
|
+
* A tiny, dependency-free deterministic random number generator. {@link fnv1a}
|
|
8
|
+
* folds an arbitrary string into a 32-bit seed; {@link mulberry32} turns that
|
|
9
|
+
* seed into a `() => number` stream of floats in `[0, 1)`. Given the same seed
|
|
10
|
+
* the stream is byte-for-byte reproducible, which is what makes deterministic
|
|
11
|
+
* mocking — and therefore stable pagination — possible.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/** FNV-1a offset basis (32-bit). */
|
|
16
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
17
|
+
/** FNV-1a prime (32-bit). */
|
|
18
|
+
const FNV_PRIME = 16777619;
|
|
19
|
+
/** Mask to keep arithmetic within 32 unsigned bits. */
|
|
20
|
+
const U32_MASK = 4294967295;
|
|
21
|
+
/** mulberry32 mixing constant added to the running state each step. */
|
|
22
|
+
const MULBERRY_INCREMENT = 1831565813;
|
|
23
|
+
/** mulberry32 second-round multiply constant. */
|
|
24
|
+
const MULBERRY_ROUND2 = 3266489909;
|
|
25
|
+
/** Divisor that maps a 32-bit unsigned integer into `[0, 1)`. */
|
|
26
|
+
const U32_DIVISOR = 4294967296;
|
|
27
|
+
/**
|
|
28
|
+
* Hash a string into a 32-bit unsigned integer with FNV-1a.
|
|
29
|
+
*
|
|
30
|
+
* Used to fold a composite seed (`baseSeed + endpoint + JSON(request)`) down to a
|
|
31
|
+
* single integer the PRNG can be initialized from.
|
|
32
|
+
*/
|
|
33
|
+
function fnv1a(input) {
|
|
34
|
+
let hash = FNV_OFFSET_BASIS;
|
|
35
|
+
for (let i = 0; i < input.length; i++) {
|
|
36
|
+
hash ^= input.charCodeAt(i);
|
|
37
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
38
|
+
}
|
|
39
|
+
return hash >>> 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a deterministic `() => number` PRNG (floats in `[0, 1)`) from a 32-bit
|
|
43
|
+
* seed using the mulberry32 algorithm. Identical seeds yield identical streams.
|
|
44
|
+
*/
|
|
45
|
+
function mulberry32(seed) {
|
|
46
|
+
let state = seed >>> 0;
|
|
47
|
+
return () => {
|
|
48
|
+
state = state + MULBERRY_INCREMENT & U32_MASK;
|
|
49
|
+
let t = state;
|
|
50
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
51
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | MULBERRY_ROUND2);
|
|
52
|
+
t = (t ^ t >>> 14) >>> 0;
|
|
53
|
+
return t / U32_DIVISOR;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convenience: hash a string seed and return a fresh PRNG stream for it.
|
|
58
|
+
*
|
|
59
|
+
* @param parts - composite seed parts joined into the hash input.
|
|
60
|
+
*/
|
|
61
|
+
function rngFromParts(...parts) {
|
|
62
|
+
return mulberry32(fnv1a(parts.join("\0")));
|
|
63
|
+
}
|
|
64
|
+
/** Default query keys whose value sizes generated arrays. */
|
|
65
|
+
const DEFAULT_LIMIT_KEYS = [
|
|
66
|
+
"limit",
|
|
67
|
+
"pageSize",
|
|
68
|
+
"count"
|
|
69
|
+
];
|
|
70
|
+
/** Default base seed when none is supplied. */
|
|
71
|
+
const DEFAULT_SEED = "0";
|
|
72
|
+
/** Fallback string length used when a format/length constraint gives no better hint. */
|
|
73
|
+
const WORD_LENGTH = 8;
|
|
74
|
+
/** Default integer ceiling (exclusive) for unconstrained number generation. */
|
|
75
|
+
const INT_RANGE = 1e3;
|
|
76
|
+
/** Decimal places kept for unconstrained floats. */
|
|
77
|
+
const FLOAT_PRECISION = 1e3;
|
|
78
|
+
/** Probability an optional/nullable value is *present* (non-undefined / non-null). */
|
|
79
|
+
const PRESENCE_PROB = .7;
|
|
80
|
+
/** Record entry count when generating from a `z.record`. */
|
|
81
|
+
const RECORD_SIZE = 2;
|
|
82
|
+
/** Bound used for bigint generation when unconstrained. */
|
|
83
|
+
const BIGINT_RANGE = 1e3;
|
|
84
|
+
/** Number `format` values that require an integer result. */
|
|
85
|
+
const INT_FORMATS = new Set([
|
|
86
|
+
"safeint",
|
|
87
|
+
"int32",
|
|
88
|
+
"uint32"
|
|
89
|
+
]);
|
|
90
|
+
/** Characters drawn from when padding/building plain strings. */
|
|
91
|
+
const ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
92
|
+
/** Hex characters for uuid/id-shaped formats. */
|
|
93
|
+
const HEX = "0123456789abcdef";
|
|
94
|
+
/** Read a schema's internal `_zod.def`. */
|
|
95
|
+
function def(schema) {
|
|
96
|
+
return schema._zod.def;
|
|
97
|
+
}
|
|
98
|
+
/** Read the (possibly empty) list of check descriptors on a schema. */
|
|
99
|
+
function checks(d) {
|
|
100
|
+
return (d.checks ?? []).map((c) => c._zod.def);
|
|
101
|
+
}
|
|
102
|
+
/** Pull min/max/int constraints off a number schema's checks (and its `format`). */
|
|
103
|
+
function numBounds(d) {
|
|
104
|
+
let min = null;
|
|
105
|
+
let max = null;
|
|
106
|
+
let int = d.format !== void 0 && INT_FORMATS.has(d.format);
|
|
107
|
+
for (const c of checks(d)) if (c.check === "greater_than" && typeof c.value === "number") min = c.inclusive ? c.value : c.value + 1;
|
|
108
|
+
else if (c.check === "less_than" && typeof c.value === "number") max = c.inclusive ? c.value : c.value - 1;
|
|
109
|
+
else if (c.check === "number_format") int = true;
|
|
110
|
+
return {
|
|
111
|
+
min,
|
|
112
|
+
max,
|
|
113
|
+
int
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Pull min/max length constraints off a string/array schema's checks. */
|
|
117
|
+
function lenBounds(d) {
|
|
118
|
+
let min = null;
|
|
119
|
+
let max = null;
|
|
120
|
+
for (const c of checks(d)) if (c.check === "min_length" && typeof c.minimum === "number") min = c.minimum;
|
|
121
|
+
else if (c.check === "max_length" && typeof c.maximum === "number") max = c.maximum;
|
|
122
|
+
else if (c.check === "length_equals" && typeof c.length === "number") min = max = c.length;
|
|
123
|
+
return {
|
|
124
|
+
min,
|
|
125
|
+
max
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Draw an integer in `[lo, hi]` (inclusive) from the rng. */
|
|
129
|
+
function intIn(rng, lo, hi) {
|
|
130
|
+
/* v8 ignore next */ if (hi < lo) return lo;
|
|
131
|
+
return lo + Math.floor(rng() * (hi - lo + 1));
|
|
132
|
+
}
|
|
133
|
+
/** Pick a random element of a non-empty array. */
|
|
134
|
+
function pick(rng, arr) {
|
|
135
|
+
return arr[Math.floor(rng() * arr.length)];
|
|
136
|
+
}
|
|
137
|
+
/** Build a lowercase word of `len` characters. */
|
|
138
|
+
function word(rng, len) {
|
|
139
|
+
let s = "";
|
|
140
|
+
for (let i = 0; i < len; i++) s += ALPHABET[Math.floor(rng() * 26)];
|
|
141
|
+
return s;
|
|
142
|
+
}
|
|
143
|
+
/** Build a hex string of `len` characters. */
|
|
144
|
+
function hex(rng, len) {
|
|
145
|
+
let s = "";
|
|
146
|
+
for (let i = 0; i < len; i++) s += HEX[Math.floor(rng() * 16)];
|
|
147
|
+
return s;
|
|
148
|
+
}
|
|
149
|
+
/** Build a value for a detected/declared zod string `format`. */
|
|
150
|
+
function formatValue(format, rng, now) {
|
|
151
|
+
switch (format) {
|
|
152
|
+
case "email": return `${word(rng, WORD_LENGTH)}@example.com`;
|
|
153
|
+
case "url": return `https://${word(rng, WORD_LENGTH)}.example.com`;
|
|
154
|
+
case "uuid":
|
|
155
|
+
case "guid": return `${hex(rng, 8)}-${hex(rng, 4)}-4${hex(rng, 3)}-8${hex(rng, 3)}-${hex(rng, 12)}`;
|
|
156
|
+
case "datetime": return new Date(now).toISOString();
|
|
157
|
+
case "date": return new Date(now).toISOString().slice(0, 10);
|
|
158
|
+
case "time": return new Date(now).toISOString().slice(11, 23);
|
|
159
|
+
case "duration": return `PT${intIn(rng, 1, 60)}M`;
|
|
160
|
+
case "ipv4": return `${intIn(rng, 1, 254)}.${intIn(rng, 0, 255)}.${intIn(rng, 0, 255)}.${intIn(rng, 1, 254)}`;
|
|
161
|
+
case "ipv6": return `${hex(rng, 4)}:${hex(rng, 4)}::${hex(rng, 4)}`;
|
|
162
|
+
case "cuid":
|
|
163
|
+
case "cuid2": return `c${word(rng, 24)}`;
|
|
164
|
+
case "ulid": return hex(rng, 26).toUpperCase();
|
|
165
|
+
case "nanoid": return word(rng, 21);
|
|
166
|
+
case "emoji": return pick(rng, [
|
|
167
|
+
"😀",
|
|
168
|
+
"🎉",
|
|
169
|
+
"🚀",
|
|
170
|
+
"🌟",
|
|
171
|
+
"🔥"
|
|
172
|
+
]);
|
|
173
|
+
case "e164": return `+1${intIn(rng, 2e9, 9999999999)}`;
|
|
174
|
+
case "base64": return Buffer.from(word(rng, WORD_LENGTH)).toString("base64");
|
|
175
|
+
case "base64url": return Buffer.from(word(rng, WORD_LENGTH)).toString("base64url");
|
|
176
|
+
default: return word(rng, WORD_LENGTH);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Build a plain string whose length lands within the schema's min/max bounds. */
|
|
180
|
+
function plainString(rng, bounds) {
|
|
181
|
+
const min = bounds.min ?? 0;
|
|
182
|
+
return word(rng, intIn(rng, min, bounds.max ?? Math.max(min, WORD_LENGTH)));
|
|
183
|
+
}
|
|
184
|
+
/** Build a number honoring int/min/max constraints. */
|
|
185
|
+
function genNumber(d, rng) {
|
|
186
|
+
const { min, max, int } = numBounds(d);
|
|
187
|
+
if (int) {
|
|
188
|
+
const lo = min ?? 0;
|
|
189
|
+
return intIn(rng, lo, max ?? lo + INT_RANGE);
|
|
190
|
+
}
|
|
191
|
+
const lo = min ?? 0;
|
|
192
|
+
const hi = max ?? lo + INT_RANGE;
|
|
193
|
+
const raw = lo + rng() * (hi - lo);
|
|
194
|
+
return Math.round(raw * FLOAT_PRECISION) / FLOAT_PRECISION;
|
|
195
|
+
}
|
|
196
|
+
/** Build a bigint honoring min/max constraints. */
|
|
197
|
+
function genBigint(d, rng) {
|
|
198
|
+
let min = null;
|
|
199
|
+
let max = null;
|
|
200
|
+
for (const c of checks(d)) if (c.check === "greater_than" && typeof c.value === "bigint") min = c.inclusive ? c.value : c.value + 1n;
|
|
201
|
+
else if (c.check === "less_than" && typeof c.value === "bigint") max = c.inclusive ? c.value : c.value - 1n;
|
|
202
|
+
const lo = min ?? 0n;
|
|
203
|
+
const span = (max ?? lo + BigInt(BIGINT_RANGE)) - lo;
|
|
204
|
+
if (span <= 0n) return lo;
|
|
205
|
+
return lo + BigInt(Math.floor(rng() * BIGINT_RANGE)) % (span + 1n);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolve a collection size from the request query. A `limit`-style query key
|
|
209
|
+
* (the first of {@link MockOptions.limitKeys | limitKeys} present and non-negative)
|
|
210
|
+
* wins; otherwise the configured default {@link MockOptions.arraySize | arraySize}
|
|
211
|
+
* is used. Shared by array generation and `streamOut` item counting so pagination
|
|
212
|
+
* is consistent across both.
|
|
213
|
+
*/
|
|
214
|
+
function resolveSize(query, cfg) {
|
|
215
|
+
for (const key of cfg.limitKeys) {
|
|
216
|
+
const raw = query[key];
|
|
217
|
+
if (typeof raw !== "string" && typeof raw !== "number") continue;
|
|
218
|
+
const n = Number(raw);
|
|
219
|
+
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
220
|
+
}
|
|
221
|
+
return cfg.arraySize;
|
|
222
|
+
}
|
|
223
|
+
/** Last path segment (the field/property name) for override lookup. */
|
|
224
|
+
function leaf(path) {
|
|
225
|
+
const i = path.lastIndexOf(".");
|
|
226
|
+
return i < 0 ? path : path.slice(i + 1);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Generate a value for `schema` at `ctx.path`. Recurses over the zod type tree,
|
|
230
|
+
* applying field overrides (by leaf name or full path) and format overrides.
|
|
231
|
+
*/
|
|
232
|
+
function genValue(schema, ctx, cfg) {
|
|
233
|
+
const fieldOverride = cfg.overrides.fields[ctx.path] ?? cfg.overrides.fields[leaf(ctx.path)];
|
|
234
|
+
if (fieldOverride) return fieldOverride(ctx);
|
|
235
|
+
const d = def(schema);
|
|
236
|
+
switch (d.type) {
|
|
237
|
+
case "string": {
|
|
238
|
+
const format = d.format;
|
|
239
|
+
if (format) {
|
|
240
|
+
const fmtOverride = cfg.overrides.formats[format];
|
|
241
|
+
if (fmtOverride) return fmtOverride(ctx);
|
|
242
|
+
return formatValue(format, ctx.rng, cfg.now());
|
|
243
|
+
}
|
|
244
|
+
return plainString(ctx.rng, lenBounds(d));
|
|
245
|
+
}
|
|
246
|
+
case "number": return genNumber(d, ctx.rng);
|
|
247
|
+
case "bigint": return genBigint(d, ctx.rng);
|
|
248
|
+
case "boolean": return ctx.rng() < .5;
|
|
249
|
+
case "date": return new Date(cfg.now());
|
|
250
|
+
case "literal": return d.values[0];
|
|
251
|
+
case "enum": return pick(ctx.rng, Object.values(d.entries));
|
|
252
|
+
case "object": {
|
|
253
|
+
const out = {};
|
|
254
|
+
for (const [key, child] of Object.entries(d.shape)) {
|
|
255
|
+
const childPath = ctx.path ? `${ctx.path}.${key}` : key;
|
|
256
|
+
out[key] = genValue(child, {
|
|
257
|
+
...ctx,
|
|
258
|
+
path: childPath
|
|
259
|
+
}, cfg);
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
case "array": {
|
|
264
|
+
const n = resolveSize(ctx.query, cfg);
|
|
265
|
+
const bounds = lenBounds(d);
|
|
266
|
+
const count = Math.max(bounds.min ?? 0, Math.min(n, bounds.max ?? n));
|
|
267
|
+
const out = [];
|
|
268
|
+
for (let i = 0; i < count; i++) out.push(genValue(d.element, {
|
|
269
|
+
...ctx,
|
|
270
|
+
path: `${ctx.path}.${i}`
|
|
271
|
+
}, cfg));
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
case "tuple": {
|
|
275
|
+
const out = d.items.map((item, i) => genValue(item, {
|
|
276
|
+
...ctx,
|
|
277
|
+
path: `${ctx.path}.${i}`
|
|
278
|
+
}, cfg));
|
|
279
|
+
if (d.rest) out.push(genValue(d.rest, {
|
|
280
|
+
...ctx,
|
|
281
|
+
path: `${ctx.path}.${out.length}`
|
|
282
|
+
}, cfg));
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
case "record": {
|
|
286
|
+
const out = {};
|
|
287
|
+
for (let i = 0; i < RECORD_SIZE; i++) {
|
|
288
|
+
const key = word(ctx.rng, WORD_LENGTH);
|
|
289
|
+
out[key] = genValue(d.valueType, {
|
|
290
|
+
...ctx,
|
|
291
|
+
path: `${ctx.path}.${key}`
|
|
292
|
+
}, cfg);
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
case "union": return genValue(pick(ctx.rng, d.options), ctx, cfg);
|
|
297
|
+
case "optional":
|
|
298
|
+
if (ctx.rng() < PRESENCE_PROB) return genValue(d.innerType, ctx, cfg);
|
|
299
|
+
return;
|
|
300
|
+
case "nullable":
|
|
301
|
+
if (ctx.rng() < PRESENCE_PROB) return genValue(d.innerType, ctx, cfg);
|
|
302
|
+
return null;
|
|
303
|
+
case "default":
|
|
304
|
+
case "prefault":
|
|
305
|
+
if (ctx.rng() < PRESENCE_PROB) return genValue(d.innerType, ctx, cfg);
|
|
306
|
+
return d.defaultValue;
|
|
307
|
+
case "catch":
|
|
308
|
+
case "readonly":
|
|
309
|
+
case "nonoptional": return genValue(d.innerType, ctx, cfg);
|
|
310
|
+
default: return fallback(d, ctx);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Sensible value for schema types we don't model precisely (any/unknown/null/etc.). */
|
|
314
|
+
function fallback(d, ctx) {
|
|
315
|
+
switch (d.type) {
|
|
316
|
+
case "null": return null;
|
|
317
|
+
case "undefined":
|
|
318
|
+
case "void": return;
|
|
319
|
+
default: return word(ctx.rng, WORD_LENGTH);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Build the resolved {@link GenConfig} from public {@link MockOptions}. */
|
|
323
|
+
function resolveConfig(opts) {
|
|
324
|
+
return {
|
|
325
|
+
arraySize: opts?.arraySize ?? 3,
|
|
326
|
+
limitKeys: opts?.limitKeys ?? DEFAULT_LIMIT_KEYS,
|
|
327
|
+
overrides: {
|
|
328
|
+
fields: { ...opts?.overrides?.fields ?? {} },
|
|
329
|
+
formats: { ...opts?.overrides?.formats ?? {} }
|
|
330
|
+
},
|
|
331
|
+
now: opts?.now ?? Date.now
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Build the per-generation RNG: deterministic hash-seeded, or `Math.random`. */
|
|
335
|
+
function makeRng(opts, ...seedParts) {
|
|
336
|
+
if (opts?.deterministic === false) return Math.random;
|
|
337
|
+
return rngFromParts(String(opts?.seed ?? DEFAULT_SEED), ...seedParts);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Generate one schema-valid fake value from a zod schema.
|
|
341
|
+
*
|
|
342
|
+
* @param schema - any zod schema.
|
|
343
|
+
* @param opts - seeding / sizing / override options.
|
|
344
|
+
* @param ctx - partial context overrides (e.g. a fixed `path`, `request`, `query`).
|
|
345
|
+
* @returns a value that parses against `schema`.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* const user = generate(z.object({ id: z.uuid(), name: z.string() }), { seed: 1 })
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
function generate(schema, opts, ctx) {
|
|
353
|
+
const cfg = resolveConfig(opts);
|
|
354
|
+
const rng = ctx?.rng ?? makeRng(opts, JSON.stringify(ctx?.request ?? null));
|
|
355
|
+
return genValue(schema, {
|
|
356
|
+
path: ctx?.path ?? "",
|
|
357
|
+
rng,
|
|
358
|
+
request: ctx?.request,
|
|
359
|
+
query: ctx?.query ?? {}
|
|
360
|
+
}, cfg);
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/server.ts
|
|
364
|
+
/** Build a {@link GenContext} from a handler payload + endpoint name. */
|
|
365
|
+
function contextFor(name, payload, opts) {
|
|
366
|
+
const data = payload.data ?? {};
|
|
367
|
+
return {
|
|
368
|
+
path: "",
|
|
369
|
+
rng: makeRng(opts, name, JSON.stringify(payload.data ?? null)),
|
|
370
|
+
request: payload.data,
|
|
371
|
+
query: data
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** Read an endpoint config's schemas off its erased definition. */
|
|
375
|
+
function cfgOf(def) {
|
|
376
|
+
return def.cfg;
|
|
377
|
+
}
|
|
378
|
+
/** Smallest declared status in a multi-status map (deterministic choice). */
|
|
379
|
+
function firstStatus(responses) {
|
|
380
|
+
return Math.min(...Object.keys(responses).map(Number));
|
|
381
|
+
}
|
|
382
|
+
/** Build the handler for one endpoint, dispatching on its response shape. */
|
|
383
|
+
function handlerFor(name, def, opts) {
|
|
384
|
+
const cfg = cfgOf(def);
|
|
385
|
+
const gcfg = resolveConfig(opts);
|
|
386
|
+
if (cfg.streamOut && typeof cfg.streamOut !== "string") {
|
|
387
|
+
const itemSchema = cfg.streamOut;
|
|
388
|
+
return (payload) => {
|
|
389
|
+
const ctx = contextFor(name, payload, opts);
|
|
390
|
+
const n = resolveSize(ctx.query, gcfg);
|
|
391
|
+
return (async function* () {
|
|
392
|
+
for (let i = 0; i < n; i++) yield genValue(itemSchema, {
|
|
393
|
+
...ctx,
|
|
394
|
+
path: String(i)
|
|
395
|
+
}, gcfg);
|
|
396
|
+
})();
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (cfg.responses) {
|
|
400
|
+
const responses = cfg.responses;
|
|
401
|
+
const status = firstStatus(responses);
|
|
402
|
+
const schema = responses[status];
|
|
403
|
+
return (payload) => {
|
|
404
|
+
return {
|
|
405
|
+
status,
|
|
406
|
+
data: genValue(schema, contextFor(name, payload, opts), gcfg)
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (cfg.response) {
|
|
411
|
+
const schema = cfg.response;
|
|
412
|
+
return (payload) => genValue(schema, contextFor(name, payload, opts), gcfg);
|
|
413
|
+
}
|
|
414
|
+
return () => void 0;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Build the handler bag for a spec — one generated handler per endpoint, suitable
|
|
418
|
+
* for `server(spec, [implement(spec).handlers(bag)])`.
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
function mockHandlers(spec, opts) {
|
|
426
|
+
const bag = {};
|
|
427
|
+
for (const [name, def] of Object.entries(spec.endpoints)) bag[name] = handlerFor(name, def, opts);
|
|
428
|
+
return bag;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Build a real ayepi {@link Server} for a spec where every endpoint returns
|
|
432
|
+
* generated, schema-valid data. Routing, input validation, output validation, and
|
|
433
|
+
* the OpenAPI/AsyncAPI docs all behave exactly as a real server's.
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* ```ts
|
|
437
|
+
* const app = mockServer(api, { seed: 1, arraySize: 5 })
|
|
438
|
+
* const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
function mockServer(spec, opts) {
|
|
442
|
+
const bag = mockHandlers(spec, opts);
|
|
443
|
+
return (0, _ayepi_core.server)(spec, [(0, _ayepi_core.implement)(spec).handlers(bag)]);
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
exports.generate = generate;
|
|
447
|
+
exports.mockHandlers = mockHandlers;
|
|
448
|
+
exports.mockServer = mockServer;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AnySpec, Server } from "@ayepi/core";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* # Public option & context types
|
|
8
|
+
*
|
|
9
|
+
* The configuration surface ({@link MockOptions}) and the per-generation context
|
|
10
|
+
* ({@link GenContext}) passed to override callbacks. Kept in their own module so
|
|
11
|
+
* both the generator and the server entry points can import them without a cycle.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Context handed to override callbacks (and threaded through the generator). It
|
|
17
|
+
* exposes where in the schema we are ({@link GenContext.path | path}), the seeded
|
|
18
|
+
* {@link GenContext.rng | rng}, and the request inputs that seeded it.
|
|
19
|
+
*/
|
|
20
|
+
interface GenContext {
|
|
21
|
+
/** Dotted path to the value being generated, e.g. `user.address.city` or `items.0`. */
|
|
22
|
+
readonly path: string;
|
|
23
|
+
/** The seeded PRNG for this generation — call it for floats in `[0, 1)`. */
|
|
24
|
+
readonly rng: () => number;
|
|
25
|
+
/** The full request data that seeded this generation (server mocks), else `undefined`. */
|
|
26
|
+
readonly request: unknown;
|
|
27
|
+
/** The request query params that seeded this generation (drives pagination). */
|
|
28
|
+
readonly query: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/** An override callback: receives the {@link GenContext} and returns a value. */
|
|
31
|
+
type Override = (g: GenContext) => unknown;
|
|
32
|
+
/** Per-field and per-format override tables. */
|
|
33
|
+
interface MockOverrides {
|
|
34
|
+
/** Keyed by property name or dotted path (e.g. `email` or `user.email`). */
|
|
35
|
+
readonly fields?: Readonly<Record<string, Override>>;
|
|
36
|
+
/** Keyed by detected zod string format (e.g. `email`, `uuid`, `url`, `datetime`). */
|
|
37
|
+
readonly formats?: Readonly<Record<string, Override>>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Configuration for fake-data generation and the mock server.
|
|
41
|
+
*
|
|
42
|
+
* The defaults aim for *deterministic* output: with {@link MockOptions.deterministic}
|
|
43
|
+
* on (the default), the PRNG is seeded from `seed + endpoint + JSON(request)`, so the
|
|
44
|
+
* same inputs always produce the same bytes — which is what makes paginated responses
|
|
45
|
+
* stable across calls.
|
|
46
|
+
*/
|
|
47
|
+
interface MockOptions {
|
|
48
|
+
/** Base seed; combined with the endpoint + request to seed the PRNG. Default `0`. */
|
|
49
|
+
readonly seed?: number | string;
|
|
50
|
+
/** When `true` (default), generation is a pure function of seed + inputs. When `false`, uses `Math.random`. */
|
|
51
|
+
readonly deterministic?: boolean;
|
|
52
|
+
/** Default element count for arrays with no size hint. Default `3`. */
|
|
53
|
+
readonly arraySize?: number;
|
|
54
|
+
/** Query keys whose value sizes generated arrays (pagination). Default `['limit', 'pageSize', 'count']`. */
|
|
55
|
+
readonly limitKeys?: readonly string[];
|
|
56
|
+
/** Per-field / per-format value overrides. */
|
|
57
|
+
readonly overrides?: MockOverrides;
|
|
58
|
+
/** Clock used for date generation (injectable for tests). Default `Date.now`. */
|
|
59
|
+
readonly now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/generate.d.ts
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate one schema-valid fake value from a zod schema.
|
|
66
|
+
*
|
|
67
|
+
* @param schema - any zod schema.
|
|
68
|
+
* @param opts - seeding / sizing / override options.
|
|
69
|
+
* @param ctx - partial context overrides (e.g. a fixed `path`, `request`, `query`).
|
|
70
|
+
* @returns a value that parses against `schema`.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const user = generate(z.object({ id: z.uuid(), name: z.string() }), { seed: 1 })
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
declare function generate(schema: z.ZodType, opts?: MockOptions, ctx?: Partial<GenContext>): unknown;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/server.d.ts
|
|
80
|
+
/** A handler payload, narrowed to the parts the mock reads. */
|
|
81
|
+
interface MockPayload {
|
|
82
|
+
readonly data?: unknown;
|
|
83
|
+
}
|
|
84
|
+
/** A single generated mock handler (erased payload/return types). */
|
|
85
|
+
type MockHandler = (payload: MockPayload) => unknown;
|
|
86
|
+
/**
|
|
87
|
+
* Build the handler bag for a spec — one generated handler per endpoint, suitable
|
|
88
|
+
* for `server(spec, [implement(spec).handlers(bag)])`.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function mockHandlers(spec: AnySpec, opts?: MockOptions): Record<string, MockHandler>;
|
|
96
|
+
/**
|
|
97
|
+
* Build a real ayepi {@link Server} for a spec where every endpoint returns
|
|
98
|
+
* generated, schema-valid data. Routing, input validation, output validation, and
|
|
99
|
+
* the OpenAPI/AsyncAPI docs all behave exactly as a real server's.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const app = mockServer(api, { seed: 1, arraySize: 5 })
|
|
104
|
+
* const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
declare function mockServer(spec: AnySpec, opts?: MockOptions): Server<AnySpec>;
|
|
108
|
+
//#endregion
|
|
109
|
+
export { type GenContext, type MockOptions, type MockOverrides, type Override, generate, mockHandlers, mockServer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AnySpec, Server } from "@ayepi/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* # Public option & context types
|
|
8
|
+
*
|
|
9
|
+
* The configuration surface ({@link MockOptions}) and the per-generation context
|
|
10
|
+
* ({@link GenContext}) passed to override callbacks. Kept in their own module so
|
|
11
|
+
* both the generator and the server entry points can import them without a cycle.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Context handed to override callbacks (and threaded through the generator). It
|
|
17
|
+
* exposes where in the schema we are ({@link GenContext.path | path}), the seeded
|
|
18
|
+
* {@link GenContext.rng | rng}, and the request inputs that seeded it.
|
|
19
|
+
*/
|
|
20
|
+
interface GenContext {
|
|
21
|
+
/** Dotted path to the value being generated, e.g. `user.address.city` or `items.0`. */
|
|
22
|
+
readonly path: string;
|
|
23
|
+
/** The seeded PRNG for this generation — call it for floats in `[0, 1)`. */
|
|
24
|
+
readonly rng: () => number;
|
|
25
|
+
/** The full request data that seeded this generation (server mocks), else `undefined`. */
|
|
26
|
+
readonly request: unknown;
|
|
27
|
+
/** The request query params that seeded this generation (drives pagination). */
|
|
28
|
+
readonly query: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/** An override callback: receives the {@link GenContext} and returns a value. */
|
|
31
|
+
type Override = (g: GenContext) => unknown;
|
|
32
|
+
/** Per-field and per-format override tables. */
|
|
33
|
+
interface MockOverrides {
|
|
34
|
+
/** Keyed by property name or dotted path (e.g. `email` or `user.email`). */
|
|
35
|
+
readonly fields?: Readonly<Record<string, Override>>;
|
|
36
|
+
/** Keyed by detected zod string format (e.g. `email`, `uuid`, `url`, `datetime`). */
|
|
37
|
+
readonly formats?: Readonly<Record<string, Override>>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Configuration for fake-data generation and the mock server.
|
|
41
|
+
*
|
|
42
|
+
* The defaults aim for *deterministic* output: with {@link MockOptions.deterministic}
|
|
43
|
+
* on (the default), the PRNG is seeded from `seed + endpoint + JSON(request)`, so the
|
|
44
|
+
* same inputs always produce the same bytes — which is what makes paginated responses
|
|
45
|
+
* stable across calls.
|
|
46
|
+
*/
|
|
47
|
+
interface MockOptions {
|
|
48
|
+
/** Base seed; combined with the endpoint + request to seed the PRNG. Default `0`. */
|
|
49
|
+
readonly seed?: number | string;
|
|
50
|
+
/** When `true` (default), generation is a pure function of seed + inputs. When `false`, uses `Math.random`. */
|
|
51
|
+
readonly deterministic?: boolean;
|
|
52
|
+
/** Default element count for arrays with no size hint. Default `3`. */
|
|
53
|
+
readonly arraySize?: number;
|
|
54
|
+
/** Query keys whose value sizes generated arrays (pagination). Default `['limit', 'pageSize', 'count']`. */
|
|
55
|
+
readonly limitKeys?: readonly string[];
|
|
56
|
+
/** Per-field / per-format value overrides. */
|
|
57
|
+
readonly overrides?: MockOverrides;
|
|
58
|
+
/** Clock used for date generation (injectable for tests). Default `Date.now`. */
|
|
59
|
+
readonly now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/generate.d.ts
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate one schema-valid fake value from a zod schema.
|
|
66
|
+
*
|
|
67
|
+
* @param schema - any zod schema.
|
|
68
|
+
* @param opts - seeding / sizing / override options.
|
|
69
|
+
* @param ctx - partial context overrides (e.g. a fixed `path`, `request`, `query`).
|
|
70
|
+
* @returns a value that parses against `schema`.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const user = generate(z.object({ id: z.uuid(), name: z.string() }), { seed: 1 })
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
declare function generate(schema: z.ZodType, opts?: MockOptions, ctx?: Partial<GenContext>): unknown;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/server.d.ts
|
|
80
|
+
/** A handler payload, narrowed to the parts the mock reads. */
|
|
81
|
+
interface MockPayload {
|
|
82
|
+
readonly data?: unknown;
|
|
83
|
+
}
|
|
84
|
+
/** A single generated mock handler (erased payload/return types). */
|
|
85
|
+
type MockHandler = (payload: MockPayload) => unknown;
|
|
86
|
+
/**
|
|
87
|
+
* Build the handler bag for a spec — one generated handler per endpoint, suitable
|
|
88
|
+
* for `server(spec, [implement(spec).handlers(bag)])`.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function mockHandlers(spec: AnySpec, opts?: MockOptions): Record<string, MockHandler>;
|
|
96
|
+
/**
|
|
97
|
+
* Build a real ayepi {@link Server} for a spec where every endpoint returns
|
|
98
|
+
* generated, schema-valid data. Routing, input validation, output validation, and
|
|
99
|
+
* the OpenAPI/AsyncAPI docs all behave exactly as a real server's.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const app = mockServer(api, { seed: 1, arraySize: 5 })
|
|
104
|
+
* const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
declare function mockServer(spec: AnySpec, opts?: MockOptions): Server<AnySpec>;
|
|
108
|
+
//#endregion
|
|
109
|
+
export { type GenContext, type MockOptions, type MockOverrides, type Override, generate, mockHandlers, mockServer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { implement, server } from "@ayepi/core";
|
|
2
|
+
//#region src/rng.ts
|
|
3
|
+
/**
|
|
4
|
+
* # Seeded PRNG
|
|
5
|
+
*
|
|
6
|
+
* A tiny, dependency-free deterministic random number generator. {@link fnv1a}
|
|
7
|
+
* folds an arbitrary string into a 32-bit seed; {@link mulberry32} turns that
|
|
8
|
+
* seed into a `() => number` stream of floats in `[0, 1)`. Given the same seed
|
|
9
|
+
* the stream is byte-for-byte reproducible, which is what makes deterministic
|
|
10
|
+
* mocking — and therefore stable pagination — possible.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** FNV-1a offset basis (32-bit). */
|
|
15
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
16
|
+
/** FNV-1a prime (32-bit). */
|
|
17
|
+
const FNV_PRIME = 16777619;
|
|
18
|
+
/** Mask to keep arithmetic within 32 unsigned bits. */
|
|
19
|
+
const U32_MASK = 4294967295;
|
|
20
|
+
/** mulberry32 mixing constant added to the running state each step. */
|
|
21
|
+
const MULBERRY_INCREMENT = 1831565813;
|
|
22
|
+
/** mulberry32 second-round multiply constant. */
|
|
23
|
+
const MULBERRY_ROUND2 = 3266489909;
|
|
24
|
+
/** Divisor that maps a 32-bit unsigned integer into `[0, 1)`. */
|
|
25
|
+
const U32_DIVISOR = 4294967296;
|
|
26
|
+
/**
|
|
27
|
+
* Hash a string into a 32-bit unsigned integer with FNV-1a.
|
|
28
|
+
*
|
|
29
|
+
* Used to fold a composite seed (`baseSeed + endpoint + JSON(request)`) down to a
|
|
30
|
+
* single integer the PRNG can be initialized from.
|
|
31
|
+
*/
|
|
32
|
+
function fnv1a(input) {
|
|
33
|
+
let hash = FNV_OFFSET_BASIS;
|
|
34
|
+
for (let i = 0; i < input.length; i++) {
|
|
35
|
+
hash ^= input.charCodeAt(i);
|
|
36
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
37
|
+
}
|
|
38
|
+
return hash >>> 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a deterministic `() => number` PRNG (floats in `[0, 1)`) from a 32-bit
|
|
42
|
+
* seed using the mulberry32 algorithm. Identical seeds yield identical streams.
|
|
43
|
+
*/
|
|
44
|
+
function mulberry32(seed) {
|
|
45
|
+
let state = seed >>> 0;
|
|
46
|
+
return () => {
|
|
47
|
+
state = state + MULBERRY_INCREMENT & U32_MASK;
|
|
48
|
+
let t = state;
|
|
49
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
50
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | MULBERRY_ROUND2);
|
|
51
|
+
t = (t ^ t >>> 14) >>> 0;
|
|
52
|
+
return t / U32_DIVISOR;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Convenience: hash a string seed and return a fresh PRNG stream for it.
|
|
57
|
+
*
|
|
58
|
+
* @param parts - composite seed parts joined into the hash input.
|
|
59
|
+
*/
|
|
60
|
+
function rngFromParts(...parts) {
|
|
61
|
+
return mulberry32(fnv1a(parts.join("\0")));
|
|
62
|
+
}
|
|
63
|
+
/** Default query keys whose value sizes generated arrays. */
|
|
64
|
+
const DEFAULT_LIMIT_KEYS = [
|
|
65
|
+
"limit",
|
|
66
|
+
"pageSize",
|
|
67
|
+
"count"
|
|
68
|
+
];
|
|
69
|
+
/** Default base seed when none is supplied. */
|
|
70
|
+
const DEFAULT_SEED = "0";
|
|
71
|
+
/** Fallback string length used when a format/length constraint gives no better hint. */
|
|
72
|
+
const WORD_LENGTH = 8;
|
|
73
|
+
/** Default integer ceiling (exclusive) for unconstrained number generation. */
|
|
74
|
+
const INT_RANGE = 1e3;
|
|
75
|
+
/** Decimal places kept for unconstrained floats. */
|
|
76
|
+
const FLOAT_PRECISION = 1e3;
|
|
77
|
+
/** Probability an optional/nullable value is *present* (non-undefined / non-null). */
|
|
78
|
+
const PRESENCE_PROB = .7;
|
|
79
|
+
/** Record entry count when generating from a `z.record`. */
|
|
80
|
+
const RECORD_SIZE = 2;
|
|
81
|
+
/** Bound used for bigint generation when unconstrained. */
|
|
82
|
+
const BIGINT_RANGE = 1e3;
|
|
83
|
+
/** Number `format` values that require an integer result. */
|
|
84
|
+
const INT_FORMATS = new Set([
|
|
85
|
+
"safeint",
|
|
86
|
+
"int32",
|
|
87
|
+
"uint32"
|
|
88
|
+
]);
|
|
89
|
+
/** Characters drawn from when padding/building plain strings. */
|
|
90
|
+
const ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
91
|
+
/** Hex characters for uuid/id-shaped formats. */
|
|
92
|
+
const HEX = "0123456789abcdef";
|
|
93
|
+
/** Read a schema's internal `_zod.def`. */
|
|
94
|
+
function def(schema) {
|
|
95
|
+
return schema._zod.def;
|
|
96
|
+
}
|
|
97
|
+
/** Read the (possibly empty) list of check descriptors on a schema. */
|
|
98
|
+
function checks(d) {
|
|
99
|
+
return (d.checks ?? []).map((c) => c._zod.def);
|
|
100
|
+
}
|
|
101
|
+
/** Pull min/max/int constraints off a number schema's checks (and its `format`). */
|
|
102
|
+
function numBounds(d) {
|
|
103
|
+
let min = null;
|
|
104
|
+
let max = null;
|
|
105
|
+
let int = d.format !== void 0 && INT_FORMATS.has(d.format);
|
|
106
|
+
for (const c of checks(d)) if (c.check === "greater_than" && typeof c.value === "number") min = c.inclusive ? c.value : c.value + 1;
|
|
107
|
+
else if (c.check === "less_than" && typeof c.value === "number") max = c.inclusive ? c.value : c.value - 1;
|
|
108
|
+
else if (c.check === "number_format") int = true;
|
|
109
|
+
return {
|
|
110
|
+
min,
|
|
111
|
+
max,
|
|
112
|
+
int
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Pull min/max length constraints off a string/array schema's checks. */
|
|
116
|
+
function lenBounds(d) {
|
|
117
|
+
let min = null;
|
|
118
|
+
let max = null;
|
|
119
|
+
for (const c of checks(d)) if (c.check === "min_length" && typeof c.minimum === "number") min = c.minimum;
|
|
120
|
+
else if (c.check === "max_length" && typeof c.maximum === "number") max = c.maximum;
|
|
121
|
+
else if (c.check === "length_equals" && typeof c.length === "number") min = max = c.length;
|
|
122
|
+
return {
|
|
123
|
+
min,
|
|
124
|
+
max
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Draw an integer in `[lo, hi]` (inclusive) from the rng. */
|
|
128
|
+
function intIn(rng, lo, hi) {
|
|
129
|
+
/* v8 ignore next */ if (hi < lo) return lo;
|
|
130
|
+
return lo + Math.floor(rng() * (hi - lo + 1));
|
|
131
|
+
}
|
|
132
|
+
/** Pick a random element of a non-empty array. */
|
|
133
|
+
function pick(rng, arr) {
|
|
134
|
+
return arr[Math.floor(rng() * arr.length)];
|
|
135
|
+
}
|
|
136
|
+
/** Build a lowercase word of `len` characters. */
|
|
137
|
+
function word(rng, len) {
|
|
138
|
+
let s = "";
|
|
139
|
+
for (let i = 0; i < len; i++) s += ALPHABET[Math.floor(rng() * 26)];
|
|
140
|
+
return s;
|
|
141
|
+
}
|
|
142
|
+
/** Build a hex string of `len` characters. */
|
|
143
|
+
function hex(rng, len) {
|
|
144
|
+
let s = "";
|
|
145
|
+
for (let i = 0; i < len; i++) s += HEX[Math.floor(rng() * 16)];
|
|
146
|
+
return s;
|
|
147
|
+
}
|
|
148
|
+
/** Build a value for a detected/declared zod string `format`. */
|
|
149
|
+
function formatValue(format, rng, now) {
|
|
150
|
+
switch (format) {
|
|
151
|
+
case "email": return `${word(rng, WORD_LENGTH)}@example.com`;
|
|
152
|
+
case "url": return `https://${word(rng, WORD_LENGTH)}.example.com`;
|
|
153
|
+
case "uuid":
|
|
154
|
+
case "guid": return `${hex(rng, 8)}-${hex(rng, 4)}-4${hex(rng, 3)}-8${hex(rng, 3)}-${hex(rng, 12)}`;
|
|
155
|
+
case "datetime": return new Date(now).toISOString();
|
|
156
|
+
case "date": return new Date(now).toISOString().slice(0, 10);
|
|
157
|
+
case "time": return new Date(now).toISOString().slice(11, 23);
|
|
158
|
+
case "duration": return `PT${intIn(rng, 1, 60)}M`;
|
|
159
|
+
case "ipv4": return `${intIn(rng, 1, 254)}.${intIn(rng, 0, 255)}.${intIn(rng, 0, 255)}.${intIn(rng, 1, 254)}`;
|
|
160
|
+
case "ipv6": return `${hex(rng, 4)}:${hex(rng, 4)}::${hex(rng, 4)}`;
|
|
161
|
+
case "cuid":
|
|
162
|
+
case "cuid2": return `c${word(rng, 24)}`;
|
|
163
|
+
case "ulid": return hex(rng, 26).toUpperCase();
|
|
164
|
+
case "nanoid": return word(rng, 21);
|
|
165
|
+
case "emoji": return pick(rng, [
|
|
166
|
+
"😀",
|
|
167
|
+
"🎉",
|
|
168
|
+
"🚀",
|
|
169
|
+
"🌟",
|
|
170
|
+
"🔥"
|
|
171
|
+
]);
|
|
172
|
+
case "e164": return `+1${intIn(rng, 2e9, 9999999999)}`;
|
|
173
|
+
case "base64": return Buffer.from(word(rng, WORD_LENGTH)).toString("base64");
|
|
174
|
+
case "base64url": return Buffer.from(word(rng, WORD_LENGTH)).toString("base64url");
|
|
175
|
+
default: return word(rng, WORD_LENGTH);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Build a plain string whose length lands within the schema's min/max bounds. */
|
|
179
|
+
function plainString(rng, bounds) {
|
|
180
|
+
const min = bounds.min ?? 0;
|
|
181
|
+
return word(rng, intIn(rng, min, bounds.max ?? Math.max(min, WORD_LENGTH)));
|
|
182
|
+
}
|
|
183
|
+
/** Build a number honoring int/min/max constraints. */
|
|
184
|
+
function genNumber(d, rng) {
|
|
185
|
+
const { min, max, int } = numBounds(d);
|
|
186
|
+
if (int) {
|
|
187
|
+
const lo = min ?? 0;
|
|
188
|
+
return intIn(rng, lo, max ?? lo + INT_RANGE);
|
|
189
|
+
}
|
|
190
|
+
const lo = min ?? 0;
|
|
191
|
+
const hi = max ?? lo + INT_RANGE;
|
|
192
|
+
const raw = lo + rng() * (hi - lo);
|
|
193
|
+
return Math.round(raw * FLOAT_PRECISION) / FLOAT_PRECISION;
|
|
194
|
+
}
|
|
195
|
+
/** Build a bigint honoring min/max constraints. */
|
|
196
|
+
function genBigint(d, rng) {
|
|
197
|
+
let min = null;
|
|
198
|
+
let max = null;
|
|
199
|
+
for (const c of checks(d)) if (c.check === "greater_than" && typeof c.value === "bigint") min = c.inclusive ? c.value : c.value + 1n;
|
|
200
|
+
else if (c.check === "less_than" && typeof c.value === "bigint") max = c.inclusive ? c.value : c.value - 1n;
|
|
201
|
+
const lo = min ?? 0n;
|
|
202
|
+
const span = (max ?? lo + BigInt(BIGINT_RANGE)) - lo;
|
|
203
|
+
if (span <= 0n) return lo;
|
|
204
|
+
return lo + BigInt(Math.floor(rng() * BIGINT_RANGE)) % (span + 1n);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Resolve a collection size from the request query. A `limit`-style query key
|
|
208
|
+
* (the first of {@link MockOptions.limitKeys | limitKeys} present and non-negative)
|
|
209
|
+
* wins; otherwise the configured default {@link MockOptions.arraySize | arraySize}
|
|
210
|
+
* is used. Shared by array generation and `streamOut` item counting so pagination
|
|
211
|
+
* is consistent across both.
|
|
212
|
+
*/
|
|
213
|
+
function resolveSize(query, cfg) {
|
|
214
|
+
for (const key of cfg.limitKeys) {
|
|
215
|
+
const raw = query[key];
|
|
216
|
+
if (typeof raw !== "string" && typeof raw !== "number") continue;
|
|
217
|
+
const n = Number(raw);
|
|
218
|
+
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
219
|
+
}
|
|
220
|
+
return cfg.arraySize;
|
|
221
|
+
}
|
|
222
|
+
/** Last path segment (the field/property name) for override lookup. */
|
|
223
|
+
function leaf(path) {
|
|
224
|
+
const i = path.lastIndexOf(".");
|
|
225
|
+
return i < 0 ? path : path.slice(i + 1);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Generate a value for `schema` at `ctx.path`. Recurses over the zod type tree,
|
|
229
|
+
* applying field overrides (by leaf name or full path) and format overrides.
|
|
230
|
+
*/
|
|
231
|
+
function genValue(schema, ctx, cfg) {
|
|
232
|
+
const fieldOverride = cfg.overrides.fields[ctx.path] ?? cfg.overrides.fields[leaf(ctx.path)];
|
|
233
|
+
if (fieldOverride) return fieldOverride(ctx);
|
|
234
|
+
const d = def(schema);
|
|
235
|
+
switch (d.type) {
|
|
236
|
+
case "string": {
|
|
237
|
+
const format = d.format;
|
|
238
|
+
if (format) {
|
|
239
|
+
const fmtOverride = cfg.overrides.formats[format];
|
|
240
|
+
if (fmtOverride) return fmtOverride(ctx);
|
|
241
|
+
return formatValue(format, ctx.rng, cfg.now());
|
|
242
|
+
}
|
|
243
|
+
return plainString(ctx.rng, lenBounds(d));
|
|
244
|
+
}
|
|
245
|
+
case "number": return genNumber(d, ctx.rng);
|
|
246
|
+
case "bigint": return genBigint(d, ctx.rng);
|
|
247
|
+
case "boolean": return ctx.rng() < .5;
|
|
248
|
+
case "date": return new Date(cfg.now());
|
|
249
|
+
case "literal": return d.values[0];
|
|
250
|
+
case "enum": return pick(ctx.rng, Object.values(d.entries));
|
|
251
|
+
case "object": {
|
|
252
|
+
const out = {};
|
|
253
|
+
for (const [key, child] of Object.entries(d.shape)) {
|
|
254
|
+
const childPath = ctx.path ? `${ctx.path}.${key}` : key;
|
|
255
|
+
out[key] = genValue(child, {
|
|
256
|
+
...ctx,
|
|
257
|
+
path: childPath
|
|
258
|
+
}, cfg);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
case "array": {
|
|
263
|
+
const n = resolveSize(ctx.query, cfg);
|
|
264
|
+
const bounds = lenBounds(d);
|
|
265
|
+
const count = Math.max(bounds.min ?? 0, Math.min(n, bounds.max ?? n));
|
|
266
|
+
const out = [];
|
|
267
|
+
for (let i = 0; i < count; i++) out.push(genValue(d.element, {
|
|
268
|
+
...ctx,
|
|
269
|
+
path: `${ctx.path}.${i}`
|
|
270
|
+
}, cfg));
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
case "tuple": {
|
|
274
|
+
const out = d.items.map((item, i) => genValue(item, {
|
|
275
|
+
...ctx,
|
|
276
|
+
path: `${ctx.path}.${i}`
|
|
277
|
+
}, cfg));
|
|
278
|
+
if (d.rest) out.push(genValue(d.rest, {
|
|
279
|
+
...ctx,
|
|
280
|
+
path: `${ctx.path}.${out.length}`
|
|
281
|
+
}, cfg));
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
case "record": {
|
|
285
|
+
const out = {};
|
|
286
|
+
for (let i = 0; i < RECORD_SIZE; i++) {
|
|
287
|
+
const key = word(ctx.rng, WORD_LENGTH);
|
|
288
|
+
out[key] = genValue(d.valueType, {
|
|
289
|
+
...ctx,
|
|
290
|
+
path: `${ctx.path}.${key}`
|
|
291
|
+
}, cfg);
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
case "union": return genValue(pick(ctx.rng, d.options), ctx, cfg);
|
|
296
|
+
case "optional":
|
|
297
|
+
if (ctx.rng() < PRESENCE_PROB) return genValue(d.innerType, ctx, cfg);
|
|
298
|
+
return;
|
|
299
|
+
case "nullable":
|
|
300
|
+
if (ctx.rng() < PRESENCE_PROB) return genValue(d.innerType, ctx, cfg);
|
|
301
|
+
return null;
|
|
302
|
+
case "default":
|
|
303
|
+
case "prefault":
|
|
304
|
+
if (ctx.rng() < PRESENCE_PROB) return genValue(d.innerType, ctx, cfg);
|
|
305
|
+
return d.defaultValue;
|
|
306
|
+
case "catch":
|
|
307
|
+
case "readonly":
|
|
308
|
+
case "nonoptional": return genValue(d.innerType, ctx, cfg);
|
|
309
|
+
default: return fallback(d, ctx);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** Sensible value for schema types we don't model precisely (any/unknown/null/etc.). */
|
|
313
|
+
function fallback(d, ctx) {
|
|
314
|
+
switch (d.type) {
|
|
315
|
+
case "null": return null;
|
|
316
|
+
case "undefined":
|
|
317
|
+
case "void": return;
|
|
318
|
+
default: return word(ctx.rng, WORD_LENGTH);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Build the resolved {@link GenConfig} from public {@link MockOptions}. */
|
|
322
|
+
function resolveConfig(opts) {
|
|
323
|
+
return {
|
|
324
|
+
arraySize: opts?.arraySize ?? 3,
|
|
325
|
+
limitKeys: opts?.limitKeys ?? DEFAULT_LIMIT_KEYS,
|
|
326
|
+
overrides: {
|
|
327
|
+
fields: { ...opts?.overrides?.fields ?? {} },
|
|
328
|
+
formats: { ...opts?.overrides?.formats ?? {} }
|
|
329
|
+
},
|
|
330
|
+
now: opts?.now ?? Date.now
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/** Build the per-generation RNG: deterministic hash-seeded, or `Math.random`. */
|
|
334
|
+
function makeRng(opts, ...seedParts) {
|
|
335
|
+
if (opts?.deterministic === false) return Math.random;
|
|
336
|
+
return rngFromParts(String(opts?.seed ?? DEFAULT_SEED), ...seedParts);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Generate one schema-valid fake value from a zod schema.
|
|
340
|
+
*
|
|
341
|
+
* @param schema - any zod schema.
|
|
342
|
+
* @param opts - seeding / sizing / override options.
|
|
343
|
+
* @param ctx - partial context overrides (e.g. a fixed `path`, `request`, `query`).
|
|
344
|
+
* @returns a value that parses against `schema`.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```ts
|
|
348
|
+
* const user = generate(z.object({ id: z.uuid(), name: z.string() }), { seed: 1 })
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
function generate(schema, opts, ctx) {
|
|
352
|
+
const cfg = resolveConfig(opts);
|
|
353
|
+
const rng = ctx?.rng ?? makeRng(opts, JSON.stringify(ctx?.request ?? null));
|
|
354
|
+
return genValue(schema, {
|
|
355
|
+
path: ctx?.path ?? "",
|
|
356
|
+
rng,
|
|
357
|
+
request: ctx?.request,
|
|
358
|
+
query: ctx?.query ?? {}
|
|
359
|
+
}, cfg);
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/server.ts
|
|
363
|
+
/** Build a {@link GenContext} from a handler payload + endpoint name. */
|
|
364
|
+
function contextFor(name, payload, opts) {
|
|
365
|
+
const data = payload.data ?? {};
|
|
366
|
+
return {
|
|
367
|
+
path: "",
|
|
368
|
+
rng: makeRng(opts, name, JSON.stringify(payload.data ?? null)),
|
|
369
|
+
request: payload.data,
|
|
370
|
+
query: data
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/** Read an endpoint config's schemas off its erased definition. */
|
|
374
|
+
function cfgOf(def) {
|
|
375
|
+
return def.cfg;
|
|
376
|
+
}
|
|
377
|
+
/** Smallest declared status in a multi-status map (deterministic choice). */
|
|
378
|
+
function firstStatus(responses) {
|
|
379
|
+
return Math.min(...Object.keys(responses).map(Number));
|
|
380
|
+
}
|
|
381
|
+
/** Build the handler for one endpoint, dispatching on its response shape. */
|
|
382
|
+
function handlerFor(name, def, opts) {
|
|
383
|
+
const cfg = cfgOf(def);
|
|
384
|
+
const gcfg = resolveConfig(opts);
|
|
385
|
+
if (cfg.streamOut && typeof cfg.streamOut !== "string") {
|
|
386
|
+
const itemSchema = cfg.streamOut;
|
|
387
|
+
return (payload) => {
|
|
388
|
+
const ctx = contextFor(name, payload, opts);
|
|
389
|
+
const n = resolveSize(ctx.query, gcfg);
|
|
390
|
+
return (async function* () {
|
|
391
|
+
for (let i = 0; i < n; i++) yield genValue(itemSchema, {
|
|
392
|
+
...ctx,
|
|
393
|
+
path: String(i)
|
|
394
|
+
}, gcfg);
|
|
395
|
+
})();
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (cfg.responses) {
|
|
399
|
+
const responses = cfg.responses;
|
|
400
|
+
const status = firstStatus(responses);
|
|
401
|
+
const schema = responses[status];
|
|
402
|
+
return (payload) => {
|
|
403
|
+
return {
|
|
404
|
+
status,
|
|
405
|
+
data: genValue(schema, contextFor(name, payload, opts), gcfg)
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (cfg.response) {
|
|
410
|
+
const schema = cfg.response;
|
|
411
|
+
return (payload) => genValue(schema, contextFor(name, payload, opts), gcfg);
|
|
412
|
+
}
|
|
413
|
+
return () => void 0;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Build the handler bag for a spec — one generated handler per endpoint, suitable
|
|
417
|
+
* for `server(spec, [implement(spec).handlers(bag)])`.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* ```ts
|
|
421
|
+
* const app = server(api, [implement(api).handlers(mockHandlers(api, { seed: 1 }))])
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
function mockHandlers(spec, opts) {
|
|
425
|
+
const bag = {};
|
|
426
|
+
for (const [name, def] of Object.entries(spec.endpoints)) bag[name] = handlerFor(name, def, opts);
|
|
427
|
+
return bag;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Build a real ayepi {@link Server} for a spec where every endpoint returns
|
|
431
|
+
* generated, schema-valid data. Routing, input validation, output validation, and
|
|
432
|
+
* the OpenAPI/AsyncAPI docs all behave exactly as a real server's.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* const app = mockServer(api, { seed: 1, arraySize: 5 })
|
|
437
|
+
* const res = await app.fetch(new Request('http://x/listUsers?limit=10', { method: 'POST' }))
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
function mockServer(spec, opts) {
|
|
441
|
+
const bag = mockHandlers(spec, opts);
|
|
442
|
+
return server(spec, [implement(spec).handlers(bag)]);
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
export { generate, mockHandlers, mockServer };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ayepi/mock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mock server for @ayepi/core — generate schema-valid fake data from a spec, with deterministic seeding and automatic pagination",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ClickerMonkey/ayepi.git",
|
|
12
|
+
"directory": "packages/mock"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/ClickerMonkey/ayepi/tree/main/packages/mock#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/ClickerMonkey/ayepi/issues"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/index.d.cts",
|
|
31
|
+
"default": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"zod": "^4",
|
|
41
|
+
"@ayepi/core": "^0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
45
|
+
"publint": "^0.3.0",
|
|
46
|
+
"tsdown": "^0.12.0",
|
|
47
|
+
"vitest": "^2.1.8",
|
|
48
|
+
"zod": "^4.4.3",
|
|
49
|
+
"@ayepi/core": "0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"ayepi",
|
|
53
|
+
"mock",
|
|
54
|
+
"faker",
|
|
55
|
+
"testing",
|
|
56
|
+
"fixtures",
|
|
57
|
+
"seed"
|
|
58
|
+
],
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"test": "vitest run --coverage",
|
|
63
|
+
"publint": "publint"
|
|
64
|
+
}
|
|
65
|
+
}
|