@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.
@@ -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
+ }
@@ -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
+ }