@schmock/faker 2.2.3 → 2.3.1
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 +45 -0
- package/dist/constants.d.ts +82 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/field-mappings.d.ts.map +1 -1
- package/dist/field-name-matcher.d.ts +19 -0
- package/dist/field-name-matcher.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/jsf-config.d.ts +27 -2
- package/dist/jsf-config.d.ts.map +1 -1
- package/dist/output-limits.d.ts +3 -0
- package/dist/output-limits.d.ts.map +1 -0
- package/dist/overrides.d.ts +2 -1
- package/dist/overrides.d.ts.map +1 -1
- package/dist/schema-children.d.ts +14 -0
- package/dist/schema-children.d.ts.map +1 -0
- package/dist/schema-enhancement.d.ts +1 -0
- package/dist/schema-enhancement.d.ts.map +1 -1
- package/dist/validation.d.ts +24 -5
- package/dist/validation.d.ts.map +1 -1
- package/package.json +19 -9
- package/dist/constants.js +0 -8
- package/dist/field-mappings.js +0 -1107
- package/dist/field-name-matcher.js +0 -149
- package/dist/jsf-config.js +0 -35
- package/dist/overrides.js +0 -131
- package/dist/schema-enhancement.js +0 -89
- package/dist/test-utils.d.ts +0 -57
- package/dist/test-utils.d.ts.map +0 -1
- package/dist/test-utils.js +0 -271
- package/dist/validation.js +0 -300
- package/src/advanced-features.test.ts +0 -912
- package/src/audit-faker-args.test.ts +0 -97
- package/src/constants.d.ts.map +0 -1
- package/src/constants.ts +0 -9
- package/src/data-quality.test.ts +0 -415
- package/src/error-handling.test.ts +0 -504
- package/src/field-mappings.test.ts +0 -351
- package/src/field-mappings.ts +0 -1141
- package/src/field-name-matcher.test.ts +0 -292
- package/src/field-name-matcher.ts +0 -188
- package/src/index.d.ts.map +0 -1
- package/src/index.test.ts +0 -1218
- package/src/index.ts +0 -168
- package/src/integration.test.ts +0 -634
- package/src/jsf-config.d.ts.map +0 -1
- package/src/jsf-config.ts +0 -48
- package/src/overrides.d.ts.map +0 -1
- package/src/overrides.property.test.ts +0 -172
- package/src/overrides.test.ts +0 -38
- package/src/overrides.ts +0 -176
- package/src/performance.test.ts +0 -494
- package/src/plugin-integration.test.ts +0 -575
- package/src/post-process.test.ts +0 -151
- package/src/real-world.test.ts +0 -636
- package/src/schema-enhancement.d.ts.map +0 -1
- package/src/schema-enhancement.test.ts +0 -216
- package/src/schema-enhancement.ts +0 -128
- package/src/steps/deterministic-seeds.steps.ts +0 -35
- package/src/steps/faker-plugin.steps.ts +0 -109
- package/src/test-utils.ts +0 -365
- package/src/validation.d.ts.map +0 -1
- package/src/validation.ts +0 -417
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import fc from "fast-check";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { applyOverrides, determineArrayCount } from "./overrides";
|
|
4
|
-
|
|
5
|
-
describe("applyOverrides — property-based tests", () => {
|
|
6
|
-
it("never adds keys not in data or overrides (shape preservation)", () => {
|
|
7
|
-
// Use simple alphanumeric keys to avoid dot-path splitting edge cases,
|
|
8
|
-
// since keys containing "." are treated as nested paths by applyOverrides.
|
|
9
|
-
const safeKey = fc.stringMatching(/^[a-z][a-zA-Z0-9]{0,9}$/);
|
|
10
|
-
|
|
11
|
-
fc.assert(
|
|
12
|
-
fc.property(
|
|
13
|
-
fc.dictionary(safeKey, fc.jsonValue()),
|
|
14
|
-
fc.dictionary(safeKey, fc.jsonValue()),
|
|
15
|
-
(data, overrides) => {
|
|
16
|
-
const result = applyOverrides(data, overrides) as Record<
|
|
17
|
-
string,
|
|
18
|
-
unknown
|
|
19
|
-
>;
|
|
20
|
-
|
|
21
|
-
const allowedKeys = new Set([
|
|
22
|
-
...Object.keys(data),
|
|
23
|
-
...Object.keys(overrides),
|
|
24
|
-
]);
|
|
25
|
-
for (const key of Object.keys(result)) {
|
|
26
|
-
expect(allowedKeys.has(key)).toBe(true);
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
),
|
|
30
|
-
{ numRuns: 100 },
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("with empty overrides returns structuredClone of data", () => {
|
|
35
|
-
fc.assert(
|
|
36
|
-
fc.property(fc.dictionary(fc.string(), fc.jsonValue()), (data) => {
|
|
37
|
-
const result = applyOverrides(data, {});
|
|
38
|
-
|
|
39
|
-
// Should be deep-equal but not the same reference
|
|
40
|
-
expect(result).toEqual(data);
|
|
41
|
-
if (Object.keys(data).length > 0) {
|
|
42
|
-
expect(result).not.toBe(data);
|
|
43
|
-
}
|
|
44
|
-
}),
|
|
45
|
-
{ numRuns: 100 },
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("processTemplate idempotency: processing an already-resolved template is a no-op", () => {
|
|
50
|
-
// Resolved values contain no {{ }} markers, so re-applying overrides
|
|
51
|
-
// with the same non-template values should be idempotent
|
|
52
|
-
fc.assert(
|
|
53
|
-
fc.property(
|
|
54
|
-
fc.record({
|
|
55
|
-
name: fc.string().filter((s) => !s.includes("{{")),
|
|
56
|
-
age: fc.integer({ min: 0, max: 200 }),
|
|
57
|
-
}),
|
|
58
|
-
(data) => {
|
|
59
|
-
const overrides = { name: "Alice", age: 42 };
|
|
60
|
-
|
|
61
|
-
const first = applyOverrides(data, overrides);
|
|
62
|
-
const second = applyOverrides(
|
|
63
|
-
first as Record<string, unknown>,
|
|
64
|
-
overrides,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
expect(second).toEqual(first);
|
|
68
|
-
},
|
|
69
|
-
),
|
|
70
|
-
{ numRuns: 100 },
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("DANGEROUS_KEYS (__proto__, constructor, prototype) are always rejected in setNestedProperty", () => {
|
|
75
|
-
const dangerousKeys = ["__proto__", "constructor", "prototype"];
|
|
76
|
-
|
|
77
|
-
for (const dangerous of dangerousKeys) {
|
|
78
|
-
// Flat key — dangerous key is skipped entirely, so no own property is set
|
|
79
|
-
const flatResult = applyOverrides(
|
|
80
|
-
{ safe: "value" },
|
|
81
|
-
{ [dangerous]: "injected" },
|
|
82
|
-
) as Record<string, unknown>;
|
|
83
|
-
expect(Object.hasOwn(flatResult, dangerous)).toBe(false);
|
|
84
|
-
|
|
85
|
-
// Nested path with dangerous key at start — entire path is skipped
|
|
86
|
-
const nestedStartResult = applyOverrides(
|
|
87
|
-
{ safe: "value" },
|
|
88
|
-
{ [`${dangerous}.nested`]: "injected" },
|
|
89
|
-
) as Record<string, unknown>;
|
|
90
|
-
expect(Object.hasOwn(nestedStartResult, dangerous)).toBe(false);
|
|
91
|
-
|
|
92
|
-
// Nested path with dangerous key in middle — traversal aborts
|
|
93
|
-
const nestedMiddleResult = applyOverrides(
|
|
94
|
-
{ a: { b: "original" } },
|
|
95
|
-
{ [`a.${dangerous}.c`]: "injected" },
|
|
96
|
-
) as Record<string, unknown>;
|
|
97
|
-
const inner = nestedMiddleResult.a as Record<string, unknown>;
|
|
98
|
-
expect(Object.hasOwn(inner, dangerous)).toBe(false);
|
|
99
|
-
|
|
100
|
-
// Nested path with dangerous key at end — final key set is blocked
|
|
101
|
-
const nestedEndResult = applyOverrides(
|
|
102
|
-
{ a: { b: "original" } },
|
|
103
|
-
{ [`a.${dangerous}`]: "injected" },
|
|
104
|
-
) as Record<string, unknown>;
|
|
105
|
-
const innerEnd = nestedEndResult.a as Record<string, unknown>;
|
|
106
|
-
expect(Object.hasOwn(innerEnd, dangerous)).toBe(false);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("applyOverrides — edge cases", () => {
|
|
112
|
-
it("deeply nested path 'a.b.c.d.e' creates intermediate objects", () => {
|
|
113
|
-
const data = { existing: "value" };
|
|
114
|
-
const result = applyOverrides(data, {
|
|
115
|
-
"a.b.c.d.e": "deep",
|
|
116
|
-
}) as Record<string, unknown>;
|
|
117
|
-
|
|
118
|
-
expect(result.existing).toBe("value");
|
|
119
|
-
const a = result.a as Record<string, unknown>;
|
|
120
|
-
const b = a.b as Record<string, unknown>;
|
|
121
|
-
const c = b.c as Record<string, unknown>;
|
|
122
|
-
const d = c.d as Record<string, unknown>;
|
|
123
|
-
expect(d.e).toBe("deep");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("template '{{params.id}}' resolves correctly", () => {
|
|
127
|
-
const data = { userId: "placeholder" };
|
|
128
|
-
const result = applyOverrides(
|
|
129
|
-
data,
|
|
130
|
-
{ userId: "{{params.id}}" },
|
|
131
|
-
{ id: "abc-123" },
|
|
132
|
-
) as Record<string, unknown>;
|
|
133
|
-
|
|
134
|
-
expect(result.userId).toBe("abc-123");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("mixed template 'Hello {{params.name}}!' resolves to string", () => {
|
|
138
|
-
const data = { greeting: "placeholder" };
|
|
139
|
-
const result = applyOverrides(
|
|
140
|
-
data,
|
|
141
|
-
{ greeting: "Hello {{params.name}}!" },
|
|
142
|
-
{ name: "World" },
|
|
143
|
-
) as Record<string, unknown>;
|
|
144
|
-
|
|
145
|
-
expect(result.greeting).toBe("Hello World!");
|
|
146
|
-
expect(typeof result.greeting).toBe("string");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("on non-object data (null) returns data unchanged", () => {
|
|
150
|
-
expect(applyOverrides(null, { key: "value" })).toBe(null);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("on non-object data (string) returns data unchanged", () => {
|
|
154
|
-
expect(applyOverrides("hello", { key: "value" })).toBe("hello");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("on non-object data (number) returns data unchanged", () => {
|
|
158
|
-
expect(applyOverrides(42, { key: "value" })).toBe(42);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("determineArrayCount — edge cases", () => {
|
|
163
|
-
it("with explicitCount=0 returns 0", () => {
|
|
164
|
-
expect(determineArrayCount({}, 0)).toBe(0);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("with negative explicitCount returns 0", () => {
|
|
168
|
-
expect(determineArrayCount({}, -1)).toBe(0);
|
|
169
|
-
expect(determineArrayCount({}, -100)).toBe(0);
|
|
170
|
-
expect(determineArrayCount({}, Number.NEGATIVE_INFINITY)).toBe(0);
|
|
171
|
-
});
|
|
172
|
-
});
|
package/src/overrides.test.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { JSONSchema7 } from "json-schema";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { determineArrayCount } from "./overrides";
|
|
4
|
-
|
|
5
|
-
describe("determineArrayCount", () => {
|
|
6
|
-
it("returns explicit count when provided", () => {
|
|
7
|
-
expect(determineArrayCount({}, 5)).toBe(5);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("returns 0 for negative explicit count", () => {
|
|
11
|
-
expect(determineArrayCount({}, -3)).toBe(0);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("returns value within minItems/maxItems range", () => {
|
|
15
|
-
const schema: JSONSchema7 = { minItems: 2, maxItems: 5 };
|
|
16
|
-
for (let i = 0; i < 50; i++) {
|
|
17
|
-
const count = determineArrayCount(schema);
|
|
18
|
-
expect(count).toBeGreaterThanOrEqual(2);
|
|
19
|
-
expect(count).toBeLessThanOrEqual(5);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("handles minItems greater than maxItems without producing garbage", () => {
|
|
24
|
-
const schema: JSONSchema7 = { minItems: 10, maxItems: 3 };
|
|
25
|
-
const count = determineArrayCount(schema);
|
|
26
|
-
|
|
27
|
-
// Should not produce negative numbers or unreasonably large values
|
|
28
|
-
expect(count).toBeGreaterThanOrEqual(0);
|
|
29
|
-
expect(Number.isFinite(count)).toBe(true);
|
|
30
|
-
expect(Number.isNaN(count)).toBe(false);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("handles minItems equal to maxItems", () => {
|
|
34
|
-
const schema: JSONSchema7 = { minItems: 5, maxItems: 5 };
|
|
35
|
-
const count = determineArrayCount(schema);
|
|
36
|
-
expect(count).toBe(5);
|
|
37
|
-
});
|
|
38
|
-
});
|
package/src/overrides.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import type { JSONSchema7 } from "json-schema";
|
|
2
|
-
import { DEFAULT_ARRAY_COUNT } from "./constants.js";
|
|
3
|
-
|
|
4
|
-
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Determine number of items to generate for array schema
|
|
8
|
-
* Prefers explicit count, then schema minItems/maxItems, with sane defaults
|
|
9
|
-
* @param schema - Array schema with optional minItems/maxItems
|
|
10
|
-
* @param explicitCount - Explicit count override from plugin options
|
|
11
|
-
* @returns Number of array items to generate
|
|
12
|
-
*/
|
|
13
|
-
export function determineArrayCount(
|
|
14
|
-
schema: JSONSchema7,
|
|
15
|
-
explicitCount?: number,
|
|
16
|
-
random: () => number = Math.random,
|
|
17
|
-
): number {
|
|
18
|
-
if (explicitCount !== undefined) {
|
|
19
|
-
// Handle negative or invalid counts
|
|
20
|
-
if (explicitCount < 0) {
|
|
21
|
-
return 0;
|
|
22
|
-
}
|
|
23
|
-
return explicitCount;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (schema.minItems !== undefined && schema.maxItems !== undefined) {
|
|
27
|
-
const min = schema.minItems;
|
|
28
|
-
const max = Math.max(min, schema.maxItems);
|
|
29
|
-
return Math.floor(random() * (max - min + 1)) + min;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (schema.minItems !== undefined) {
|
|
33
|
-
return Math.max(schema.minItems, DEFAULT_ARRAY_COUNT);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (schema.maxItems !== undefined) {
|
|
37
|
-
return Math.min(schema.maxItems, DEFAULT_ARRAY_COUNT);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return DEFAULT_ARRAY_COUNT;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Apply overrides to generated data with support for templates
|
|
45
|
-
* Supports nested paths (dot notation), templates with {{params.id}}, and state access
|
|
46
|
-
* @param data - Generated data to apply overrides to
|
|
47
|
-
* @param overrides - Override values (can use templates)
|
|
48
|
-
* @param params - Route parameters for template expansion
|
|
49
|
-
* @param state - Plugin state for template expansion
|
|
50
|
-
* @param query - Query parameters for template expansion
|
|
51
|
-
* @returns Data with overrides applied
|
|
52
|
-
*/
|
|
53
|
-
export function applyOverrides(
|
|
54
|
-
data: unknown,
|
|
55
|
-
overrides?: Record<string, unknown>,
|
|
56
|
-
params?: Record<string, string>,
|
|
57
|
-
state?: Record<string, unknown>,
|
|
58
|
-
query?: Record<string, string>,
|
|
59
|
-
): unknown {
|
|
60
|
-
if (!overrides) return data;
|
|
61
|
-
if (typeof data !== "object" || data === null) return data;
|
|
62
|
-
|
|
63
|
-
const result = structuredClone(data) as Record<string, unknown>;
|
|
64
|
-
|
|
65
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
66
|
-
if (DANGEROUS_KEYS.has(key)) continue;
|
|
67
|
-
// Handle nested paths like "data.id" or "pagination.page"
|
|
68
|
-
if (key.includes(".")) {
|
|
69
|
-
setNestedProperty(result, key, value, { params, state, query });
|
|
70
|
-
} else {
|
|
71
|
-
// Handle flat keys and nested objects
|
|
72
|
-
if (
|
|
73
|
-
typeof value === "object" &&
|
|
74
|
-
value !== null &&
|
|
75
|
-
!Array.isArray(value)
|
|
76
|
-
) {
|
|
77
|
-
const nested = value as Record<string, unknown>;
|
|
78
|
-
// Recursively apply nested overrides
|
|
79
|
-
if (result[key] && typeof result[key] === "object") {
|
|
80
|
-
result[key] = applyOverrides(
|
|
81
|
-
result[key],
|
|
82
|
-
nested,
|
|
83
|
-
params,
|
|
84
|
-
state,
|
|
85
|
-
query,
|
|
86
|
-
);
|
|
87
|
-
} else {
|
|
88
|
-
result[key] = applyOverrides({}, nested, params, state, query);
|
|
89
|
-
}
|
|
90
|
-
} else if (typeof value === "string" && value.includes("{{")) {
|
|
91
|
-
// Template processing
|
|
92
|
-
result[key] = processTemplate(value, { params, state, query });
|
|
93
|
-
} else {
|
|
94
|
-
result[key] = value;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return result;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function setNestedProperty(
|
|
103
|
-
obj: Record<string, unknown>,
|
|
104
|
-
path: string,
|
|
105
|
-
value: unknown,
|
|
106
|
-
context: TemplateContext,
|
|
107
|
-
): void {
|
|
108
|
-
const parts = path.split(".");
|
|
109
|
-
let current: Record<string, unknown> = obj;
|
|
110
|
-
|
|
111
|
-
// Navigate to the parent of the target property
|
|
112
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
113
|
-
const part = parts[i];
|
|
114
|
-
if (DANGEROUS_KEYS.has(part)) return;
|
|
115
|
-
if (
|
|
116
|
-
!(part in current) ||
|
|
117
|
-
typeof current[part] !== "object" ||
|
|
118
|
-
current[part] === null
|
|
119
|
-
) {
|
|
120
|
-
current[part] = {};
|
|
121
|
-
}
|
|
122
|
-
current = current[part] as Record<string, unknown>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Set the final property
|
|
126
|
-
const finalKey = parts[parts.length - 1];
|
|
127
|
-
if (DANGEROUS_KEYS.has(finalKey)) return;
|
|
128
|
-
if (typeof value === "string" && value.includes("{{")) {
|
|
129
|
-
current[finalKey] = processTemplate(value, context);
|
|
130
|
-
} else {
|
|
131
|
-
current[finalKey] = value;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
interface TemplateContext {
|
|
136
|
-
params?: Record<string, string>;
|
|
137
|
-
state?: Record<string, unknown>;
|
|
138
|
-
query?: Record<string, string>;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function resolveTemplatePath(
|
|
142
|
-
context: TemplateContext,
|
|
143
|
-
expression: string,
|
|
144
|
-
): unknown {
|
|
145
|
-
const parts = expression.trim().split(".");
|
|
146
|
-
let result: unknown = context;
|
|
147
|
-
|
|
148
|
-
for (const part of parts) {
|
|
149
|
-
if (result && typeof result === "object") {
|
|
150
|
-
result = (result as Record<string, unknown>)[part];
|
|
151
|
-
} else {
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function processTemplate(template: string, context: TemplateContext): unknown {
|
|
160
|
-
// Check if the template is just a single template expression
|
|
161
|
-
const singleTemplateMatch = template.match(/^\{\{\s*([^}]+)\s*\}\}$/);
|
|
162
|
-
if (singleTemplateMatch) {
|
|
163
|
-
// For single templates, return the actual value without string conversion
|
|
164
|
-
const result = resolveTemplatePath(context, singleTemplateMatch[1]);
|
|
165
|
-
return result !== undefined ? result : template;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// For templates mixed with other text, do string replacement
|
|
169
|
-
return template.replace(
|
|
170
|
-
/\{\{\s*([^}]+)\s*\}\}/g,
|
|
171
|
-
(match, expression: string) => {
|
|
172
|
-
const result = resolveTemplatePath(context, expression);
|
|
173
|
-
return result !== undefined ? String(result) : match;
|
|
174
|
-
},
|
|
175
|
-
);
|
|
176
|
-
}
|