@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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/constants.d.ts +82 -1
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/field-name-matcher.d.ts +19 -0
  6. package/dist/field-name-matcher.d.ts.map +1 -1
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -1
  10. package/dist/jsf-config.d.ts +19 -4
  11. package/dist/jsf-config.d.ts.map +1 -1
  12. package/dist/output-limits.d.ts +3 -0
  13. package/dist/output-limits.d.ts.map +1 -0
  14. package/dist/overrides.d.ts +2 -1
  15. package/dist/overrides.d.ts.map +1 -1
  16. package/dist/schema-children.d.ts +14 -0
  17. package/dist/schema-children.d.ts.map +1 -0
  18. package/dist/schema-enhancement.d.ts +1 -0
  19. package/dist/schema-enhancement.d.ts.map +1 -1
  20. package/dist/validation.d.ts +24 -5
  21. package/dist/validation.d.ts.map +1 -1
  22. package/package.json +21 -7
  23. package/dist/constants.js +0 -8
  24. package/dist/field-mappings.js +0 -1114
  25. package/dist/field-name-matcher.js +0 -148
  26. package/dist/jsf-config.js +0 -117
  27. package/dist/overrides.js +0 -137
  28. package/dist/schema-enhancement.js +0 -104
  29. package/dist/test-utils.d.ts +0 -57
  30. package/dist/test-utils.d.ts.map +0 -1
  31. package/dist/test-utils.js +0 -284
  32. package/dist/validation.js +0 -300
  33. package/src/advanced-features.test.ts +0 -912
  34. package/src/audit-faker-args.test.ts +0 -97
  35. package/src/constants.d.ts.map +0 -1
  36. package/src/constants.ts +0 -9
  37. package/src/data-quality.test.ts +0 -415
  38. package/src/error-handling.test.ts +0 -504
  39. package/src/field-mappings.test.ts +0 -351
  40. package/src/field-mappings.ts +0 -1148
  41. package/src/field-name-matcher.test.ts +0 -292
  42. package/src/field-name-matcher.ts +0 -187
  43. package/src/index.d.ts.map +0 -1
  44. package/src/index.test.ts +0 -1320
  45. package/src/index.ts +0 -185
  46. package/src/integration.test.ts +0 -634
  47. package/src/jsf-config.d.ts.map +0 -1
  48. package/src/jsf-config.test.ts +0 -65
  49. package/src/jsf-config.ts +0 -156
  50. package/src/overrides.d.ts.map +0 -1
  51. package/src/overrides.property.test.ts +0 -172
  52. package/src/overrides.test.ts +0 -38
  53. package/src/overrides.ts +0 -179
  54. package/src/performance.test.ts +0 -441
  55. package/src/plugin-integration.test.ts +0 -575
  56. package/src/post-process.test.ts +0 -177
  57. package/src/real-world.test.ts +0 -636
  58. package/src/schema-enhancement.d.ts.map +0 -1
  59. package/src/schema-enhancement.test.ts +0 -222
  60. package/src/schema-enhancement.ts +0 -144
  61. package/src/steps/deterministic-seeds.steps.ts +0 -112
  62. package/src/steps/faker-plugin.steps.ts +0 -301
  63. package/src/test-utils.ts +0 -385
  64. package/src/validation.d.ts.map +0 -1
  65. 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
- }
@@ -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
- });
@@ -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
- }