@schmock/faker 1.9.0 → 1.9.2

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.
@@ -0,0 +1,58 @@
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
+ } else {
55
+ jsf.option({ random: Math.random });
56
+ }
57
+ return jsf;
58
+ }
@@ -0,0 +1,185 @@
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
+ /**
7
+ * Determine number of items to generate for array schema
8
+ * Prefers explicit count, then schema minItems/maxItems, with sane defaults
9
+ * @param schema - Array schema with optional minItems/maxItems
10
+ * @param explicitCount - Explicit count override from plugin options
11
+ * @returns Number of array items to generate
12
+ */
13
+ export function determineArrayCount(
14
+ schema: JSONSchema7,
15
+ explicitCount?: number,
16
+ random: () => number = Math.random,
17
+ ): number {
18
+ if (explicitCount !== undefined) {
19
+ // Handle negative or invalid counts
20
+ if (explicitCount < 0) {
21
+ return 0;
22
+ }
23
+ return explicitCount;
24
+ }
25
+
26
+ if (schema.minItems !== undefined && schema.maxItems !== undefined) {
27
+ return (
28
+ Math.floor(random() * (schema.maxItems - schema.minItems + 1)) +
29
+ schema.minItems
30
+ );
31
+ }
32
+
33
+ if (schema.minItems !== undefined) {
34
+ return Math.max(schema.minItems, DEFAULT_ARRAY_COUNT);
35
+ }
36
+
37
+ if (schema.maxItems !== undefined) {
38
+ return Math.min(schema.maxItems, DEFAULT_ARRAY_COUNT);
39
+ }
40
+
41
+ return DEFAULT_ARRAY_COUNT;
42
+ }
43
+
44
+ /**
45
+ * Apply overrides to generated data with support for templates
46
+ * Supports nested paths (dot notation), templates with {{params.id}}, and state access
47
+ * @param data - Generated data to apply overrides to
48
+ * @param overrides - Override values (can use templates)
49
+ * @param params - Route parameters for template expansion
50
+ * @param state - Plugin state for template expansion
51
+ * @param query - Query parameters for template expansion
52
+ * @returns Data with overrides applied
53
+ */
54
+ export function applyOverrides(
55
+ data: any,
56
+ overrides?: Record<string, any>,
57
+ params?: Record<string, string>,
58
+ state?: any,
59
+ query?: Record<string, string>,
60
+ ): any {
61
+ if (!overrides) return data;
62
+
63
+ const result = structuredClone(data);
64
+
65
+ for (const [key, value] of Object.entries(overrides)) {
66
+ if (DANGEROUS_KEYS.has(key)) continue;
67
+ // Handle nested paths like "data.id" or "pagination.page"
68
+ if (key.includes(".")) {
69
+ setNestedProperty(result, key, value, { params, state, query });
70
+ } else {
71
+ // Handle flat keys and nested objects
72
+ if (
73
+ typeof value === "object" &&
74
+ value !== null &&
75
+ !Array.isArray(value)
76
+ ) {
77
+ // Recursively apply nested overrides
78
+ if (result[key] && typeof result[key] === "object") {
79
+ result[key] = applyOverrides(
80
+ result[key],
81
+ value,
82
+ params,
83
+ state,
84
+ query,
85
+ );
86
+ } else {
87
+ result[key] = applyOverrides({}, value, params, state, query);
88
+ }
89
+ } else if (typeof value === "string" && value.includes("{{")) {
90
+ // Template processing
91
+ result[key] = processTemplate(value, { params, state, query });
92
+ } else {
93
+ result[key] = value;
94
+ }
95
+ }
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ function setNestedProperty(
102
+ obj: any,
103
+ path: string,
104
+ value: any,
105
+ context: {
106
+ params?: Record<string, string>;
107
+ state?: any;
108
+ query?: Record<string, string>;
109
+ },
110
+ ): void {
111
+ const parts = path.split(".");
112
+ let current = 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
+ if (
119
+ !(part in current) ||
120
+ typeof current[part] !== "object" ||
121
+ current[part] === null
122
+ ) {
123
+ current[part] = {};
124
+ }
125
+ current = current[part];
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
+ function processTemplate(
139
+ template: string,
140
+ context: {
141
+ params?: Record<string, string>;
142
+ state?: any;
143
+ query?: Record<string, string>;
144
+ },
145
+ ): any {
146
+ // Check if the template is just a single template expression
147
+ const singleTemplateMatch = template.match(/^\{\{\s*([^}]+)\s*\}\}$/);
148
+ if (singleTemplateMatch) {
149
+ // For single templates, return the actual value without string conversion
150
+ const expression = singleTemplateMatch[1];
151
+ const parts = expression.trim().split(".");
152
+ let result: any = context;
153
+
154
+ for (const part of parts) {
155
+ if (result && typeof result === "object") {
156
+ result = result[part];
157
+ } else {
158
+ return template; // Return original if can't resolve
159
+ }
160
+ }
161
+
162
+ return result !== undefined ? result : template;
163
+ }
164
+
165
+ // For templates mixed with other text, do string replacement
166
+ const processed = template.replace(
167
+ /\{\{\s*([^}]+)\s*\}\}/g,
168
+ (match, expression) => {
169
+ const parts = expression.trim().split(".");
170
+ let result: any = context;
171
+
172
+ for (const part of parts) {
173
+ if (result && typeof result === "object") {
174
+ result = result[part];
175
+ } else {
176
+ return match; // Return original if can't resolve
177
+ }
178
+ }
179
+
180
+ return result !== undefined ? String(result) : match;
181
+ },
182
+ );
183
+
184
+ return processed;
185
+ }
@@ -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
+ }