@schmock/faker 1.10.0 → 1.13.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/dist/field-name-matcher.d.ts.map +1 -1
- package/dist/field-name-matcher.js +9 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/jsf-config.d.ts +8 -5
- package/dist/jsf-config.d.ts.map +1 -1
- package/dist/jsf-config.js +19 -42
- package/dist/schema-enhancement.js +4 -0
- package/dist/test-utils.d.ts +5 -5
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +25 -13
- package/package.json +7 -7
- package/src/advanced-features.test.ts +72 -71
- package/src/data-quality.test.ts +38 -38
- package/src/error-handling.test.ts +54 -56
- package/src/field-mappings.test.ts +74 -63
- package/src/field-name-matcher.ts +12 -2
- package/src/index.test.ts +145 -135
- package/src/index.ts +8 -6
- package/src/integration.test.ts +30 -28
- package/src/jsf-config.ts +21 -37
- package/src/performance.test.ts +45 -43
- package/src/plugin-integration.test.ts +46 -46
- package/src/real-world.test.ts +22 -22
- package/src/schema-enhancement.ts +5 -0
- package/src/steps/deterministic-seeds.steps.ts +24 -21
- package/src/steps/faker-plugin.steps.ts +190 -142
- package/src/test-utils.ts +38 -22
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { ALL_FIELD_MAPPINGS
|
|
2
|
+
import { ALL_FIELD_MAPPINGS } from "./field-mappings";
|
|
3
3
|
import { findBestMapping } from "./field-name-matcher";
|
|
4
4
|
import { generateFromSchema } from "./index";
|
|
5
5
|
|
|
@@ -7,14 +7,17 @@ import { generateFromSchema } from "./index";
|
|
|
7
7
|
* Generate a value for a field by placing it inside an object schema
|
|
8
8
|
* and calling generateFromSchema. Returns the generated value.
|
|
9
9
|
*/
|
|
10
|
-
function generateField(
|
|
10
|
+
async function generateField(
|
|
11
|
+
fieldName: string,
|
|
12
|
+
schemaType: string,
|
|
13
|
+
): Promise<unknown> {
|
|
11
14
|
const schema = {
|
|
12
15
|
type: "object" as const,
|
|
13
16
|
properties: {
|
|
14
17
|
[fieldName]: { type: schemaType as "string" | "number" | "boolean" },
|
|
15
18
|
},
|
|
16
19
|
};
|
|
17
|
-
const result = generateFromSchema({ schema });
|
|
20
|
+
const result = await generateFromSchema({ schema });
|
|
18
21
|
return result[fieldName];
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -53,8 +56,8 @@ describe("Field mapping generation", () => {
|
|
|
53
56
|
for (const mapping of stringMappings) {
|
|
54
57
|
const keyword = mapping.keywords[0];
|
|
55
58
|
|
|
56
|
-
it(`"${keyword}" generates a non-empty string`, () => {
|
|
57
|
-
const value = generateField(keyword, "string");
|
|
59
|
+
it(`"${keyword}" generates a non-empty string`, async () => {
|
|
60
|
+
const value = await generateField(keyword, "string");
|
|
58
61
|
expect(typeof value).toBe("string");
|
|
59
62
|
expect((value as string).length).toBeGreaterThan(0);
|
|
60
63
|
});
|
|
@@ -65,8 +68,8 @@ describe("Field mapping generation", () => {
|
|
|
65
68
|
for (const mapping of numberMappings) {
|
|
66
69
|
const keyword = mapping.keywords[0];
|
|
67
70
|
|
|
68
|
-
it(`"${keyword}" generates a number`, () => {
|
|
69
|
-
const value = generateField(keyword, "number");
|
|
71
|
+
it(`"${keyword}" generates a number`, async () => {
|
|
72
|
+
const value = await generateField(keyword, "number");
|
|
70
73
|
expect(typeof value).toBe("number");
|
|
71
74
|
expect(Number.isFinite(value as number)).toBe(true);
|
|
72
75
|
});
|
|
@@ -77,8 +80,8 @@ describe("Field mapping generation", () => {
|
|
|
77
80
|
for (const mapping of booleanMappings) {
|
|
78
81
|
const keyword = mapping.keywords[0];
|
|
79
82
|
|
|
80
|
-
it(`"${keyword}" generates a boolean`, () => {
|
|
81
|
-
const value = generateField(keyword, "boolean");
|
|
83
|
+
it(`"${keyword}" generates a boolean`, async () => {
|
|
84
|
+
const value = await generateField(keyword, "boolean");
|
|
82
85
|
expect(typeof value).toBe("boolean");
|
|
83
86
|
});
|
|
84
87
|
}
|
|
@@ -111,19 +114,27 @@ describe("All keyword variants match", () => {
|
|
|
111
114
|
// ─── Category-specific quality checks ──────────────────────────────
|
|
112
115
|
|
|
113
116
|
describe("Category quality checks", () => {
|
|
114
|
-
function samples(
|
|
115
|
-
|
|
117
|
+
async function samples(
|
|
118
|
+
fieldName: string,
|
|
119
|
+
type: string,
|
|
120
|
+
count = 10,
|
|
121
|
+
): Promise<unknown[]> {
|
|
122
|
+
const results: unknown[] = [];
|
|
123
|
+
for (let i = 0; i < count; i++) {
|
|
124
|
+
results.push(await generateField(fieldName, type));
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
describe("Identity & Auth", () => {
|
|
119
|
-
it("email contains @ and .", () => {
|
|
120
|
-
for (const v of samples("email", "string")) {
|
|
130
|
+
it("email contains @ and .", async () => {
|
|
131
|
+
for (const v of await samples("email", "string")) {
|
|
121
132
|
expect(v).toMatch(/@.*\./);
|
|
122
133
|
}
|
|
123
134
|
});
|
|
124
135
|
|
|
125
|
-
it("username is a non-empty string", () => {
|
|
126
|
-
for (const v of samples("username", "string")) {
|
|
136
|
+
it("username is a non-empty string", async () => {
|
|
137
|
+
for (const v of await samples("username", "string")) {
|
|
127
138
|
expect(typeof v).toBe("string");
|
|
128
139
|
expect((v as string).length).toBeGreaterThan(0);
|
|
129
140
|
}
|
|
@@ -131,92 +142,92 @@ describe("Category quality checks", () => {
|
|
|
131
142
|
});
|
|
132
143
|
|
|
133
144
|
describe("Person", () => {
|
|
134
|
-
it("firstName starts with uppercase", () => {
|
|
135
|
-
for (const v of samples("firstName", "string")) {
|
|
145
|
+
it("firstName starts with uppercase", async () => {
|
|
146
|
+
for (const v of await samples("firstName", "string")) {
|
|
136
147
|
expect(v).toMatch(/^[A-Z]/);
|
|
137
148
|
}
|
|
138
149
|
});
|
|
139
150
|
|
|
140
|
-
it("lastName starts with uppercase", () => {
|
|
141
|
-
for (const v of samples("lastName", "string")) {
|
|
151
|
+
it("lastName starts with uppercase", async () => {
|
|
152
|
+
for (const v of await samples("lastName", "string")) {
|
|
142
153
|
expect(v).toMatch(/^[A-Z]/);
|
|
143
154
|
}
|
|
144
155
|
});
|
|
145
156
|
|
|
146
|
-
it("name (fullName) contains a space", () => {
|
|
147
|
-
for (const v of samples("name", "string")) {
|
|
157
|
+
it("name (fullName) contains a space", async () => {
|
|
158
|
+
for (const v of await samples("name", "string")) {
|
|
148
159
|
expect(v).toContain(" ");
|
|
149
160
|
}
|
|
150
161
|
});
|
|
151
162
|
});
|
|
152
163
|
|
|
153
164
|
describe("Contact", () => {
|
|
154
|
-
it("phone contains digits", () => {
|
|
155
|
-
for (const v of samples("phone", "string")) {
|
|
165
|
+
it("phone contains digits", async () => {
|
|
166
|
+
for (const v of await samples("phone", "string")) {
|
|
156
167
|
expect(v).toMatch(/\d/);
|
|
157
168
|
}
|
|
158
169
|
});
|
|
159
170
|
});
|
|
160
171
|
|
|
161
172
|
describe("Address", () => {
|
|
162
|
-
it("city is a capitalized string", () => {
|
|
163
|
-
for (const v of samples("city", "string")) {
|
|
173
|
+
it("city is a capitalized string", async () => {
|
|
174
|
+
for (const v of await samples("city", "string")) {
|
|
164
175
|
expect(v).toMatch(/^[A-Z]/);
|
|
165
176
|
}
|
|
166
177
|
});
|
|
167
178
|
|
|
168
|
-
it("zipcode matches US format", () => {
|
|
169
|
-
for (const v of samples("zipcode", "string")) {
|
|
179
|
+
it("zipcode matches US format", async () => {
|
|
180
|
+
for (const v of await samples("zipcode", "string")) {
|
|
170
181
|
expect(v).toMatch(/^\d{5}/);
|
|
171
182
|
}
|
|
172
183
|
});
|
|
173
184
|
|
|
174
|
-
it("latitude is a valid number", () => {
|
|
175
|
-
for (const v of samples("latitude", "number")) {
|
|
185
|
+
it("latitude is a valid number", async () => {
|
|
186
|
+
for (const v of await samples("latitude", "number")) {
|
|
176
187
|
expect(typeof v).toBe("number");
|
|
177
188
|
}
|
|
178
189
|
});
|
|
179
190
|
|
|
180
|
-
it("longitude is a valid number", () => {
|
|
181
|
-
for (const v of samples("longitude", "number")) {
|
|
191
|
+
it("longitude is a valid number", async () => {
|
|
192
|
+
for (const v of await samples("longitude", "number")) {
|
|
182
193
|
expect(typeof v).toBe("number");
|
|
183
194
|
}
|
|
184
195
|
});
|
|
185
196
|
});
|
|
186
197
|
|
|
187
198
|
describe("Internet", () => {
|
|
188
|
-
it("url starts with http", () => {
|
|
189
|
-
for (const v of samples("url", "string")) {
|
|
199
|
+
it("url starts with http", async () => {
|
|
200
|
+
for (const v of await samples("url", "string")) {
|
|
190
201
|
expect(v).toMatch(/^https?:\/\//);
|
|
191
202
|
}
|
|
192
203
|
});
|
|
193
204
|
|
|
194
|
-
it("ip_address looks like an IP", () => {
|
|
195
|
-
for (const v of samples("ip_address", "string")) {
|
|
205
|
+
it("ip_address looks like an IP", async () => {
|
|
206
|
+
for (const v of await samples("ip_address", "string")) {
|
|
196
207
|
expect(v).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
|
|
197
208
|
}
|
|
198
209
|
});
|
|
199
210
|
});
|
|
200
211
|
|
|
201
212
|
describe("IDs", () => {
|
|
202
|
-
it("uuid matches UUID pattern", () => {
|
|
203
|
-
for (const v of samples("uuid", "string")) {
|
|
213
|
+
it("uuid matches UUID pattern", async () => {
|
|
214
|
+
for (const v of await samples("uuid", "string")) {
|
|
204
215
|
expect(v).toMatch(
|
|
205
216
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
206
217
|
);
|
|
207
218
|
}
|
|
208
219
|
});
|
|
209
220
|
|
|
210
|
-
it("userId (suffix rule) generates UUID", () => {
|
|
211
|
-
for (const v of samples("userId", "string")) {
|
|
221
|
+
it("userId (suffix rule) generates UUID", async () => {
|
|
222
|
+
for (const v of await samples("userId", "string")) {
|
|
212
223
|
expect(v).toMatch(
|
|
213
224
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
214
225
|
);
|
|
215
226
|
}
|
|
216
227
|
});
|
|
217
228
|
|
|
218
|
-
it("order_id (suffix rule) generates UUID", () => {
|
|
219
|
-
for (const v of samples("order_id", "string")) {
|
|
229
|
+
it("order_id (suffix rule) generates UUID", async () => {
|
|
230
|
+
for (const v of await samples("order_id", "string")) {
|
|
220
231
|
expect(v).toMatch(
|
|
221
232
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
222
233
|
);
|
|
@@ -225,71 +236,71 @@ describe("Category quality checks", () => {
|
|
|
225
236
|
});
|
|
226
237
|
|
|
227
238
|
describe("Text/Content", () => {
|
|
228
|
-
it("description is a paragraph (> 20 chars)", () => {
|
|
229
|
-
for (const v of samples("description", "string")) {
|
|
239
|
+
it("description is a paragraph (> 20 chars)", async () => {
|
|
240
|
+
for (const v of await samples("description", "string")) {
|
|
230
241
|
expect((v as string).length).toBeGreaterThan(20);
|
|
231
242
|
}
|
|
232
243
|
});
|
|
233
244
|
|
|
234
|
-
it("title is sentence-length", () => {
|
|
235
|
-
for (const v of samples("title", "string")) {
|
|
245
|
+
it("title is sentence-length", async () => {
|
|
246
|
+
for (const v of await samples("title", "string")) {
|
|
236
247
|
expect((v as string).length).toBeGreaterThan(5);
|
|
237
248
|
}
|
|
238
249
|
});
|
|
239
250
|
});
|
|
240
251
|
|
|
241
252
|
describe("Business", () => {
|
|
242
|
-
it("company generates a non-empty string", () => {
|
|
243
|
-
for (const v of samples("company", "string")) {
|
|
253
|
+
it("company generates a non-empty string", async () => {
|
|
254
|
+
for (const v of await samples("company", "string")) {
|
|
244
255
|
expect((v as string).length).toBeGreaterThan(0);
|
|
245
256
|
}
|
|
246
257
|
});
|
|
247
258
|
});
|
|
248
259
|
|
|
249
260
|
describe("Date/Time", () => {
|
|
250
|
-
it("createdAt generates parseable dates", () => {
|
|
251
|
-
for (const v of samples("createdAt", "string")) {
|
|
261
|
+
it("createdAt generates parseable dates", async () => {
|
|
262
|
+
for (const v of await samples("createdAt", "string")) {
|
|
252
263
|
expect(Date.parse(v as string)).not.toBeNaN();
|
|
253
264
|
}
|
|
254
265
|
});
|
|
255
266
|
|
|
256
|
-
it("updatedAt generates parseable dates", () => {
|
|
257
|
-
for (const v of samples("updatedAt", "string")) {
|
|
267
|
+
it("updatedAt generates parseable dates", async () => {
|
|
268
|
+
for (const v of await samples("updatedAt", "string")) {
|
|
258
269
|
expect(Date.parse(v as string)).not.toBeNaN();
|
|
259
270
|
}
|
|
260
271
|
});
|
|
261
272
|
|
|
262
|
-
it("expires_at generates parseable dates", () => {
|
|
263
|
-
for (const v of samples("expires_at", "string")) {
|
|
273
|
+
it("expires_at generates parseable dates", async () => {
|
|
274
|
+
for (const v of await samples("expires_at", "string")) {
|
|
264
275
|
expect(Date.parse(v as string)).not.toBeNaN();
|
|
265
276
|
}
|
|
266
277
|
});
|
|
267
278
|
});
|
|
268
279
|
|
|
269
280
|
describe("Boolean weighting", () => {
|
|
270
|
-
it("isActive is mostly true (~90%)", () => {
|
|
271
|
-
const values = samples("isActive", "boolean", 200) as boolean[];
|
|
281
|
+
it("isActive is mostly true (~90%)", async () => {
|
|
282
|
+
const values = (await samples("isActive", "boolean", 200)) as boolean[];
|
|
272
283
|
const trueCount = values.filter(Boolean).length;
|
|
273
284
|
// With p=0.9 and n=200, expect 160-200 trues
|
|
274
285
|
expect(trueCount).toBeGreaterThan(140);
|
|
275
286
|
});
|
|
276
287
|
|
|
277
|
-
it("isDeleted is mostly false (~5% true)", () => {
|
|
278
|
-
const values = samples("isDeleted", "boolean", 200) as boolean[];
|
|
288
|
+
it("isDeleted is mostly false (~5% true)", async () => {
|
|
289
|
+
const values = (await samples("isDeleted", "boolean", 200)) as boolean[];
|
|
279
290
|
const trueCount = values.filter(Boolean).length;
|
|
280
291
|
// With p=0.05 and n=200, expect 0-30 trues
|
|
281
292
|
expect(trueCount).toBeLessThan(40);
|
|
282
293
|
});
|
|
283
294
|
|
|
284
|
-
it("isVerified is mostly true (~80%)", () => {
|
|
285
|
-
const values = samples("isVerified", "boolean", 200) as boolean[];
|
|
295
|
+
it("isVerified is mostly true (~80%)", async () => {
|
|
296
|
+
const values = (await samples("isVerified", "boolean", 200)) as boolean[];
|
|
286
297
|
const trueCount = values.filter(Boolean).length;
|
|
287
298
|
expect(trueCount).toBeGreaterThan(120);
|
|
288
299
|
});
|
|
289
300
|
});
|
|
290
301
|
|
|
291
302
|
describe("Nullable post-processing", () => {
|
|
292
|
-
it("schmockNullable fields are null ~5% of the time", () => {
|
|
303
|
+
it("schmockNullable fields are null ~5% of the time", async () => {
|
|
293
304
|
const schema = {
|
|
294
305
|
type: "object" as const,
|
|
295
306
|
properties: {
|
|
@@ -303,7 +314,7 @@ describe("Category quality checks", () => {
|
|
|
303
314
|
let nullCount = 0;
|
|
304
315
|
const total = 500;
|
|
305
316
|
for (let i = 0; i < total; i++) {
|
|
306
|
-
const result = generateFromSchema({ schema });
|
|
317
|
+
const result = await generateFromSchema({ schema });
|
|
307
318
|
if (result.value === null) nullCount++;
|
|
308
319
|
}
|
|
309
320
|
|
|
@@ -314,7 +325,7 @@ describe("Category quality checks", () => {
|
|
|
314
325
|
});
|
|
315
326
|
|
|
316
327
|
describe("Composition recursion", () => {
|
|
317
|
-
it("enhances fields inside allOf branches", () => {
|
|
328
|
+
it("enhances fields inside allOf branches", async () => {
|
|
318
329
|
const schema = {
|
|
319
330
|
type: "object" as const,
|
|
320
331
|
properties: {
|
|
@@ -332,7 +343,7 @@ describe("Category quality checks", () => {
|
|
|
332
343
|
},
|
|
333
344
|
};
|
|
334
345
|
|
|
335
|
-
const result = generateFromSchema({ schema });
|
|
346
|
+
const result = await generateFromSchema({ schema });
|
|
336
347
|
expect(result.data.email).toMatch(/@/);
|
|
337
348
|
expect(result.data.city).toMatch(/^[A-Z]/);
|
|
338
349
|
});
|
|
@@ -109,8 +109,13 @@ export function findBestMapping(
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// Skip if schema already has pattern, enum, or
|
|
113
|
-
if (
|
|
112
|
+
// Skip if schema already has pattern, enum, faker, or $ref constraint
|
|
113
|
+
if (
|
|
114
|
+
schemaAny.pattern ||
|
|
115
|
+
schemaAny.enum ||
|
|
116
|
+
schemaAny.faker ||
|
|
117
|
+
schemaAny.$ref
|
|
118
|
+
) {
|
|
114
119
|
return undefined;
|
|
115
120
|
}
|
|
116
121
|
|
|
@@ -134,6 +139,11 @@ export function findBestMapping(
|
|
|
134
139
|
continue;
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
// Skip faker mappings for object/array schemas — they generate primitive values
|
|
143
|
+
if (schemaType === "object" || schemaType === "array") {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
137
147
|
// Check schema type constraint
|
|
138
148
|
if (mapping.schemaType && schemaType && mapping.schemaType !== schemaType) {
|
|
139
149
|
const isNumeric =
|