@schmock/faker 1.9.4 → 1.10.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.
@@ -1 +1 @@
1
- {"version":3,"file":"jsf-config.d.ts","sourceRoot":"","sources":["../src/jsf-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAM,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,GAAG,MAAM,mBAAmB,CAAC;AAEpC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,CAAC,EAAE,MAAM,SAMhD;AAoBD,wBAAgB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,CAuBhD"}
1
+ {"version":3,"file":"jsf-config.d.ts","sourceRoot":"","sources":["../src/jsf-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,GAAG,MAAM,mBAAmB,CAAC;AAEpC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,CAAC,EAAE,MAAM,SAMhD;AAoBD,wBAAgB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,CAuBhD"}
@@ -1,4 +1,4 @@
1
- import { en, Faker } from "@faker-js/faker";
1
+ import { base, en, Faker } from "@faker-js/faker";
2
2
  import jsf from "json-schema-faker";
3
3
  /**
4
4
  * Create isolated faker instance to avoid race conditions
@@ -6,7 +6,7 @@ import jsf from "json-schema-faker";
6
6
  * @returns Fresh Faker instance with English locale
7
7
  */
8
8
  export function createFakerInstance(seed) {
9
- const faker = new Faker({ locale: [en] });
9
+ const faker = new Faker({ locale: [en, base] });
10
10
  if (seed !== undefined) {
11
11
  faker.seed(seed);
12
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"schema-enhancement.d.ts","sourceRoot":"","sources":["../src/schema-enhancement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAQ/C,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,WAAW,GAClB,WAAW,CAwBb"}
1
+ {"version":3,"file":"schema-enhancement.d.ts","sourceRoot":"","sources":["../src/schema-enhancement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAW/C,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,WAAW,GAClB,WAAW,CAwDb"}
@@ -1,3 +1,4 @@
1
+ import { findBestMapping } from "./field-name-matcher.js";
1
2
  import { isJSONSchema7, validateFakerMethod } from "./validation.js";
2
3
  export function enhanceSchemaWithSmartMapping(schema) {
3
4
  if (!schema || typeof schema !== "object") {
@@ -5,7 +6,7 @@ export function enhanceSchemaWithSmartMapping(schema) {
5
6
  }
6
7
  const enhanced = { ...schema };
7
8
  // Handle object properties
8
- if (enhanced.type === "object" && enhanced.properties) {
9
+ if (enhanced.properties) {
9
10
  enhanced.properties = { ...enhanced.properties };
10
11
  for (const [fieldName, fieldSchema] of Object.entries(enhanced.properties)) {
11
12
  if (isJSONSchema7(fieldSchema)) {
@@ -13,6 +14,28 @@ export function enhanceSchemaWithSmartMapping(schema) {
13
14
  }
14
15
  }
15
16
  }
17
+ // Recurse into composition keywords
18
+ for (const keyword of ["allOf", "anyOf", "oneOf"]) {
19
+ const branches = enhanced[keyword];
20
+ if (Array.isArray(branches)) {
21
+ enhanced[keyword] = branches.map((branch) => isJSONSchema7(branch) ? enhanceSchemaWithSmartMapping(branch) : branch);
22
+ }
23
+ }
24
+ // Recurse into array items
25
+ if (enhanced.items) {
26
+ if (Array.isArray(enhanced.items)) {
27
+ enhanced.items = enhanced.items.map((item) => isJSONSchema7(item) ? enhanceSchemaWithSmartMapping(item) : item);
28
+ }
29
+ else if (isJSONSchema7(enhanced.items)) {
30
+ enhanced.items = enhanceSchemaWithSmartMapping(enhanced.items);
31
+ }
32
+ }
33
+ // Recurse into additionalProperties
34
+ if (enhanced.additionalProperties &&
35
+ typeof enhanced.additionalProperties === "object" &&
36
+ !Array.isArray(enhanced.additionalProperties)) {
37
+ enhanced.additionalProperties = enhanceSchemaWithSmartMapping(enhanced.additionalProperties);
38
+ }
16
39
  return enhanced;
17
40
  }
18
41
  function enhanceFieldSchema(fieldName, fieldSchema) {
@@ -22,60 +45,26 @@ function enhanceFieldSchema(fieldName, fieldSchema) {
22
45
  validateFakerMethod(enhanced.faker);
23
46
  return enhanced;
24
47
  }
25
- // Apply smart field name mapping
26
- const lowerFieldName = fieldName.toLowerCase();
27
- // Email fields
28
- if (lowerFieldName.includes("email")) {
29
- enhanced.format = "email";
30
- enhanced.faker = "internet.email";
31
- }
32
- // Name fields
33
- else if (lowerFieldName === "firstname" || lowerFieldName === "first_name") {
34
- enhanced.faker = "person.firstName";
35
- }
36
- else if (lowerFieldName === "lastname" || lowerFieldName === "last_name") {
37
- enhanced.faker = "person.lastName";
38
- }
39
- else if (lowerFieldName === "name" || lowerFieldName === "fullname") {
40
- enhanced.faker = "person.fullName";
41
- }
42
- // Phone fields
43
- else if (lowerFieldName.includes("phone") || lowerFieldName === "mobile") {
44
- enhanced.faker = "phone.number";
48
+ // Recursively enhance nested schemas first
49
+ const hasComposition = enhanced.allOf || enhanced.anyOf || enhanced.oneOf;
50
+ if (enhanced.properties || hasComposition || enhanced.items) {
51
+ const recursed = enhanceSchemaWithSmartMapping(enhanced);
52
+ Object.assign(enhanced, recursed);
45
53
  }
46
- // Address fields
47
- else if (lowerFieldName === "street" || lowerFieldName === "address") {
48
- enhanced.faker = "location.streetAddress";
49
- }
50
- else if (lowerFieldName === "city") {
51
- enhanced.faker = "location.city";
52
- }
53
- else if (lowerFieldName === "zipcode" || lowerFieldName === "zip") {
54
- enhanced.faker = "location.zipCode";
55
- }
56
- // UUID fields
57
- else if (lowerFieldName === "uuid" ||
58
- (lowerFieldName === "id" && enhanced.format === "uuid")) {
59
- enhanced.faker = "string.uuid";
60
- }
61
- // Date fields
62
- else if (lowerFieldName.includes("createdat") ||
63
- lowerFieldName.includes("created_at") ||
64
- lowerFieldName.includes("updatedat") ||
65
- lowerFieldName.includes("updated_at")) {
66
- enhanced.format = "date-time";
67
- enhanced.faker = "date.recent";
68
- }
69
- // Company fields
70
- else if (lowerFieldName.includes("company")) {
71
- enhanced.faker = "company.name";
72
- }
73
- else if (lowerFieldName === "position" || lowerFieldName === "jobtitle") {
74
- enhanced.faker = "person.jobTitle";
54
+ // Don't apply field-level faker mapping to composition schemas — the branches define their own types
55
+ if (hasComposition) {
56
+ return enhanced;
75
57
  }
76
- // Price/money fields
77
- else if (lowerFieldName === "price" || lowerFieldName === "amount") {
78
- enhanced.faker = "commerce.price";
58
+ // Apply smart field name mapping via the scoring matcher
59
+ const match = findBestMapping(fieldName, enhanced);
60
+ if (match) {
61
+ enhanced.faker = match.mapping.fakerMethod;
62
+ if (match.mapping.format) {
63
+ enhanced.format = match.mapping.format;
64
+ }
65
+ if (match.mapping.trueProbability !== undefined) {
66
+ enhanced.schmockTrueProbability = match.mapping.trueProbability;
67
+ }
79
68
  }
80
69
  return enhanced;
81
70
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/faker",
3
3
  "description": "Faker-powered automatic data generation for Schmock",
4
- "version": "1.9.4",
4
+ "version": "1.10.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "scripts": {
21
21
  "build": "bun build:lib && bun build:types",
22
- "build:lib": "bun build --minify --target node --outdir=dist src/index.ts",
22
+ "build:lib": "bun build --minify --target browser --outdir=dist src/index.ts --external @faker-js/faker --external json-schema-faker --external @schmock/core",
23
23
  "build:types": "tsc -p tsconfig.json",
24
24
  "test": "vitest",
25
25
  "test:watch": "vitest --watch",
@@ -34,7 +34,7 @@
34
34
  "json-schema-faker": "^0.5.6"
35
35
  },
36
36
  "peerDependencies": {
37
- "@schmock/core": "^1.9.4"
37
+ "@schmock/core": "^1.10.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@amiceli/vitest-cucumber": "^6.2.0",
@@ -0,0 +1,6 @@
1
+ export declare const MAX_ARRAY_SIZE = 10000;
2
+ export declare const MAX_NESTING_DEPTH = 10;
3
+ export declare const DEFAULT_ARRAY_COUNT = 3;
4
+ export declare const DEEP_NESTING_THRESHOLD = 3;
5
+ export declare const LARGE_ARRAY_THRESHOLD = 100;
6
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,cAAc,QAAQ,CAAC;AACpC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,qBAAqB,MAAM,CAAC"}
package/src/constants.ts CHANGED
@@ -4,3 +4,6 @@ export const MAX_NESTING_DEPTH = 10; // Reasonable limit for schema nesting
4
4
  export const DEFAULT_ARRAY_COUNT = 3; // Default items to generate when not specified
5
5
  export const DEEP_NESTING_THRESHOLD = 3; // Depth at which to check for memory risks
6
6
  export const LARGE_ARRAY_THRESHOLD = 100; // Array size considered "large"
7
+
8
+ // Data quality tuning
9
+ export const NULLABLE_NULL_PROBABILITY = 0.05; // 5% chance nullable fields are null
@@ -0,0 +1,340 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ALL_FIELD_MAPPINGS, type FieldMapping } from "./field-mappings";
3
+ import { findBestMapping } from "./field-name-matcher";
4
+ import { generateFromSchema } from "./index";
5
+
6
+ /**
7
+ * Generate a value for a field by placing it inside an object schema
8
+ * and calling generateFromSchema. Returns the generated value.
9
+ */
10
+ function generateField(fieldName: string, schemaType: string): unknown {
11
+ const schema = {
12
+ type: "object" as const,
13
+ properties: {
14
+ [fieldName]: { type: schemaType as "string" | "number" | "boolean" },
15
+ },
16
+ };
17
+ const result = generateFromSchema({ schema });
18
+ return result[fieldName];
19
+ }
20
+
21
+ // ─── Matcher coverage: every mapping's first keyword resolves ───────
22
+
23
+ describe("Field mapping matcher coverage", () => {
24
+ for (const mapping of ALL_FIELD_MAPPINGS) {
25
+ const keyword = mapping.keywords[0];
26
+ const type = mapping.schemaType ?? "string";
27
+
28
+ it(`"${keyword}" → ${mapping.fakerMethod}`, () => {
29
+ const match = findBestMapping(keyword, {
30
+ type: type as "string" | "number" | "boolean",
31
+ });
32
+ expect(match).toBeDefined();
33
+ expect(match?.mapping.fakerMethod).toBe(mapping.fakerMethod);
34
+ });
35
+ }
36
+ });
37
+
38
+ // ─── Generation: every mapping produces a value of the right type ──
39
+
40
+ describe("Field mapping generation", () => {
41
+ // Group mappings by category for readable output
42
+ const stringMappings = ALL_FIELD_MAPPINGS.filter(
43
+ (m) => !m.schemaType || m.schemaType === "string",
44
+ );
45
+ const numberMappings = ALL_FIELD_MAPPINGS.filter(
46
+ (m) => m.schemaType === "number",
47
+ );
48
+ const booleanMappings = ALL_FIELD_MAPPINGS.filter(
49
+ (m) => m.schemaType === "boolean",
50
+ );
51
+
52
+ describe("string fields", () => {
53
+ for (const mapping of stringMappings) {
54
+ const keyword = mapping.keywords[0];
55
+
56
+ it(`"${keyword}" generates a non-empty string`, () => {
57
+ const value = generateField(keyword, "string");
58
+ expect(typeof value).toBe("string");
59
+ expect((value as string).length).toBeGreaterThan(0);
60
+ });
61
+ }
62
+ });
63
+
64
+ describe("number fields", () => {
65
+ for (const mapping of numberMappings) {
66
+ const keyword = mapping.keywords[0];
67
+
68
+ it(`"${keyword}" generates a number`, () => {
69
+ const value = generateField(keyword, "number");
70
+ expect(typeof value).toBe("number");
71
+ expect(Number.isFinite(value as number)).toBe(true);
72
+ });
73
+ }
74
+ });
75
+
76
+ describe("boolean fields", () => {
77
+ for (const mapping of booleanMappings) {
78
+ const keyword = mapping.keywords[0];
79
+
80
+ it(`"${keyword}" generates a boolean`, () => {
81
+ const value = generateField(keyword, "boolean");
82
+ expect(typeof value).toBe("boolean");
83
+ });
84
+ }
85
+ });
86
+ });
87
+
88
+ // ─── All keyword variants resolve to the same faker method ─────────
89
+
90
+ describe("All keyword variants match", () => {
91
+ for (const mapping of ALL_FIELD_MAPPINGS) {
92
+ if (mapping.keywords.length <= 1) continue;
93
+
94
+ describe(`${mapping.fakerMethod}`, () => {
95
+ for (const keyword of mapping.keywords) {
96
+ it(`"${keyword}" matches`, () => {
97
+ const match = findBestMapping(keyword, {
98
+ type: (mapping.schemaType ?? "string") as
99
+ | "string"
100
+ | "number"
101
+ | "boolean",
102
+ });
103
+ expect(match).toBeDefined();
104
+ expect(match?.mapping.fakerMethod).toBe(mapping.fakerMethod);
105
+ });
106
+ }
107
+ });
108
+ }
109
+ });
110
+
111
+ // ─── Category-specific quality checks ──────────────────────────────
112
+
113
+ describe("Category quality checks", () => {
114
+ function samples(fieldName: string, type: string, count = 10): unknown[] {
115
+ return Array.from({ length: count }, () => generateField(fieldName, type));
116
+ }
117
+
118
+ describe("Identity & Auth", () => {
119
+ it("email contains @ and .", () => {
120
+ for (const v of samples("email", "string")) {
121
+ expect(v).toMatch(/@.*\./);
122
+ }
123
+ });
124
+
125
+ it("username is a non-empty string", () => {
126
+ for (const v of samples("username", "string")) {
127
+ expect(typeof v).toBe("string");
128
+ expect((v as string).length).toBeGreaterThan(0);
129
+ }
130
+ });
131
+ });
132
+
133
+ describe("Person", () => {
134
+ it("firstName starts with uppercase", () => {
135
+ for (const v of samples("firstName", "string")) {
136
+ expect(v).toMatch(/^[A-Z]/);
137
+ }
138
+ });
139
+
140
+ it("lastName starts with uppercase", () => {
141
+ for (const v of samples("lastName", "string")) {
142
+ expect(v).toMatch(/^[A-Z]/);
143
+ }
144
+ });
145
+
146
+ it("name (fullName) contains a space", () => {
147
+ for (const v of samples("name", "string")) {
148
+ expect(v).toContain(" ");
149
+ }
150
+ });
151
+ });
152
+
153
+ describe("Contact", () => {
154
+ it("phone contains digits", () => {
155
+ for (const v of samples("phone", "string")) {
156
+ expect(v).toMatch(/\d/);
157
+ }
158
+ });
159
+ });
160
+
161
+ describe("Address", () => {
162
+ it("city is a capitalized string", () => {
163
+ for (const v of samples("city", "string")) {
164
+ expect(v).toMatch(/^[A-Z]/);
165
+ }
166
+ });
167
+
168
+ it("zipcode matches US format", () => {
169
+ for (const v of samples("zipcode", "string")) {
170
+ expect(v).toMatch(/^\d{5}/);
171
+ }
172
+ });
173
+
174
+ it("latitude is a valid number", () => {
175
+ for (const v of samples("latitude", "number")) {
176
+ expect(typeof v).toBe("number");
177
+ }
178
+ });
179
+
180
+ it("longitude is a valid number", () => {
181
+ for (const v of samples("longitude", "number")) {
182
+ expect(typeof v).toBe("number");
183
+ }
184
+ });
185
+ });
186
+
187
+ describe("Internet", () => {
188
+ it("url starts with http", () => {
189
+ for (const v of samples("url", "string")) {
190
+ expect(v).toMatch(/^https?:\/\//);
191
+ }
192
+ });
193
+
194
+ it("ip_address looks like an IP", () => {
195
+ for (const v of samples("ip_address", "string")) {
196
+ expect(v).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
197
+ }
198
+ });
199
+ });
200
+
201
+ describe("IDs", () => {
202
+ it("uuid matches UUID pattern", () => {
203
+ for (const v of samples("uuid", "string")) {
204
+ expect(v).toMatch(
205
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
206
+ );
207
+ }
208
+ });
209
+
210
+ it("userId (suffix rule) generates UUID", () => {
211
+ for (const v of samples("userId", "string")) {
212
+ expect(v).toMatch(
213
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
214
+ );
215
+ }
216
+ });
217
+
218
+ it("order_id (suffix rule) generates UUID", () => {
219
+ for (const v of samples("order_id", "string")) {
220
+ expect(v).toMatch(
221
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
222
+ );
223
+ }
224
+ });
225
+ });
226
+
227
+ describe("Text/Content", () => {
228
+ it("description is a paragraph (> 20 chars)", () => {
229
+ for (const v of samples("description", "string")) {
230
+ expect((v as string).length).toBeGreaterThan(20);
231
+ }
232
+ });
233
+
234
+ it("title is sentence-length", () => {
235
+ for (const v of samples("title", "string")) {
236
+ expect((v as string).length).toBeGreaterThan(5);
237
+ }
238
+ });
239
+ });
240
+
241
+ describe("Business", () => {
242
+ it("company generates a non-empty string", () => {
243
+ for (const v of samples("company", "string")) {
244
+ expect((v as string).length).toBeGreaterThan(0);
245
+ }
246
+ });
247
+ });
248
+
249
+ describe("Date/Time", () => {
250
+ it("createdAt generates parseable dates", () => {
251
+ for (const v of samples("createdAt", "string")) {
252
+ expect(Date.parse(v as string)).not.toBeNaN();
253
+ }
254
+ });
255
+
256
+ it("updatedAt generates parseable dates", () => {
257
+ for (const v of samples("updatedAt", "string")) {
258
+ expect(Date.parse(v as string)).not.toBeNaN();
259
+ }
260
+ });
261
+
262
+ it("expires_at generates parseable dates", () => {
263
+ for (const v of samples("expires_at", "string")) {
264
+ expect(Date.parse(v as string)).not.toBeNaN();
265
+ }
266
+ });
267
+ });
268
+
269
+ describe("Boolean weighting", () => {
270
+ it("isActive is mostly true (~90%)", () => {
271
+ const values = samples("isActive", "boolean", 200) as boolean[];
272
+ const trueCount = values.filter(Boolean).length;
273
+ // With p=0.9 and n=200, expect 160-200 trues
274
+ expect(trueCount).toBeGreaterThan(140);
275
+ });
276
+
277
+ it("isDeleted is mostly false (~5% true)", () => {
278
+ const values = samples("isDeleted", "boolean", 200) as boolean[];
279
+ const trueCount = values.filter(Boolean).length;
280
+ // With p=0.05 and n=200, expect 0-30 trues
281
+ expect(trueCount).toBeLessThan(40);
282
+ });
283
+
284
+ it("isVerified is mostly true (~80%)", () => {
285
+ const values = samples("isVerified", "boolean", 200) as boolean[];
286
+ const trueCount = values.filter(Boolean).length;
287
+ expect(trueCount).toBeGreaterThan(120);
288
+ });
289
+ });
290
+
291
+ describe("Nullable post-processing", () => {
292
+ it("schmockNullable fields are null ~5% of the time", () => {
293
+ const schema = {
294
+ type: "object" as const,
295
+ properties: {
296
+ value: {
297
+ type: "string" as const,
298
+ schmockNullable: true,
299
+ } as any,
300
+ },
301
+ };
302
+
303
+ let nullCount = 0;
304
+ const total = 500;
305
+ for (let i = 0; i < total; i++) {
306
+ const result = generateFromSchema({ schema });
307
+ if (result.value === null) nullCount++;
308
+ }
309
+
310
+ // With p=0.05 and n=500, expect 10-45 nulls (generous bounds)
311
+ expect(nullCount).toBeGreaterThan(5);
312
+ expect(nullCount).toBeLessThan(60);
313
+ });
314
+ });
315
+
316
+ describe("Composition recursion", () => {
317
+ it("enhances fields inside allOf branches", () => {
318
+ const schema = {
319
+ type: "object" as const,
320
+ properties: {
321
+ data: {
322
+ allOf: [
323
+ {
324
+ type: "object" as const,
325
+ properties: {
326
+ email: { type: "string" as const },
327
+ city: { type: "string" as const },
328
+ },
329
+ },
330
+ ],
331
+ },
332
+ },
333
+ };
334
+
335
+ const result = generateFromSchema({ schema });
336
+ expect(result.data.email).toMatch(/@/);
337
+ expect(result.data.city).toMatch(/^[A-Z]/);
338
+ });
339
+ });
340
+ });