@schmock/faker 2.3.0 → 2.4.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 +45 -0
- package/dist/constants.d.ts +82 -1
- package/dist/constants.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 +19 -4
- 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 +21 -7
- package/dist/constants.js +0 -8
- package/dist/field-mappings.js +0 -1114
- package/dist/field-name-matcher.js +0 -148
- package/dist/jsf-config.js +0 -117
- package/dist/overrides.js +0 -137
- package/dist/schema-enhancement.js +0 -104
- package/dist/test-utils.d.ts +0 -57
- package/dist/test-utils.d.ts.map +0 -1
- package/dist/test-utils.js +0 -284
- 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 -1148
- package/src/field-name-matcher.test.ts +0 -292
- package/src/field-name-matcher.ts +0 -187
- package/src/index.d.ts.map +0 -1
- package/src/index.test.ts +0 -1320
- package/src/index.ts +0 -185
- package/src/integration.test.ts +0 -634
- package/src/jsf-config.d.ts.map +0 -1
- package/src/jsf-config.test.ts +0 -65
- package/src/jsf-config.ts +0 -156
- 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 -179
- package/src/performance.test.ts +0 -441
- package/src/plugin-integration.test.ts +0 -575
- package/src/post-process.test.ts +0 -177
- package/src/real-world.test.ts +0 -636
- package/src/schema-enhancement.d.ts.map +0 -1
- package/src/schema-enhancement.test.ts +0 -222
- package/src/schema-enhancement.ts +0 -144
- package/src/steps/deterministic-seeds.steps.ts +0 -112
- package/src/steps/faker-plugin.steps.ts +0 -301
- package/src/test-utils.ts +0 -385
- package/src/validation.d.ts.map +0 -1
- package/src/validation.ts +0 -417
package/src/jsf-config.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { base, en, Faker } from "@faker-js/faker";
|
|
2
|
-
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
|
|
3
|
-
import {
|
|
4
|
-
type GenerateOptions,
|
|
5
|
-
generate,
|
|
6
|
-
type JsonSchema,
|
|
7
|
-
} from "json-schema-faker";
|
|
8
|
-
|
|
9
|
-
const MAX_GENERATION_SEED = 2_147_483_647;
|
|
10
|
-
type JsfObjectSchema = Exclude<JsonSchema, boolean>;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Create isolated faker instance to avoid race conditions.
|
|
14
|
-
* Each generation gets its own faker instance to ensure thread-safety.
|
|
15
|
-
*/
|
|
16
|
-
export function createFakerInstance(seed?: number) {
|
|
17
|
-
const faker = new Faker({ locale: [en, base] });
|
|
18
|
-
if (seed !== undefined) {
|
|
19
|
-
faker.seed(seed);
|
|
20
|
-
}
|
|
21
|
-
return faker;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function resolveGenerationSeed(seed?: number): number {
|
|
25
|
-
return seed ?? Math.floor(Math.random() * MAX_GENERATION_SEED);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createSeededRandom(seed: number): () => number {
|
|
29
|
-
let state = seed;
|
|
30
|
-
return () => {
|
|
31
|
-
state += 0x6d2b79f5;
|
|
32
|
-
let value = state;
|
|
33
|
-
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
34
|
-
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
35
|
-
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeDefinitionForJsf(
|
|
40
|
-
definition: JSONSchema7Definition,
|
|
41
|
-
): JsonSchema {
|
|
42
|
-
return typeof definition === "boolean"
|
|
43
|
-
? definition
|
|
44
|
-
: normalizeSchemaForJsf(definition);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function normalizeDefinitionMap(
|
|
48
|
-
definitions: Record<string, JSONSchema7Definition>,
|
|
49
|
-
): Record<string, JsonSchema> {
|
|
50
|
-
return Object.fromEntries(
|
|
51
|
-
Object.entries(definitions).map(([key, definition]) => [
|
|
52
|
-
key,
|
|
53
|
-
normalizeDefinitionForJsf(definition),
|
|
54
|
-
]),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* json-schema-faker consumes tuple schemas through the 2020-12 `prefixItems`
|
|
60
|
-
* keyword. Convert Draft 7 tuple `items` recursively at the library boundary.
|
|
61
|
-
*/
|
|
62
|
-
export function normalizeSchemaForJsf(schema: JSONSchema7): JsfObjectSchema {
|
|
63
|
-
const normalized: JsfObjectSchema = {};
|
|
64
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
65
|
-
normalized[key] = value;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (schema.properties) {
|
|
69
|
-
normalized.properties = normalizeDefinitionMap(schema.properties);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (schema.definitions) {
|
|
73
|
-
normalized.definitions = normalizeDefinitionMap(schema.definitions);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (schema.$defs) {
|
|
77
|
-
normalized.$defs = normalizeDefinitionMap(schema.$defs);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (schema.patternProperties) {
|
|
81
|
-
normalized.patternProperties = normalizeDefinitionMap(
|
|
82
|
-
schema.patternProperties,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (Array.isArray(schema.items)) {
|
|
87
|
-
normalized.prefixItems = schema.items.map(normalizeDefinitionForJsf);
|
|
88
|
-
normalized.items =
|
|
89
|
-
schema.additionalItems === undefined
|
|
90
|
-
? true
|
|
91
|
-
: normalizeDefinitionForJsf(schema.additionalItems);
|
|
92
|
-
delete normalized.additionalItems;
|
|
93
|
-
} else if (schema.items !== undefined) {
|
|
94
|
-
normalized.items = normalizeDefinitionForJsf(schema.items);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
for (const keyword of ["allOf", "anyOf", "oneOf"] as const) {
|
|
98
|
-
const definitions = schema[keyword];
|
|
99
|
-
if (definitions) {
|
|
100
|
-
normalized[keyword] = definitions.map(normalizeDefinitionForJsf);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (schema.additionalProperties !== undefined) {
|
|
105
|
-
normalized.additionalProperties = normalizeDefinitionForJsf(
|
|
106
|
-
schema.additionalProperties,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (const keyword of [
|
|
111
|
-
"contains",
|
|
112
|
-
"not",
|
|
113
|
-
"if",
|
|
114
|
-
"then",
|
|
115
|
-
"else",
|
|
116
|
-
"propertyNames",
|
|
117
|
-
] as const) {
|
|
118
|
-
const definition = schema[keyword];
|
|
119
|
-
if (definition !== undefined) {
|
|
120
|
-
normalized[keyword] = normalizeDefinitionForJsf(definition);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (schema.dependencies) {
|
|
125
|
-
normalized.dependencies = Object.fromEntries(
|
|
126
|
-
Object.entries(schema.dependencies).map(([key, dependency]) => [
|
|
127
|
-
key,
|
|
128
|
-
Array.isArray(dependency)
|
|
129
|
-
? [...dependency]
|
|
130
|
-
: normalizeDefinitionForJsf(dependency),
|
|
131
|
-
]),
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return normalized;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Generate data from a JSON schema using json-schema-faker 0.6.0 async API.
|
|
140
|
-
* Stateless — each call is self-contained with its own faker instance and options.
|
|
141
|
-
*/
|
|
142
|
-
export async function generateWithJsf(
|
|
143
|
-
schema: JSONSchema7,
|
|
144
|
-
seed: number,
|
|
145
|
-
): Promise<unknown> {
|
|
146
|
-
const options: GenerateOptions = {
|
|
147
|
-
seed,
|
|
148
|
-
optionalsProbability: 1.0,
|
|
149
|
-
alwaysFakeOptionals: true,
|
|
150
|
-
useDefaultValue: true,
|
|
151
|
-
failOnInvalidTypes: false,
|
|
152
|
-
extensions: { faker: createFakerInstance(seed) },
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
return generate(normalizeSchemaForJsf(schema), options);
|
|
156
|
-
}
|
package/src/overrides.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,aAAa,CAAC,EAAE,MAAM,EACtB,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAyBR;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,GAAG,EACT,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,KAAK,CAAC,EAAE,GAAG,EACX,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,GAAG,CAuCL"}
|
|
@@ -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,179 +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
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Determine number of items to generate for array schema
|
|
12
|
-
* Prefers explicit count, then schema minItems/maxItems, with sane defaults
|
|
13
|
-
* @param schema - Array schema with optional minItems/maxItems
|
|
14
|
-
* @param explicitCount - Explicit count override from plugin options
|
|
15
|
-
* @returns Number of array items to generate
|
|
16
|
-
*/
|
|
17
|
-
export function determineArrayCount(
|
|
18
|
-
schema: JSONSchema7,
|
|
19
|
-
explicitCount?: number,
|
|
20
|
-
random: () => number = Math.random,
|
|
21
|
-
): number {
|
|
22
|
-
if (explicitCount !== undefined) {
|
|
23
|
-
// Handle negative or invalid counts
|
|
24
|
-
if (explicitCount < 0) {
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
|
-
return explicitCount;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (schema.minItems !== undefined && schema.maxItems !== undefined) {
|
|
31
|
-
const min = schema.minItems;
|
|
32
|
-
const max = Math.max(min, schema.maxItems);
|
|
33
|
-
return Math.floor(random() * (max - min + 1)) + min;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (schema.minItems !== undefined) {
|
|
37
|
-
return Math.max(schema.minItems, DEFAULT_ARRAY_COUNT);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (schema.maxItems !== undefined) {
|
|
41
|
-
return Math.min(schema.maxItems, DEFAULT_ARRAY_COUNT);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (Array.isArray(schema.items)) {
|
|
45
|
-
return schema.items.length;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return DEFAULT_ARRAY_COUNT;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Apply overrides to generated data with support for templates
|
|
53
|
-
* Supports nested paths (dot notation), templates with {{params.id}}, and state access
|
|
54
|
-
* @param data - Generated data to apply overrides to
|
|
55
|
-
* @param overrides - Override values (can use templates)
|
|
56
|
-
* @param params - Route parameters for template expansion
|
|
57
|
-
* @param state - Plugin state for template expansion
|
|
58
|
-
* @param query - Query parameters for template expansion
|
|
59
|
-
* @returns Data with overrides applied
|
|
60
|
-
*/
|
|
61
|
-
export function applyOverrides(
|
|
62
|
-
data: unknown,
|
|
63
|
-
overrides?: Record<string, unknown>,
|
|
64
|
-
params?: Record<string, string>,
|
|
65
|
-
state?: Record<string, unknown>,
|
|
66
|
-
query?: Record<string, string>,
|
|
67
|
-
): unknown {
|
|
68
|
-
if (!overrides) return data;
|
|
69
|
-
if (!isRecord(data)) return data;
|
|
70
|
-
|
|
71
|
-
const result = structuredClone(data);
|
|
72
|
-
|
|
73
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
74
|
-
if (DANGEROUS_KEYS.has(key)) continue;
|
|
75
|
-
// Handle nested paths like "data.id" or "pagination.page"
|
|
76
|
-
if (key.includes(".")) {
|
|
77
|
-
setNestedProperty(result, key, value, { params, state, query });
|
|
78
|
-
} else {
|
|
79
|
-
// Handle flat keys and nested objects
|
|
80
|
-
if (isRecord(value)) {
|
|
81
|
-
// Recursively apply nested overrides
|
|
82
|
-
if (isRecord(result[key])) {
|
|
83
|
-
result[key] = applyOverrides(
|
|
84
|
-
result[key],
|
|
85
|
-
value,
|
|
86
|
-
params,
|
|
87
|
-
state,
|
|
88
|
-
query,
|
|
89
|
-
);
|
|
90
|
-
} else {
|
|
91
|
-
result[key] = applyOverrides({}, value, params, state, query);
|
|
92
|
-
}
|
|
93
|
-
} else if (typeof value === "string" && value.includes("{{")) {
|
|
94
|
-
// Template processing
|
|
95
|
-
result[key] = processTemplate(value, { params, state, query });
|
|
96
|
-
} else {
|
|
97
|
-
result[key] = value;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function setNestedProperty(
|
|
106
|
-
obj: Record<string, unknown>,
|
|
107
|
-
path: string,
|
|
108
|
-
value: unknown,
|
|
109
|
-
context: TemplateContext,
|
|
110
|
-
): void {
|
|
111
|
-
const parts = path.split(".");
|
|
112
|
-
let current: Record<string, unknown> = obj;
|
|
113
|
-
|
|
114
|
-
// Navigate to the parent of the target property
|
|
115
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
116
|
-
const part = parts[i];
|
|
117
|
-
if (DANGEROUS_KEYS.has(part)) return;
|
|
118
|
-
const next = current[part];
|
|
119
|
-
if (isRecord(next)) {
|
|
120
|
-
current = next;
|
|
121
|
-
} else {
|
|
122
|
-
const nested: Record<string, unknown> = {};
|
|
123
|
-
current[part] = nested;
|
|
124
|
-
current = nested;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Set the final property
|
|
129
|
-
const finalKey = parts[parts.length - 1];
|
|
130
|
-
if (DANGEROUS_KEYS.has(finalKey)) return;
|
|
131
|
-
if (typeof value === "string" && value.includes("{{")) {
|
|
132
|
-
current[finalKey] = processTemplate(value, context);
|
|
133
|
-
} else {
|
|
134
|
-
current[finalKey] = value;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
interface TemplateContext {
|
|
139
|
-
params?: Record<string, string>;
|
|
140
|
-
state?: Record<string, unknown>;
|
|
141
|
-
query?: Record<string, string>;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function resolveTemplatePath(
|
|
145
|
-
context: TemplateContext,
|
|
146
|
-
expression: string,
|
|
147
|
-
): unknown {
|
|
148
|
-
const parts = expression.trim().split(".");
|
|
149
|
-
let result: unknown = context;
|
|
150
|
-
|
|
151
|
-
for (const part of parts) {
|
|
152
|
-
if (isRecord(result)) {
|
|
153
|
-
result = result[part];
|
|
154
|
-
} else {
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function processTemplate(template: string, context: TemplateContext): unknown {
|
|
163
|
-
// Check if the template is just a single template expression
|
|
164
|
-
const singleTemplateMatch = template.match(/^\{\{\s*([^}]+)\s*\}\}$/);
|
|
165
|
-
if (singleTemplateMatch) {
|
|
166
|
-
// For single templates, return the actual value without string conversion
|
|
167
|
-
const result = resolveTemplatePath(context, singleTemplateMatch[1]);
|
|
168
|
-
return result !== undefined ? result : template;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// For templates mixed with other text, do string replacement
|
|
172
|
-
return template.replace(
|
|
173
|
-
/\{\{\s*([^}]+)\s*\}\}/g,
|
|
174
|
-
(match, expression: string) => {
|
|
175
|
-
const result = resolveTemplatePath(context, expression);
|
|
176
|
-
return result !== undefined ? String(result) : match;
|
|
177
|
-
},
|
|
178
|
-
);
|
|
179
|
-
}
|