@schmock/faker 1.9.0 → 1.9.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/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -73
- package/dist/jsf-config.d.ts +10 -0
- package/dist/jsf-config.d.ts.map +1 -0
- package/dist/jsf-config.js +52 -0
- package/dist/overrides.d.ts +21 -0
- package/dist/overrides.d.ts.map +1 -0
- package/dist/overrides.js +127 -0
- package/dist/schema-enhancement.d.ts +3 -0
- package/dist/schema-enhancement.d.ts.map +1 -0
- package/dist/schema-enhancement.js +81 -0
- package/dist/validation.d.ts +20 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +298 -0
- package/package.json +2 -2
- package/src/constants.ts +6 -0
- package/src/index.ts +5 -747
- package/src/jsf-config.ts +56 -0
- package/src/overrides.ts +179 -0
- package/src/schema-enhancement.ts +106 -0
- package/src/validation.ts +415 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { en, Faker } from "@faker-js/faker";
|
|
2
|
+
import jsf from "json-schema-faker";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create isolated faker instance to avoid race conditions
|
|
6
|
+
* Each generation gets its own faker instance to ensure thread-safety
|
|
7
|
+
* @returns Fresh Faker instance with English locale
|
|
8
|
+
*/
|
|
9
|
+
export function createFakerInstance(seed?: number) {
|
|
10
|
+
const faker = new Faker({ locale: [en] });
|
|
11
|
+
if (seed !== undefined) {
|
|
12
|
+
faker.seed(seed);
|
|
13
|
+
}
|
|
14
|
+
return faker;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let jsfConfigured = false;
|
|
18
|
+
let currentSeed: number | undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a seeded PRNG using the mulberry32 algorithm.
|
|
22
|
+
* Returns a function that produces deterministic values in [0, 1).
|
|
23
|
+
*/
|
|
24
|
+
function createSeededRandom(seed: number): () => number {
|
|
25
|
+
let state = seed | 0;
|
|
26
|
+
return () => {
|
|
27
|
+
state = (state + 0x6d2b79f5) | 0;
|
|
28
|
+
let t = state;
|
|
29
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
30
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
31
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getJsf(seed?: number): typeof jsf {
|
|
36
|
+
const seedChanged = seed !== currentSeed;
|
|
37
|
+
if (!jsfConfigured || seedChanged) {
|
|
38
|
+
currentSeed = seed;
|
|
39
|
+
jsf.extend("faker", () => createFakerInstance(seed));
|
|
40
|
+
jsf.option({
|
|
41
|
+
requiredOnly: false,
|
|
42
|
+
alwaysFakeOptionals: true,
|
|
43
|
+
useDefaultValue: true,
|
|
44
|
+
ignoreMissingRefs: true,
|
|
45
|
+
failOnInvalidTypes: false,
|
|
46
|
+
failOnInvalidFormat: false,
|
|
47
|
+
});
|
|
48
|
+
jsfConfigured = true;
|
|
49
|
+
}
|
|
50
|
+
// Always reset PRNG for deterministic output per call
|
|
51
|
+
if (seed !== undefined) {
|
|
52
|
+
jsf.option({ random: createSeededRandom(seed) });
|
|
53
|
+
jsf.extend("faker", () => createFakerInstance(seed));
|
|
54
|
+
}
|
|
55
|
+
return jsf;
|
|
56
|
+
}
|
package/src/overrides.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { DEFAULT_ARRAY_COUNT } from "./constants.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Determine number of items to generate for array schema
|
|
6
|
+
* Prefers explicit count, then schema minItems/maxItems, with sane defaults
|
|
7
|
+
* @param schema - Array schema with optional minItems/maxItems
|
|
8
|
+
* @param explicitCount - Explicit count override from plugin options
|
|
9
|
+
* @returns Number of array items to generate
|
|
10
|
+
*/
|
|
11
|
+
export function determineArrayCount(
|
|
12
|
+
schema: JSONSchema7,
|
|
13
|
+
explicitCount?: number,
|
|
14
|
+
): number {
|
|
15
|
+
if (explicitCount !== undefined) {
|
|
16
|
+
// Handle negative or invalid counts
|
|
17
|
+
if (explicitCount < 0) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
return explicitCount;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (schema.minItems !== undefined && schema.maxItems !== undefined) {
|
|
24
|
+
return (
|
|
25
|
+
Math.floor(Math.random() * (schema.maxItems - schema.minItems + 1)) +
|
|
26
|
+
schema.minItems
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (schema.minItems !== undefined) {
|
|
31
|
+
return Math.max(schema.minItems, DEFAULT_ARRAY_COUNT);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (schema.maxItems !== undefined) {
|
|
35
|
+
return Math.min(schema.maxItems, DEFAULT_ARRAY_COUNT);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return DEFAULT_ARRAY_COUNT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Apply overrides to generated data with support for templates
|
|
43
|
+
* Supports nested paths (dot notation), templates with {{params.id}}, and state access
|
|
44
|
+
* @param data - Generated data to apply overrides to
|
|
45
|
+
* @param overrides - Override values (can use templates)
|
|
46
|
+
* @param params - Route parameters for template expansion
|
|
47
|
+
* @param state - Plugin state for template expansion
|
|
48
|
+
* @param query - Query parameters for template expansion
|
|
49
|
+
* @returns Data with overrides applied
|
|
50
|
+
*/
|
|
51
|
+
export function applyOverrides(
|
|
52
|
+
data: any,
|
|
53
|
+
overrides?: Record<string, any>,
|
|
54
|
+
params?: Record<string, string>,
|
|
55
|
+
state?: any,
|
|
56
|
+
query?: Record<string, string>,
|
|
57
|
+
): any {
|
|
58
|
+
if (!overrides) return data;
|
|
59
|
+
|
|
60
|
+
const result = structuredClone(data);
|
|
61
|
+
|
|
62
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
63
|
+
// Handle nested paths like "data.id" or "pagination.page"
|
|
64
|
+
if (key.includes(".")) {
|
|
65
|
+
setNestedProperty(result, key, value, { params, state, query });
|
|
66
|
+
} else {
|
|
67
|
+
// Handle flat keys and nested objects
|
|
68
|
+
if (
|
|
69
|
+
typeof value === "object" &&
|
|
70
|
+
value !== null &&
|
|
71
|
+
!Array.isArray(value)
|
|
72
|
+
) {
|
|
73
|
+
// Recursively apply nested overrides
|
|
74
|
+
if (result[key] && typeof result[key] === "object") {
|
|
75
|
+
result[key] = applyOverrides(
|
|
76
|
+
result[key],
|
|
77
|
+
value,
|
|
78
|
+
params,
|
|
79
|
+
state,
|
|
80
|
+
query,
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
result[key] = applyOverrides({}, value, params, state, query);
|
|
84
|
+
}
|
|
85
|
+
} else if (typeof value === "string" && value.includes("{{")) {
|
|
86
|
+
// Template processing
|
|
87
|
+
result[key] = processTemplate(value, { params, state, query });
|
|
88
|
+
} else {
|
|
89
|
+
result[key] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setNestedProperty(
|
|
98
|
+
obj: any,
|
|
99
|
+
path: string,
|
|
100
|
+
value: any,
|
|
101
|
+
context: {
|
|
102
|
+
params?: Record<string, string>;
|
|
103
|
+
state?: any;
|
|
104
|
+
query?: Record<string, string>;
|
|
105
|
+
},
|
|
106
|
+
): void {
|
|
107
|
+
const parts = path.split(".");
|
|
108
|
+
let current = obj;
|
|
109
|
+
|
|
110
|
+
// Navigate to the parent of the target property
|
|
111
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
112
|
+
const part = parts[i];
|
|
113
|
+
if (
|
|
114
|
+
!(part in current) ||
|
|
115
|
+
typeof current[part] !== "object" ||
|
|
116
|
+
current[part] === null
|
|
117
|
+
) {
|
|
118
|
+
current[part] = {};
|
|
119
|
+
}
|
|
120
|
+
current = current[part];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Set the final property
|
|
124
|
+
const finalKey = parts[parts.length - 1];
|
|
125
|
+
if (typeof value === "string" && value.includes("{{")) {
|
|
126
|
+
current[finalKey] = processTemplate(value, context);
|
|
127
|
+
} else {
|
|
128
|
+
current[finalKey] = value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function processTemplate(
|
|
133
|
+
template: string,
|
|
134
|
+
context: {
|
|
135
|
+
params?: Record<string, string>;
|
|
136
|
+
state?: any;
|
|
137
|
+
query?: Record<string, string>;
|
|
138
|
+
},
|
|
139
|
+
): any {
|
|
140
|
+
// Check if the template is just a single template expression
|
|
141
|
+
const singleTemplateMatch = template.match(/^\{\{\s*([^}]+)\s*\}\}$/);
|
|
142
|
+
if (singleTemplateMatch) {
|
|
143
|
+
// For single templates, return the actual value without string conversion
|
|
144
|
+
const expression = singleTemplateMatch[1];
|
|
145
|
+
const parts = expression.trim().split(".");
|
|
146
|
+
let result: any = context;
|
|
147
|
+
|
|
148
|
+
for (const part of parts) {
|
|
149
|
+
if (result && typeof result === "object") {
|
|
150
|
+
result = result[part];
|
|
151
|
+
} else {
|
|
152
|
+
return template; // Return original if can't resolve
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result !== undefined ? result : template;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// For templates mixed with other text, do string replacement
|
|
160
|
+
const processed = template.replace(
|
|
161
|
+
/\{\{\s*([^}]+)\s*\}\}/g,
|
|
162
|
+
(match, expression) => {
|
|
163
|
+
const parts = expression.trim().split(".");
|
|
164
|
+
let result: any = context;
|
|
165
|
+
|
|
166
|
+
for (const part of parts) {
|
|
167
|
+
if (result && typeof result === "object") {
|
|
168
|
+
result = result[part];
|
|
169
|
+
} else {
|
|
170
|
+
return match; // Return original if can't resolve
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result !== undefined ? String(result) : match;
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return processed;
|
|
179
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { JSONSchema7 } from "json-schema";
|
|
2
|
+
import { isJSONSchema7, validateFakerMethod } from "./validation.js";
|
|
3
|
+
|
|
4
|
+
/** JSONSchema7 extended with json-schema-faker's `faker` property */
|
|
5
|
+
interface FakerSchema extends JSONSchema7 {
|
|
6
|
+
faker?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function enhanceSchemaWithSmartMapping(
|
|
10
|
+
schema: JSONSchema7,
|
|
11
|
+
): JSONSchema7 {
|
|
12
|
+
if (!schema || typeof schema !== "object") {
|
|
13
|
+
return schema;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const enhanced = { ...schema };
|
|
17
|
+
|
|
18
|
+
// Handle object properties
|
|
19
|
+
if (enhanced.type === "object" && enhanced.properties) {
|
|
20
|
+
enhanced.properties = { ...enhanced.properties };
|
|
21
|
+
|
|
22
|
+
for (const [fieldName, fieldSchema] of Object.entries(
|
|
23
|
+
enhanced.properties,
|
|
24
|
+
)) {
|
|
25
|
+
if (isJSONSchema7(fieldSchema)) {
|
|
26
|
+
enhanced.properties[fieldName] = enhanceFieldSchema(
|
|
27
|
+
fieldName,
|
|
28
|
+
fieldSchema,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return enhanced;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function enhanceFieldSchema(
|
|
38
|
+
fieldName: string,
|
|
39
|
+
fieldSchema: JSONSchema7,
|
|
40
|
+
): FakerSchema {
|
|
41
|
+
const enhanced: FakerSchema = { ...fieldSchema };
|
|
42
|
+
|
|
43
|
+
// If already has faker extension, validate it and don't override
|
|
44
|
+
if (enhanced.faker) {
|
|
45
|
+
validateFakerMethod(enhanced.faker);
|
|
46
|
+
return enhanced;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Apply smart field name mapping
|
|
50
|
+
const lowerFieldName = fieldName.toLowerCase();
|
|
51
|
+
|
|
52
|
+
// Email fields
|
|
53
|
+
if (lowerFieldName.includes("email")) {
|
|
54
|
+
enhanced.format = "email";
|
|
55
|
+
enhanced.faker = "internet.email";
|
|
56
|
+
}
|
|
57
|
+
// Name fields
|
|
58
|
+
else if (lowerFieldName === "firstname" || lowerFieldName === "first_name") {
|
|
59
|
+
enhanced.faker = "person.firstName";
|
|
60
|
+
} else if (lowerFieldName === "lastname" || lowerFieldName === "last_name") {
|
|
61
|
+
enhanced.faker = "person.lastName";
|
|
62
|
+
} else if (lowerFieldName === "name" || lowerFieldName === "fullname") {
|
|
63
|
+
enhanced.faker = "person.fullName";
|
|
64
|
+
}
|
|
65
|
+
// Phone fields
|
|
66
|
+
else if (lowerFieldName.includes("phone") || lowerFieldName === "mobile") {
|
|
67
|
+
enhanced.faker = "phone.number";
|
|
68
|
+
}
|
|
69
|
+
// Address fields
|
|
70
|
+
else if (lowerFieldName === "street" || lowerFieldName === "address") {
|
|
71
|
+
enhanced.faker = "location.streetAddress";
|
|
72
|
+
} else if (lowerFieldName === "city") {
|
|
73
|
+
enhanced.faker = "location.city";
|
|
74
|
+
} else if (lowerFieldName === "zipcode" || lowerFieldName === "zip") {
|
|
75
|
+
enhanced.faker = "location.zipCode";
|
|
76
|
+
}
|
|
77
|
+
// UUID fields
|
|
78
|
+
else if (
|
|
79
|
+
lowerFieldName === "uuid" ||
|
|
80
|
+
(lowerFieldName === "id" && enhanced.format === "uuid")
|
|
81
|
+
) {
|
|
82
|
+
enhanced.faker = "string.uuid";
|
|
83
|
+
}
|
|
84
|
+
// Date fields
|
|
85
|
+
else if (
|
|
86
|
+
lowerFieldName.includes("createdat") ||
|
|
87
|
+
lowerFieldName.includes("created_at") ||
|
|
88
|
+
lowerFieldName.includes("updatedat") ||
|
|
89
|
+
lowerFieldName.includes("updated_at")
|
|
90
|
+
) {
|
|
91
|
+
enhanced.format = "date-time";
|
|
92
|
+
enhanced.faker = "date.recent";
|
|
93
|
+
}
|
|
94
|
+
// Company fields
|
|
95
|
+
else if (lowerFieldName.includes("company")) {
|
|
96
|
+
enhanced.faker = "company.name";
|
|
97
|
+
} else if (lowerFieldName === "position" || lowerFieldName === "jobtitle") {
|
|
98
|
+
enhanced.faker = "person.jobTitle";
|
|
99
|
+
}
|
|
100
|
+
// Price/money fields
|
|
101
|
+
else if (lowerFieldName === "price" || lowerFieldName === "amount") {
|
|
102
|
+
enhanced.faker = "commerce.price";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return enhanced;
|
|
106
|
+
}
|