@zero-server/orm 0.9.1 → 0.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.
- package/LICENSE +21 -21
- package/index.js +35 -35
- package/lib/debug.js +372 -0
- package/lib/orm/adapters/json.js +290 -0
- package/lib/orm/adapters/memory.js +764 -0
- package/lib/orm/adapters/mongo.js +764 -0
- package/lib/orm/adapters/mysql.js +933 -0
- package/lib/orm/adapters/postgres.js +1144 -0
- package/lib/orm/adapters/redis.js +1534 -0
- package/lib/orm/adapters/sql-base.js +212 -0
- package/lib/orm/adapters/sqlite.js +858 -0
- package/lib/orm/audit.js +649 -0
- package/lib/orm/cache.js +394 -0
- package/lib/orm/geo.js +387 -0
- package/lib/orm/index.js +784 -0
- package/lib/orm/migrate.js +432 -0
- package/lib/orm/model.js +1706 -0
- package/lib/orm/plugin.js +375 -0
- package/lib/orm/procedures.js +836 -0
- package/lib/orm/profiler.js +233 -0
- package/lib/orm/query.js +1772 -0
- package/lib/orm/replicas.js +241 -0
- package/lib/orm/schema.js +307 -0
- package/lib/orm/search.js +380 -0
- package/lib/orm/seed/data/commerce.js +136 -0
- package/lib/orm/seed/data/internet.js +111 -0
- package/lib/orm/seed/data/locations.js +204 -0
- package/lib/orm/seed/data/names.js +338 -0
- package/lib/orm/seed/data/person.js +128 -0
- package/lib/orm/seed/data/phone.js +211 -0
- package/lib/orm/seed/data/words.js +134 -0
- package/lib/orm/seed/factory.js +178 -0
- package/lib/orm/seed/fake.js +1186 -0
- package/lib/orm/seed/index.js +18 -0
- package/lib/orm/seed/rng.js +71 -0
- package/lib/orm/seed/seeder.js +125 -0
- package/lib/orm/seed/unique.js +68 -0
- package/lib/orm/snapshot.js +366 -0
- package/lib/orm/tenancy.js +605 -0
- package/lib/orm/views.js +350 -0
- package/package.json +11 -2
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module seed/fake
|
|
5
|
+
* @description Extensible fake data generator.
|
|
6
|
+
*
|
|
7
|
+
* Key capabilities:
|
|
8
|
+
* • Seeded / reproducible output — Fake.seed(42)
|
|
9
|
+
* • Guaranteed-unique values — Fake.unique(() => Fake.email())
|
|
10
|
+
* • Multi-locale names — Fake.firstName({ locale: 'ja', sex: 'female' })
|
|
11
|
+
* • Rich phone formats — Fake.phone({ countryCode: 'DE', format: 'international' })
|
|
12
|
+
* • Configurable emails — Fake.email({ provider: 'company.io' })
|
|
13
|
+
* • Flexible usernames — Fake.username({ style: 'underscore' })
|
|
14
|
+
* • Fixed-length numeric strings — Fake.numericString(8)
|
|
15
|
+
* • Person: job titles, bio, zodiac, gender, prefix/suffix
|
|
16
|
+
* • Location: city, country, state, address, lat/lng, timezone
|
|
17
|
+
* • Commerce: product, company, category, price, industry
|
|
18
|
+
* • Internet: password, mac, port, userAgent, domain, ipv6, slug
|
|
19
|
+
* • All original generators retained (100 % backward-compatible)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { rand, seed: _seedFn, getSeed } = require('./rng');
|
|
24
|
+
const { UniqueTracker } = require('./unique');
|
|
25
|
+
|
|
26
|
+
// -- Data modules ------------------------------------------------
|
|
27
|
+
const { NAMES, LOCALES } = require('./data/names');
|
|
28
|
+
const { PHONE_BY_COUNTRY, COUNTRY_CODES } = require('./data/phone');
|
|
29
|
+
const {
|
|
30
|
+
EMAIL_PROVIDERS, SAFE_DOMAINS, TLDS,
|
|
31
|
+
DOMAIN_ADJECTIVES, DOMAIN_NOUNS,
|
|
32
|
+
USERNAME_SEPARATORS, USER_AGENTS, MAC_OUIS,
|
|
33
|
+
CHARSET_LOWERCASE, CHARSET_UPPERCASE, CHARSET_DIGITS, CHARSET_SPECIAL,
|
|
34
|
+
} = require('./data/internet');
|
|
35
|
+
const {
|
|
36
|
+
LOREM_WORDS, HACKER_ADJECTIVES, HACKER_NOUNS, HACKER_VERBS,
|
|
37
|
+
ADJECTIVES, NOUNS,
|
|
38
|
+
} = require('./data/words');
|
|
39
|
+
const {
|
|
40
|
+
NAME_PREFIXES, NAME_SUFFIXES,
|
|
41
|
+
JOB_DESCRIPTORS, JOB_AREAS, JOB_TYPES, JOB_TITLES,
|
|
42
|
+
ZODIAC_SIGNS, GENDERS, BIO_ADJECTIVES, BIO_NOUNS, BIO_PHRASES,
|
|
43
|
+
BLOOD_TYPES,
|
|
44
|
+
} = require('./data/person');
|
|
45
|
+
const {
|
|
46
|
+
CITIES, COUNTRIES, US_STATES,
|
|
47
|
+
STREET_TYPES, STREET_NAMES, ZIP_PATTERNS, TIMEZONES,
|
|
48
|
+
} = require('./data/locations');
|
|
49
|
+
const {
|
|
50
|
+
PRODUCT_ADJECTIVES, PRODUCT_MATERIALS, PRODUCT_NOUNS,
|
|
51
|
+
CATEGORIES, DEPARTMENTS,
|
|
52
|
+
COMPANY_ADJECTIVES, COMPANY_NOUNS, COMPANY_SUFFIXES, INDUSTRIES,
|
|
53
|
+
CATCH_PHRASE_ADJECTIVES, CATCH_PHRASE_NOUNS,
|
|
54
|
+
} = require('./data/commerce');
|
|
55
|
+
|
|
56
|
+
// ================================================================
|
|
57
|
+
// Internal helpers
|
|
58
|
+
// ================================================================
|
|
59
|
+
|
|
60
|
+
/** @private Pick a random element from an array using the active RNG. */
|
|
61
|
+
function _pick(arr) {
|
|
62
|
+
return arr[Math.floor(rand() * arr.length)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @private Pick n unique-indexed elements from an array (no repeats). */
|
|
66
|
+
function _pickMany(arr, n) {
|
|
67
|
+
const copy = arr.slice();
|
|
68
|
+
for (let i = copy.length - 1; i > 0; i--) {
|
|
69
|
+
const j = Math.floor(rand() * (i + 1));
|
|
70
|
+
[copy[i], copy[j]] = [copy[j], copy[i]];
|
|
71
|
+
}
|
|
72
|
+
return copy.slice(0, Math.min(n, arr.length));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @private Random integer in [min, max] inclusive. */
|
|
76
|
+
function _int(min, max) {
|
|
77
|
+
return Math.floor(rand() * (max - min + 1)) + min;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @private
|
|
82
|
+
* Expand a phone / zip template string.
|
|
83
|
+
* # → digit 0–9 N → digit 1–9 A → uppercase letter
|
|
84
|
+
*/
|
|
85
|
+
function _expandTemplate(template) {
|
|
86
|
+
return template.replace(/[#NA]/g, c => {
|
|
87
|
+
if (c === '#') return _int(0, 9);
|
|
88
|
+
if (c === 'N') return _int(1, 9);
|
|
89
|
+
return String.fromCharCode(_int(65, 90)); // c === 'A'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @private Resolve name pool for a locale, falling back to 'en'. */
|
|
94
|
+
function _namePool(locale) {
|
|
95
|
+
return NAMES[locale] || NAMES.en;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @private Derive sex ('male'|'female') from options or random. */
|
|
99
|
+
function _resolveSex(options = {}) {
|
|
100
|
+
const { sex } = options;
|
|
101
|
+
if (sex === 'male' || sex === 'female') return sex;
|
|
102
|
+
return rand() > 0.5 ? 'male' : 'female';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @private UUID v4 fallback for environments without crypto.randomUUID. */
|
|
106
|
+
function _uuid() {
|
|
107
|
+
if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
|
|
108
|
+
const b = crypto.randomBytes(16);
|
|
109
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
110
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
111
|
+
const h = b.toString('hex');
|
|
112
|
+
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ================================================================
|
|
116
|
+
// Fake class
|
|
117
|
+
// ================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Static fake data generator with seeded RNG, locale support, and
|
|
121
|
+
* built-in uniqueness guarantees.
|
|
122
|
+
*
|
|
123
|
+
* All methods are **100 % backward compatible** with the original Fake class.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* // Reproducible output
|
|
127
|
+
* Fake.seed(42);
|
|
128
|
+
* Fake.firstName(); // always the same value
|
|
129
|
+
*
|
|
130
|
+
* // Guaranteed unique emails
|
|
131
|
+
* const emails = Array.from({ length: 200 }, () =>
|
|
132
|
+
* Fake.unique(() => Fake.email())
|
|
133
|
+
* );
|
|
134
|
+
*
|
|
135
|
+
* // Locale-specific names
|
|
136
|
+
* Fake.firstName({ locale: 'ja', sex: 'female' }); // e.g. 'Hina'
|
|
137
|
+
*/
|
|
138
|
+
class Fake
|
|
139
|
+
{
|
|
140
|
+
// -- Seeding & Uniqueness ------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Set a deterministic seed so all subsequent calls produce the same output.
|
|
144
|
+
* Pass `null` or `undefined` to reset to Math.random.
|
|
145
|
+
*
|
|
146
|
+
* @param {number|string|null} [value] - Value to set.
|
|
147
|
+
* @returns {number|null} The resolved numeric seed.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* Fake.seed(42);
|
|
151
|
+
* Fake.firstName(); // reproducible
|
|
152
|
+
* Fake.seed(null); // back to random
|
|
153
|
+
*/
|
|
154
|
+
static seed(value) { return _seedFn(value); }
|
|
155
|
+
|
|
156
|
+
/** @returns {number|null} Active seed, or null if using Math.random. */
|
|
157
|
+
static getSeed() { return getSeed(); }
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generate a guaranteed-unique value within a namespace.
|
|
161
|
+
*
|
|
162
|
+
* @param {() => any} fn - Generator function.
|
|
163
|
+
* @param {object} [options] - Configuration options.
|
|
164
|
+
* @param {string} [options.key] - Namespace key (defaults to function name).
|
|
165
|
+
* @param {number} [options.maxAttempts] - Retry limit (default 1000).
|
|
166
|
+
* @returns {any} The unique generated value.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* const email = Fake.unique(() => Fake.email(), { key: 'email' });
|
|
170
|
+
*/
|
|
171
|
+
static unique(fn, options = {}) {
|
|
172
|
+
const key = options.key || fn.name || 'default';
|
|
173
|
+
return Fake._tracker.generate(key, fn, options.maxAttempts);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reset unique-value tracking.
|
|
178
|
+
* @param {string} [key] - Clear only this namespace, or all if omitted.
|
|
179
|
+
*/
|
|
180
|
+
static resetUnique(key) { Fake._tracker.reset(key); }
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* How many unique values have been generated for a namespace.
|
|
184
|
+
* @param {string} key - Cache or storage key.
|
|
185
|
+
* @returns {number} Count of unique values tracked for the key.
|
|
186
|
+
*/
|
|
187
|
+
static uniqueCount(key) { return Fake._tracker.seen(key); }
|
|
188
|
+
|
|
189
|
+
// -- Names --------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Random first name.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} [options] - Configuration options.
|
|
195
|
+
* @param {'male'|'female'} [options.sex] - Constrain to a sex.
|
|
196
|
+
* @param {string} [options.locale='en'] - Locale code (en, es, fr, de, …).
|
|
197
|
+
* @param {boolean} [options.unique=false] - Guarantee uniqueness per locale+sex.
|
|
198
|
+
* @returns {string} A random first name.
|
|
199
|
+
*/
|
|
200
|
+
static firstName(options = {}) {
|
|
201
|
+
const locale = options.locale || 'en';
|
|
202
|
+
const sex = _resolveSex(options);
|
|
203
|
+
const pool = _namePool(locale);
|
|
204
|
+
const fn = () => _pick(pool[sex]);
|
|
205
|
+
if (options.unique) {
|
|
206
|
+
return Fake.unique(fn, { key: `firstName_${locale}_${sex}` });
|
|
207
|
+
}
|
|
208
|
+
return fn();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Random last name.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} [options] - Configuration options.
|
|
215
|
+
* @param {string} [options.locale='en'] - Locale code (e.g. "en").
|
|
216
|
+
* @param {boolean} [options.unique=false] - Whether to ensure unique values.
|
|
217
|
+
* @returns {string} A random last name.
|
|
218
|
+
*/
|
|
219
|
+
static lastName(options = {}) {
|
|
220
|
+
const locale = options.locale || 'en';
|
|
221
|
+
const pool = _namePool(locale);
|
|
222
|
+
const fn = () => _pick(pool.last);
|
|
223
|
+
if (options.unique) return Fake.unique(fn, { key: `lastName_${locale}` });
|
|
224
|
+
return fn();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Random middle name (falls back to first-name pool if locale lacks one).
|
|
229
|
+
*
|
|
230
|
+
* @param {object} [options] - Configuration options.
|
|
231
|
+
* @param {'male'|'female'} [options.sex] - Biological sex (male/female).
|
|
232
|
+
* @param {string} [options.locale='en'] - Locale code (e.g. "en").
|
|
233
|
+
* @returns {string} A random middle name.
|
|
234
|
+
*/
|
|
235
|
+
static middleName(options = {}) {
|
|
236
|
+
// Reuse first-name pool since most locales share it
|
|
237
|
+
return Fake.firstName({ sex: options.sex, locale: options.locale });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Random full name.
|
|
242
|
+
*
|
|
243
|
+
* @param {object} [options] - Configuration options.
|
|
244
|
+
* @param {'male'|'female'} [options.sex] - Biological sex (male/female).
|
|
245
|
+
* @param {string} [options.locale='en'] - Locale code (e.g. "en").
|
|
246
|
+
* @param {boolean} [options.prefix=false] - Include name prefix (Mr., Dr., …).
|
|
247
|
+
* @param {boolean} [options.middle=false] - Include a middle name.
|
|
248
|
+
* @param {boolean} [options.suffix=false] - Include credential suffix (PhD, Jr., …).
|
|
249
|
+
* @param {string} [options.firstName] - Override the first name.
|
|
250
|
+
* @param {string} [options.lastName] - Override the last name.
|
|
251
|
+
* @param {boolean} [options.unique=false] - Guarantee uniqueness per locale.
|
|
252
|
+
* @returns {string} A full name string.
|
|
253
|
+
*/
|
|
254
|
+
static fullName(options = {}) {
|
|
255
|
+
const locale = options.locale || 'en';
|
|
256
|
+
const sex = _resolveSex(options);
|
|
257
|
+
|
|
258
|
+
const fn = () => {
|
|
259
|
+
const parts = [];
|
|
260
|
+
if (options.prefix) parts.push(Fake.namePrefix({ sex }));
|
|
261
|
+
parts.push(options.firstName || Fake.firstName({ sex, locale }));
|
|
262
|
+
if (options.middle) parts.push(Fake.middleName({ sex, locale }));
|
|
263
|
+
parts.push(options.lastName || Fake.lastName({ locale }));
|
|
264
|
+
if (options.suffix) parts.push(Fake.nameSuffix());
|
|
265
|
+
return parts.join(' ');
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (options.unique) return Fake.unique(fn, { key: `fullName_${locale}` });
|
|
269
|
+
return fn();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Name prefix (Mr., Ms., Dr., Prof., …).
|
|
274
|
+
*
|
|
275
|
+
* @param {object} [options] - Configuration options.
|
|
276
|
+
* @param {'male'|'female'|'neutral'} [options.sex] - Biological sex (male/female).
|
|
277
|
+
* @returns {string} A title prefix like 'Mr.' or 'Dr.'.
|
|
278
|
+
*/
|
|
279
|
+
static namePrefix(options = {}) {
|
|
280
|
+
const sex = options.sex === 'male' ? 'male'
|
|
281
|
+
: options.sex === 'female' ? 'female'
|
|
282
|
+
: options.sex === 'neutral' ? 'neutral'
|
|
283
|
+
: rand() > 0.5 ? 'male'
|
|
284
|
+
: 'female';
|
|
285
|
+
return _pick(NAME_PREFIXES[sex]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Random name suffix (Jr., Sr., PhD, MD, …). */
|
|
289
|
+
static nameSuffix() { return _pick(NAME_SUFFIXES); }
|
|
290
|
+
|
|
291
|
+
/** All supported locale codes. */
|
|
292
|
+
static locales() { return LOCALES.slice(); }
|
|
293
|
+
|
|
294
|
+
// -- Phone Numbers ------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Random phone number.
|
|
298
|
+
*
|
|
299
|
+
* @param {object} [options] - Configuration options.
|
|
300
|
+
* @param {string} [options.countryCode] - ISO 3166-1 alpha-2 (e.g. 'US', 'DE').
|
|
301
|
+
* Defaults to a random country.
|
|
302
|
+
* @param {'human'|'national'|'international'} [options.format='human'] - Output format.
|
|
303
|
+
* @param {boolean} [options.unique=false] - Whether to ensure unique values.
|
|
304
|
+
* @returns {string} Formatted string.
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* Fake.phone(); // '(555) 123-4567'
|
|
308
|
+
* Fake.phone({ countryCode: 'DE' }); // '0174 #######'
|
|
309
|
+
* Fake.phone({ format: 'international' }); // '+1 555 123 4567'
|
|
310
|
+
*/
|
|
311
|
+
static phone(options = {}) {
|
|
312
|
+
const code = (options.countryCode || 'US').toUpperCase();
|
|
313
|
+
const country = PHONE_BY_COUNTRY[code] || PHONE_BY_COUNTRY['US'];
|
|
314
|
+
// Default to 'national' so bare Fake.phone() produces the classic
|
|
315
|
+
// (###) ###-#### format and remains backward compatible.
|
|
316
|
+
const style = options.format || 'national';
|
|
317
|
+
const formats = country.formats[style] || country.formats.national;
|
|
318
|
+
const fn = () => _expandTemplate(_pick(formats));
|
|
319
|
+
if (options.unique) return Fake.unique(fn, { key: `phone_${code}_${style}` });
|
|
320
|
+
return fn();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* All supported phone country codes.
|
|
325
|
+
* @returns {string[]} Array of ISO 3166-1 alpha-2 codes.
|
|
326
|
+
*/
|
|
327
|
+
static phoneCodes() { return COUNTRY_CODES.slice(); }
|
|
328
|
+
|
|
329
|
+
// -- Email --------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Random email address.
|
|
333
|
+
*
|
|
334
|
+
* @param {object} [options] - Configuration options.
|
|
335
|
+
* @param {string} [options.firstName] - Override the local-part first name.
|
|
336
|
+
* @param {string} [options.lastName] - Override the local-part last name.
|
|
337
|
+
* @param {string} [options.provider] - Force a provider domain.
|
|
338
|
+
* @param {boolean} [options.safe=false] - Use only example./test. safe domains.
|
|
339
|
+
* @param {string} [options.locale='en'] - Locale for random name generation.
|
|
340
|
+
* @param {boolean} [options.unique=false] - Whether to ensure unique values.
|
|
341
|
+
* @returns {string} An email address string.
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* Fake.email();
|
|
345
|
+
* Fake.email({ provider: 'company.io', unique: true });
|
|
346
|
+
* Fake.email({ safe: true }); // only example.com/test.com domains
|
|
347
|
+
*/
|
|
348
|
+
static email(options = {}) {
|
|
349
|
+
const locale = options.locale || 'en';
|
|
350
|
+
const fn = () => {
|
|
351
|
+
const first = (options.firstName || Fake.firstName({ locale })).toLowerCase()
|
|
352
|
+
.replace(/[^a-z0-9]/g, '');
|
|
353
|
+
const last = (options.lastName || Fake.lastName({ locale })).toLowerCase()
|
|
354
|
+
.replace(/[^a-z0-9]/g, '');
|
|
355
|
+
const num = _int(1, 999);
|
|
356
|
+
const domain = options.provider
|
|
357
|
+
? options.provider
|
|
358
|
+
: options.safe
|
|
359
|
+
? _pick(SAFE_DOMAINS)
|
|
360
|
+
: _pick(EMAIL_PROVIDERS);
|
|
361
|
+
|
|
362
|
+
const patterns = [
|
|
363
|
+
`${first}.${last}${num}@${domain}`,
|
|
364
|
+
`${first}${last}@${domain}`,
|
|
365
|
+
`${first}_${last}@${domain}`,
|
|
366
|
+
`${first}${num}@${domain}`,
|
|
367
|
+
`${first[0]}${last}@${domain}`,
|
|
368
|
+
];
|
|
369
|
+
return _pick(patterns);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if (options.unique) return Fake.unique(fn, { key: `email_${options.provider || 'any'}` });
|
|
373
|
+
return fn();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// -- Username -----------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Random username.
|
|
380
|
+
*
|
|
381
|
+
* @param {object} [options] - Configuration options.
|
|
382
|
+
* @param {string} [options.firstName] - Override first name component.
|
|
383
|
+
* @param {string} [options.lastName] - Override last name component.
|
|
384
|
+
* @param {'dot'|'underscore'|'none'|'random'} [options.style='random'] - Style variant.
|
|
385
|
+
* @param {boolean} [options.numbers=true] - Append a random number suffix.
|
|
386
|
+
* @param {string} [options.locale='en'] - Locale code (e.g. "en").
|
|
387
|
+
* @param {boolean} [options.unique=false] - Whether to ensure unique values.
|
|
388
|
+
* @returns {string} A username string.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* Fake.username(); // 'alice_smith42'
|
|
392
|
+
* Fake.username({ style: 'dot' }); // 'john.doe91'
|
|
393
|
+
* Fake.username({ style: 'none', numbers: false }); // 'janedoe'
|
|
394
|
+
*/
|
|
395
|
+
static username(options = {}) {
|
|
396
|
+
const locale = options.locale || 'en';
|
|
397
|
+
const style = options.style || 'random';
|
|
398
|
+
const useNums = options.numbers !== false;
|
|
399
|
+
|
|
400
|
+
const fn = () => {
|
|
401
|
+
const first = (options.firstName || Fake.firstName({ locale })).toLowerCase()
|
|
402
|
+
.replace(/[^a-z0-9]/g, '');
|
|
403
|
+
const last = (options.lastName || Fake.lastName({ locale })).toLowerCase()
|
|
404
|
+
.replace(/[^a-z0-9]/g, '');
|
|
405
|
+
|
|
406
|
+
let sep;
|
|
407
|
+
if (style === 'dot') sep = '.';
|
|
408
|
+
else if (style === 'underscore') sep = '_';
|
|
409
|
+
else if (style === 'none') sep = '';
|
|
410
|
+
else sep = _pick(USERNAME_SEPARATORS);
|
|
411
|
+
|
|
412
|
+
const base = `${first}${sep}${last}`;
|
|
413
|
+
const num = useNums ? _int(1, 9999) : '';
|
|
414
|
+
return `${base}${num}`;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
if (options.unique) return Fake.unique(fn, { key: `username_${style}` });
|
|
418
|
+
return fn();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// -- Numbers ------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Random integer in [min, max].
|
|
425
|
+
*
|
|
426
|
+
* @param {number|object} [min=0] - Min bound, or options object.
|
|
427
|
+
* @param {number} [max=100] - Maximum value.
|
|
428
|
+
* @returns {number} A random integer.
|
|
429
|
+
*/
|
|
430
|
+
static integer(min = 0, max = 100) {
|
|
431
|
+
if (typeof min === 'object' && min !== null) {
|
|
432
|
+
const opts = min;
|
|
433
|
+
return _int(opts.min ?? 0, opts.max ?? 100);
|
|
434
|
+
}
|
|
435
|
+
return _int(Math.floor(min), Math.floor(max));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Random float in [min, max].
|
|
440
|
+
*
|
|
441
|
+
* @param {number|object} [min=0] - Minimum value.
|
|
442
|
+
* @param {number} [max=100] - Maximum value.
|
|
443
|
+
* @param {number} [decimals=2] - Decimal places.
|
|
444
|
+
* @returns {number} A random float.
|
|
445
|
+
*/
|
|
446
|
+
static float(min = 0, max = 100, decimals = 2) {
|
|
447
|
+
if (typeof min === 'object' && min !== null) {
|
|
448
|
+
const opts = min;
|
|
449
|
+
return Number((rand() * ((opts.max ?? 100) - (opts.min ?? 0)) + (opts.min ?? 0))
|
|
450
|
+
.toFixed(opts.decimals ?? 2));
|
|
451
|
+
}
|
|
452
|
+
return Number((rand() * (max - min) + min).toFixed(decimals));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Random numeric string with an exact number of digits.
|
|
457
|
+
* Leading zeros are preserved (unlike integer()).
|
|
458
|
+
*
|
|
459
|
+
* @param {number} [length=6] - String length.
|
|
460
|
+
* @param {object} [options] - Configuration options.
|
|
461
|
+
* @param {boolean} [options.leadingZeros=true] - Allow a leading zero.
|
|
462
|
+
* @param {string} [options.separator=''] - Insert separator every N digits.
|
|
463
|
+
* @param {number} [options.groupSize=3] - Group size when using separator.
|
|
464
|
+
* @returns {string} A fixed-length digit string.
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* Fake.numericString(6); // '047283'
|
|
468
|
+
* Fake.numericString(16, { separator: '-', groupSize: 4 }); // '1234-5678-9012-3456'
|
|
469
|
+
*/
|
|
470
|
+
static numericString(length = 6, options = {}) {
|
|
471
|
+
const allowLeading = options.leadingZeros !== false;
|
|
472
|
+
let str = '';
|
|
473
|
+
for (let i = 0; i < length; i++) {
|
|
474
|
+
const min = (!allowLeading && i === 0) ? 1 : 0;
|
|
475
|
+
str += _int(min, 9);
|
|
476
|
+
}
|
|
477
|
+
if (options.separator) {
|
|
478
|
+
const gs = options.groupSize || 3;
|
|
479
|
+
const re = new RegExp(`(.{${gs}})(?=.)`, 'g');
|
|
480
|
+
str = str.replace(re, `$1${options.separator}`);
|
|
481
|
+
}
|
|
482
|
+
return str;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Random alphanumeric string of `length` characters.
|
|
487
|
+
*
|
|
488
|
+
* @param {number} [length=10] - String length.
|
|
489
|
+
* @param {object} [options] - Configuration options.
|
|
490
|
+
* @param {boolean} [options.uppercase=false] - Whether to use uppercase.
|
|
491
|
+
* @param {boolean} [options.letters=true] - Include letter characters.
|
|
492
|
+
* @param {boolean} [options.digits=true] - Include digit characters.
|
|
493
|
+
* @returns {string} An alphanumeric string.
|
|
494
|
+
*/
|
|
495
|
+
static alphanumeric(length = 10, options = {}) {
|
|
496
|
+
let pool = '';
|
|
497
|
+
if (options.letters !== false) {
|
|
498
|
+
pool += options.uppercase ? CHARSET_UPPERCASE : CHARSET_LOWERCASE;
|
|
499
|
+
}
|
|
500
|
+
if (options.digits !== false) pool += CHARSET_DIGITS;
|
|
501
|
+
if (!pool) pool = CHARSET_LOWERCASE + CHARSET_DIGITS;
|
|
502
|
+
let str = '';
|
|
503
|
+
for (let i = 0; i < length; i++) str += pool[Math.floor(rand() * pool.length)];
|
|
504
|
+
return str;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Random alpha-only string of `length` characters.
|
|
509
|
+
*
|
|
510
|
+
* @param {number} [length=8] - String length.
|
|
511
|
+
* @param {object} [options] - Configuration options.
|
|
512
|
+
* @param {boolean} [options.uppercase=false] - Use uppercase letters.
|
|
513
|
+
* @param {boolean} [options.mixed=false] - Mix upper and lower.
|
|
514
|
+
* @returns {string} An alphabetic string.
|
|
515
|
+
*/
|
|
516
|
+
static alpha(length = 8, options = {}) {
|
|
517
|
+
let pool;
|
|
518
|
+
if (options.mixed) pool = CHARSET_LOWERCASE + CHARSET_UPPERCASE;
|
|
519
|
+
else if (options.uppercase) pool = CHARSET_UPPERCASE;
|
|
520
|
+
else pool = CHARSET_LOWERCASE;
|
|
521
|
+
let str = '';
|
|
522
|
+
for (let i = 0; i < length; i++) str += pool[Math.floor(rand() * pool.length)];
|
|
523
|
+
return str;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Random boolean. */
|
|
527
|
+
static boolean() { return rand() > 0.5; }
|
|
528
|
+
|
|
529
|
+
// -- Dates --------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Random Date between start and end.
|
|
533
|
+
*
|
|
534
|
+
* @param {Date} [start] - Start offset.
|
|
535
|
+
* @param {Date} [end] - End value.
|
|
536
|
+
* @returns {Date} A random Date object.
|
|
537
|
+
*/
|
|
538
|
+
static date(start = new Date(2020, 0, 1), end = new Date()) {
|
|
539
|
+
const ms = start.getTime() + rand() * (end.getTime() - start.getTime());
|
|
540
|
+
return new Date(ms);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Random ISO date string.
|
|
545
|
+
*
|
|
546
|
+
* @param {Date} [start] - Start offset.
|
|
547
|
+
* @param {Date} [end] - End value.
|
|
548
|
+
* @returns {string} An ISO 8601 date string.
|
|
549
|
+
*/
|
|
550
|
+
static dateString(start, end) {
|
|
551
|
+
return Fake.date(start, end).toISOString();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Random date in the past.
|
|
556
|
+
*
|
|
557
|
+
* @param {object} [options] - Configuration options.
|
|
558
|
+
* @param {number} [options.years=1] - How many years back the window spans.
|
|
559
|
+
* @returns {Date} A Date in the past.
|
|
560
|
+
*/
|
|
561
|
+
static datePast(options = {}) {
|
|
562
|
+
const years = options.years || 1;
|
|
563
|
+
const end = new Date();
|
|
564
|
+
const start = new Date(end);
|
|
565
|
+
start.setFullYear(start.getFullYear() - years);
|
|
566
|
+
return Fake.date(start, end);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Random date in the future.
|
|
571
|
+
*
|
|
572
|
+
* @param {object} [options] - Configuration options.
|
|
573
|
+
* @param {number} [options.years=1] - Year range offset.
|
|
574
|
+
* @returns {Date} A Date in the future.
|
|
575
|
+
*/
|
|
576
|
+
static dateFuture(options = {}) {
|
|
577
|
+
const years = options.years || 1;
|
|
578
|
+
const start = new Date();
|
|
579
|
+
const end = new Date(start);
|
|
580
|
+
end.setFullYear(end.getFullYear() + years);
|
|
581
|
+
return Fake.date(start, end);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// -- Text ---------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Random word from the lorem ipsum vocabulary.
|
|
588
|
+
*
|
|
589
|
+
* @param {object} [options] - Configuration options.
|
|
590
|
+
* @param {'lorem'|'hacker'|'adjective'|'noun'} [options.type='lorem'] - Content type variant.
|
|
591
|
+
* @returns {string} A single word.
|
|
592
|
+
*/
|
|
593
|
+
static word(options = {}) {
|
|
594
|
+
const type = options.type || 'lorem';
|
|
595
|
+
if (type === 'hacker') return _pick(HACKER_NOUNS);
|
|
596
|
+
if (type === 'adjective') return _pick(ADJECTIVES);
|
|
597
|
+
if (type === 'noun') return _pick(NOUNS);
|
|
598
|
+
return _pick(LOREM_WORDS);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Array of n random words.
|
|
603
|
+
*
|
|
604
|
+
* @param {number} [n=3] - Count.
|
|
605
|
+
* @returns {string[]} Array of random words.
|
|
606
|
+
*/
|
|
607
|
+
static words(n = 3) {
|
|
608
|
+
return Array.from({ length: n }, () => Fake.word());
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Random sentence of `wordCount` words.
|
|
613
|
+
*
|
|
614
|
+
* @param {number|object} [wordCount] - Number of words.
|
|
615
|
+
* @returns {string} A sentence ending with a period.
|
|
616
|
+
*/
|
|
617
|
+
static sentence(wordCount) {
|
|
618
|
+
const count = wordCount || _int(5, 15);
|
|
619
|
+
const words = Array.from({ length: count }, () => _pick(LOREM_WORDS));
|
|
620
|
+
words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
|
|
621
|
+
return words.join(' ') + '.';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Random paragraph of `sentences` sentences.
|
|
626
|
+
*
|
|
627
|
+
* @param {number} [sentences=3] - Number of sentences.
|
|
628
|
+
* @returns {string} A multi-sentence paragraph.
|
|
629
|
+
*/
|
|
630
|
+
static paragraph(sentences = 3) {
|
|
631
|
+
return Array.from({ length: sentences }, () => Fake.sentence()).join(' ');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Hacker-speak phrase ("synthesize redundant protocols").
|
|
636
|
+
*
|
|
637
|
+
* @returns {string} A hacker-speak phrase.
|
|
638
|
+
*/
|
|
639
|
+
static hackerPhrase() {
|
|
640
|
+
return `${_pick(HACKER_VERBS)} ${_pick(HACKER_ADJECTIVES)} ${_pick(HACKER_NOUNS)}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* URL-friendly slug from n random words.
|
|
645
|
+
*
|
|
646
|
+
* @param {number} [words=3] - Number of words.
|
|
647
|
+
* @returns {string} e.g. 'forward-online-portal'
|
|
648
|
+
*/
|
|
649
|
+
static slug(words = 3) {
|
|
650
|
+
return Array.from({ length: words }, () => _pick(ADJECTIVES.concat(NOUNS)))
|
|
651
|
+
.map(w => w.toLowerCase().replace(/[^a-z0-9]/g, '-'))
|
|
652
|
+
.join('-');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Random hashtag (no spaces, prefixed with #).
|
|
657
|
+
*
|
|
658
|
+
* @returns {string} e.g. '#resilientfuture'
|
|
659
|
+
*/
|
|
660
|
+
static hashtag() {
|
|
661
|
+
return '#' + _pick(ADJECTIVES) + _pick(NOUNS);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// -- Person -------------------------------------------------------------
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Random job title.
|
|
668
|
+
*
|
|
669
|
+
* @param {object} [options] - Configuration options.
|
|
670
|
+
* @param {boolean} [options.full=false] - Use a pre-built full title instead of generated.
|
|
671
|
+
* @returns {string} A job title string.
|
|
672
|
+
*/
|
|
673
|
+
static jobTitle(options = {}) {
|
|
674
|
+
if (options.full) return _pick(JOB_TITLES);
|
|
675
|
+
return `${_pick(JOB_DESCRIPTORS)} ${_pick(JOB_AREAS)} ${_pick(JOB_TYPES)}`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Random job area / department string (e.g. 'Engineering'). */
|
|
679
|
+
static jobArea() { return _pick(JOB_AREAS); }
|
|
680
|
+
|
|
681
|
+
/** Random job type noun (e.g. 'Manager'). */
|
|
682
|
+
static jobType() { return _pick(JOB_TYPES); }
|
|
683
|
+
|
|
684
|
+
/** Random job level descriptor (e.g. 'Senior'). */
|
|
685
|
+
static jobDescriptor() { return _pick(JOB_DESCRIPTORS); }
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Random short biography string.
|
|
689
|
+
*
|
|
690
|
+
* @param {object} [options] - Configuration options.
|
|
691
|
+
* @param {'phrase'|'full'} [options.style='phrase'] - Style variant.
|
|
692
|
+
* @returns {string} A short biography phrase.
|
|
693
|
+
*
|
|
694
|
+
* @example
|
|
695
|
+
* Fake.bio(); // 'avid maker | living one commit at a time'
|
|
696
|
+
*/
|
|
697
|
+
static bio(options = {}) {
|
|
698
|
+
if (options.style === 'full') {
|
|
699
|
+
return `${_pick(BIO_ADJECTIVES)} ${_pick(BIO_NOUNS)} | ${_pick(BIO_PHRASES)}`;
|
|
700
|
+
}
|
|
701
|
+
return _pick(BIO_PHRASES);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Random zodiac sign. */
|
|
705
|
+
static zodiacSign() { return _pick(ZODIAC_SIGNS); }
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Random gender label.
|
|
709
|
+
*
|
|
710
|
+
* @param {object} [options] - Configuration options.
|
|
711
|
+
* @param {boolean} [options.binary=false] - Return only 'Male' or 'Female'.
|
|
712
|
+
* @returns {string} A gender label.
|
|
713
|
+
*/
|
|
714
|
+
static gender(options = {}) {
|
|
715
|
+
if (options.binary) return rand() > 0.5 ? 'Male' : 'Female';
|
|
716
|
+
return _pick(GENDERS);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Random blood type (A+, B-, O+, AB+, …). */
|
|
720
|
+
static bloodType() { return _pick(BLOOD_TYPES); }
|
|
721
|
+
|
|
722
|
+
// -- Location ----------------------------------------------------------
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Random city.
|
|
726
|
+
*
|
|
727
|
+
* @param {object} [options] - Configuration options.
|
|
728
|
+
* @param {string} [options.country] - Filter by ISO 3166-1 alpha-2 country code.
|
|
729
|
+
* @returns {string} A city name.
|
|
730
|
+
*/
|
|
731
|
+
static city(options = {}) {
|
|
732
|
+
const pool = options.country
|
|
733
|
+
? CITIES.filter(c => c.country === options.country.toUpperCase())
|
|
734
|
+
: CITIES;
|
|
735
|
+
return _pick(pool.length ? pool : CITIES).name;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Random country.
|
|
740
|
+
*
|
|
741
|
+
* @param {object} [options] - Configuration options.
|
|
742
|
+
* @param {boolean} [options.codeOnly=false] - Return ISO code only.
|
|
743
|
+
* @param {boolean} [options.full=false] - Return { name, code } object.
|
|
744
|
+
* @returns {string|{name:string,code:string}}
|
|
745
|
+
*/
|
|
746
|
+
static country(options = {}) {
|
|
747
|
+
const entry = _pick(COUNTRIES);
|
|
748
|
+
if (options.codeOnly) return entry.code;
|
|
749
|
+
if (options.full) return { ...entry };
|
|
750
|
+
return entry.name;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Random US state.
|
|
755
|
+
*
|
|
756
|
+
* @param {object} [options] - Configuration options.
|
|
757
|
+
* @param {boolean} [options.abbr=false] - Return 2-letter abbreviation.
|
|
758
|
+
* @param {boolean} [options.full=false] - Return { name, abbr } object.
|
|
759
|
+
* @returns {string|{name:string,abbr:string}}
|
|
760
|
+
*/
|
|
761
|
+
static state(options = {}) {
|
|
762
|
+
const entry = _pick(US_STATES);
|
|
763
|
+
if (options.abbr) return entry.abbr;
|
|
764
|
+
if (options.full) return { ...entry };
|
|
765
|
+
return entry.name;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Random postal / ZIP code.
|
|
770
|
+
*
|
|
771
|
+
* @param {object} [options] - Configuration options.
|
|
772
|
+
* @param {string} [options.countryCode='US'] - ISO country code.
|
|
773
|
+
* @returns {string} A postal code string.
|
|
774
|
+
*/
|
|
775
|
+
static zipCode(options = {}) {
|
|
776
|
+
const code = (options.countryCode || 'US').toUpperCase();
|
|
777
|
+
const pattern = ZIP_PATTERNS[code] || ZIP_PATTERNS['US'];
|
|
778
|
+
return _expandTemplate(pattern);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Random latitude as a float in [-90, 90].
|
|
783
|
+
*
|
|
784
|
+
* @param {object} [options] - Configuration options.
|
|
785
|
+
* @param {number} [options.min=-90] - Minimum value.
|
|
786
|
+
* @param {number} [options.max=90] - Maximum value.
|
|
787
|
+
* @param {number} [options.decimals=6] - Decimal places.
|
|
788
|
+
* @returns {number} A latitude value.
|
|
789
|
+
*/
|
|
790
|
+
static latitude(options = {}) {
|
|
791
|
+
return Fake.float(options.min ?? -90, options.max ?? 90, options.decimals ?? 6);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Random longitude as a float in [-180, 180].
|
|
796
|
+
*
|
|
797
|
+
* @param {object} [options] - Configuration options.
|
|
798
|
+
* @param {number} [options.min=-180] - Minimum value.
|
|
799
|
+
* @param {number} [options.max=180] - Maximum value.
|
|
800
|
+
* @param {number} [options.decimals=6] - Decimal places.
|
|
801
|
+
* @returns {number} A longitude value.
|
|
802
|
+
*/
|
|
803
|
+
static longitude(options = {}) {
|
|
804
|
+
return Fake.float(options.min ?? -180, options.max ?? 180, options.decimals ?? 6);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Random { latitude, longitude } coordinate object.
|
|
809
|
+
*
|
|
810
|
+
* @param {object} [options] - Passed to latitude() and longitude().
|
|
811
|
+
* @returns {{ latitude: number, longitude: number }}
|
|
812
|
+
*/
|
|
813
|
+
static coordinates(options = {}) {
|
|
814
|
+
return { latitude: Fake.latitude(options), longitude: Fake.longitude(options) };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Random IANA timezone identifier (e.g. 'America/New_York'). */
|
|
818
|
+
static timezone() { return _pick(TIMEZONES); }
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Random street name (e.g. 'Oak Avenue').
|
|
822
|
+
*
|
|
823
|
+
* @returns {string} A street name.
|
|
824
|
+
*/
|
|
825
|
+
static streetName() {
|
|
826
|
+
return `${_pick(STREET_NAMES)} ${_pick(STREET_TYPES)}`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Random full street address.
|
|
831
|
+
*
|
|
832
|
+
* @param {object} [options] - Configuration options.
|
|
833
|
+
* @param {string} [options.countryCode='US'] - ISO country code.
|
|
834
|
+
* @param {'string'|'object'} [options.format='string'] - Output format.
|
|
835
|
+
* @returns {string|object} A formatted address string, or an address object when format is 'object'.
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* Fake.address();
|
|
839
|
+
* // '742 Evergreen Terrace, Springfield, IL 62701'
|
|
840
|
+
*
|
|
841
|
+
* Fake.address({ format: 'object' });
|
|
842
|
+
* // { streetNumber, streetName, city, state, zipCode, country }
|
|
843
|
+
*/
|
|
844
|
+
static address(options = {}) {
|
|
845
|
+
const code = (options.countryCode || 'US').toUpperCase();
|
|
846
|
+
const streetNum = _int(1, 9999);
|
|
847
|
+
const street = Fake.streetName();
|
|
848
|
+
const city = Fake.city({ country: code });
|
|
849
|
+
const zipCode = Fake.zipCode({ countryCode: code });
|
|
850
|
+
const stateEntry = code === 'US' ? _pick(US_STATES) : null;
|
|
851
|
+
const countryName = (COUNTRIES.find(c => c.code === code) || { name: code }).name;
|
|
852
|
+
|
|
853
|
+
if (options.format === 'object') {
|
|
854
|
+
return {
|
|
855
|
+
streetNumber: String(streetNum),
|
|
856
|
+
streetName: street,
|
|
857
|
+
city,
|
|
858
|
+
state: stateEntry ? stateEntry.abbr : null,
|
|
859
|
+
zipCode,
|
|
860
|
+
country: countryName,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (stateEntry) {
|
|
865
|
+
return `${streetNum} ${street}, ${city}, ${stateEntry.abbr} ${zipCode}`;
|
|
866
|
+
}
|
|
867
|
+
return `${streetNum} ${street}, ${city} ${zipCode}, ${countryName}`;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// -- Commerce -----------------------------------------------------------
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Random product name (adjective + material + noun).
|
|
874
|
+
*
|
|
875
|
+
* @param {object} [options] - Configuration options.
|
|
876
|
+
* @param {boolean} [options.withMaterial=true] - Include a material word.
|
|
877
|
+
* @returns {string} A product name.
|
|
878
|
+
*/
|
|
879
|
+
static productName(options = {}) {
|
|
880
|
+
const withMaterial = options.withMaterial !== false;
|
|
881
|
+
if (withMaterial) {
|
|
882
|
+
return `${_pick(PRODUCT_ADJECTIVES)} ${_pick(PRODUCT_MATERIALS)} ${_pick(PRODUCT_NOUNS)}`;
|
|
883
|
+
}
|
|
884
|
+
return `${_pick(PRODUCT_ADJECTIVES)} ${_pick(PRODUCT_NOUNS)}`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** Random product category (e.g. 'Electronics'). */
|
|
888
|
+
static category() { return _pick(CATEGORIES); }
|
|
889
|
+
|
|
890
|
+
/** Random business department name. */
|
|
891
|
+
static department() { return _pick(DEPARTMENTS); }
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Random company name.
|
|
895
|
+
*
|
|
896
|
+
* @param {object} [options] - Configuration options.
|
|
897
|
+
* @param {boolean} [options.suffix=true] - Append LLC / Inc. / etc.
|
|
898
|
+
* @returns {string} A company name.
|
|
899
|
+
*/
|
|
900
|
+
static company(options = {}) {
|
|
901
|
+
const withSuffix = options.suffix !== false;
|
|
902
|
+
const name = `${_pick(COMPANY_ADJECTIVES)} ${_pick(COMPANY_NOUNS)}`;
|
|
903
|
+
return withSuffix ? `${name} ${_pick(COMPANY_SUFFIXES)}` : name;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Random price as a float with 2 decimal places.
|
|
908
|
+
*
|
|
909
|
+
* @param {object} [options] - Configuration options.
|
|
910
|
+
* @param {number} [options.min=0.99] - Minimum value.
|
|
911
|
+
* @param {number} [options.max=999.99] - Maximum value.
|
|
912
|
+
* @param {string} [options.symbol=''] - Prepend a currency symbol.
|
|
913
|
+
* @returns {string|number} - String when symbol is provided, number otherwise.
|
|
914
|
+
*/
|
|
915
|
+
static price(options = {}) {
|
|
916
|
+
const val = Fake.float(options.min ?? 0.99, options.max ?? 999.99, 2);
|
|
917
|
+
return options.symbol ? `${options.symbol}${val.toFixed(2)}` : val;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Random industry sector name. */
|
|
921
|
+
static industry() { return _pick(INDUSTRIES); }
|
|
922
|
+
|
|
923
|
+
/** Random catch phrase buzzword phrase. */
|
|
924
|
+
static catchPhrase() {
|
|
925
|
+
return `${_pick(CATCH_PHRASE_ADJECTIVES)} ${_pick(CATCH_PHRASE_NOUNS)}`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// -- Internet & Network -------------------------------------------------
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Random email address (backward-compatible shorthand — same as email()).
|
|
932
|
+
* Accepts no arguments for historical use.
|
|
933
|
+
*/
|
|
934
|
+
|
|
935
|
+
/** Random UUID v4. */
|
|
936
|
+
static uuid() { return _uuid(); }
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Random domain name (adjective-noun.tld).
|
|
940
|
+
*
|
|
941
|
+
* @param {object} [options] - Configuration options.
|
|
942
|
+
* @param {string} [options.tld] - Force a specific TLD.
|
|
943
|
+
* @returns {string} A domain name.
|
|
944
|
+
*/
|
|
945
|
+
static domainName(options = {}) {
|
|
946
|
+
const tld = options.tld || _pick(TLDS);
|
|
947
|
+
return `${_pick(DOMAIN_ADJECTIVES)}-${_pick(DOMAIN_NOUNS)}.${tld}`;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Random URL.
|
|
952
|
+
*
|
|
953
|
+
* @param {object} [options] - Configuration options.
|
|
954
|
+
* @param {'http'|'https'} [options.protocol='https'] - URL protocol.
|
|
955
|
+
* @param {boolean} [options.appendSlash=false] - Append trailing slash.
|
|
956
|
+
* @returns {string} A URL string.
|
|
957
|
+
*/
|
|
958
|
+
static url(options = {}) {
|
|
959
|
+
const proto = options.protocol || 'https';
|
|
960
|
+
const path = options.noPath ? '' : `/${Fake.word()}`;
|
|
961
|
+
const slash = options.appendSlash ? '/' : '';
|
|
962
|
+
return `${proto}://${Fake.domainName()}${path}${slash}`;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Random IPv4 address.
|
|
967
|
+
*
|
|
968
|
+
* @param {object} [options] - Configuration options.
|
|
969
|
+
* @param {'any'|'private-a'|'private-b'|'private-c'|'loopback'} [options.network='any'] - Network type.
|
|
970
|
+
* @returns {string} An IPv4 address.
|
|
971
|
+
*/
|
|
972
|
+
static ip(options = {}) {
|
|
973
|
+
const net = options.network || 'any';
|
|
974
|
+
if (net === 'loopback') return `127.0.0.${_int(1, 254)}`;
|
|
975
|
+
if (net === 'private-a') return `10.${_int(0,255)}.${_int(0,255)}.${_int(1,254)}`;
|
|
976
|
+
if (net === 'private-b') return `172.${_int(16,31)}.${_int(0,255)}.${_int(1,254)}`;
|
|
977
|
+
if (net === 'private-c') return `192.168.${_int(0,255)}.${_int(1,254)}`;
|
|
978
|
+
return `${_int(1,254)}.${_int(0,255)}.${_int(0,255)}.${_int(1,254)}`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Random IPv6 address.
|
|
983
|
+
*
|
|
984
|
+
* @returns {string} An IPv6 address.
|
|
985
|
+
*/
|
|
986
|
+
static ipv6() {
|
|
987
|
+
const groups = Array.from({ length: 8 }, () =>
|
|
988
|
+
_int(0, 65535).toString(16).padStart(4, '0')
|
|
989
|
+
);
|
|
990
|
+
return groups.join(':');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Random MAC address.
|
|
995
|
+
*
|
|
996
|
+
* @param {object} [options] - Configuration options.
|
|
997
|
+
* @param {':'|'-'|''} [options.separator=':'] - Separator character.
|
|
998
|
+
* @param {boolean} [options.realisticOUI=false] - Use a real vendor OUI prefix.
|
|
999
|
+
* @returns {string} A MAC address string.
|
|
1000
|
+
*/
|
|
1001
|
+
static mac(options = {}) {
|
|
1002
|
+
const sep = options.separator ?? ':';
|
|
1003
|
+
if (options.realisticOUI) {
|
|
1004
|
+
const oui = _pick(MAC_OUIS).replace(/:/g, sep);
|
|
1005
|
+
const rest = Array.from({ length: 3 }, () =>
|
|
1006
|
+
_int(0, 255).toString(16).padStart(2, '0')
|
|
1007
|
+
).join(sep);
|
|
1008
|
+
return `${oui}${sep}${rest}`;
|
|
1009
|
+
}
|
|
1010
|
+
return Array.from({ length: 6 }, () =>
|
|
1011
|
+
_int(0, 255).toString(16).padStart(2, '0')
|
|
1012
|
+
).join(sep);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Random network port number.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {object} [options] - Configuration options.
|
|
1019
|
+
* @param {'well-known'|'registered'|'dynamic'|'any'} [options.range='any'] - Address range.
|
|
1020
|
+
* well-known → 1–1023
|
|
1021
|
+
* registered → 1024–49151
|
|
1022
|
+
* dynamic → 49152–65535
|
|
1023
|
+
* any → 1–65535
|
|
1024
|
+
* @returns {number} A port number.
|
|
1025
|
+
*/
|
|
1026
|
+
static port(options = {}) {
|
|
1027
|
+
const range = options.range || 'any';
|
|
1028
|
+
if (range === 'well-known') return _int(1, 1023);
|
|
1029
|
+
if (range === 'registered') return _int(1024, 49151);
|
|
1030
|
+
if (range === 'dynamic') return _int(49152, 65535);
|
|
1031
|
+
return _int(1, 65535);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Random HTTP method.
|
|
1036
|
+
*
|
|
1037
|
+
* @param {object} [options] - Configuration options.
|
|
1038
|
+
* @param {string[]} [options.methods] - Restrict to specific methods.
|
|
1039
|
+
* @returns {'GET'|'POST'|'PUT'|'PATCH'|'DELETE'|'HEAD'|'OPTIONS'} An HTTP method string.
|
|
1040
|
+
*/
|
|
1041
|
+
static httpMethod(options = {}) {
|
|
1042
|
+
const pool = options.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
1043
|
+
return _pick(pool);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Random user agent string (realistic browser/client).
|
|
1048
|
+
*
|
|
1049
|
+
* @returns {string} A user agent string.
|
|
1050
|
+
*/
|
|
1051
|
+
static userAgent() { return _pick(USER_AGENTS); }
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Random password-like string. NOT suitable for real passwords — uses a
|
|
1055
|
+
* PRNG seeded from Math.random, not a CSPRNG.
|
|
1056
|
+
*
|
|
1057
|
+
* @param {object} [options] - Configuration options.
|
|
1058
|
+
* @param {number} [options.length=16] - Character count.
|
|
1059
|
+
* @param {boolean} [options.uppercase=true] - Include uppercase letters.
|
|
1060
|
+
* @param {boolean} [options.lowercase=true] - Include lowercase letters.
|
|
1061
|
+
* @param {boolean} [options.digits=true] - Include digits.
|
|
1062
|
+
* @param {boolean} [options.special=false] - Include special characters.
|
|
1063
|
+
* @param {string} [options.prefix=''] - Prepend a prefix.
|
|
1064
|
+
* @returns {string} A password string.
|
|
1065
|
+
*/
|
|
1066
|
+
static password(options = {}) {
|
|
1067
|
+
const len = options.length ?? 16;
|
|
1068
|
+
const prefix = options.prefix ?? '';
|
|
1069
|
+
let pool = '';
|
|
1070
|
+
if (options.lowercase !== false) pool += CHARSET_LOWERCASE;
|
|
1071
|
+
if (options.uppercase !== false) pool += CHARSET_UPPERCASE;
|
|
1072
|
+
if (options.digits !== false) pool += CHARSET_DIGITS;
|
|
1073
|
+
if (options.special === true) pool += CHARSET_SPECIAL;
|
|
1074
|
+
pool = pool || (CHARSET_LOWERCASE + CHARSET_UPPERCASE + CHARSET_DIGITS);
|
|
1075
|
+
const body = Array.from({ length: Math.max(0, len - prefix.length) }, () =>
|
|
1076
|
+
pool[Math.floor(rand() * pool.length)]
|
|
1077
|
+
).join('');
|
|
1078
|
+
return `${prefix}${body}`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// -- Colors -------------------------------------------------------------
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Random hex color code.
|
|
1085
|
+
*
|
|
1086
|
+
* @returns {string} e.g. '#3a7f2b'
|
|
1087
|
+
*/
|
|
1088
|
+
static color() {
|
|
1089
|
+
return '#' + Math.floor(rand() * 16777215).toString(16).padStart(6, '0');
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Random RGB color object or string.
|
|
1094
|
+
*
|
|
1095
|
+
* @param {object} [options] - Configuration options.
|
|
1096
|
+
* @param {'object'|'css'} [options.format='css'] - Output format.
|
|
1097
|
+
* @returns {{ r:number, g:number, b:number }|string}
|
|
1098
|
+
*/
|
|
1099
|
+
static rgb(options = {}) {
|
|
1100
|
+
const r = _int(0, 255), g = _int(0, 255), b = _int(0, 255);
|
|
1101
|
+
if (options.format === 'object') return { r, g, b };
|
|
1102
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Random HSL color string or object.
|
|
1107
|
+
*
|
|
1108
|
+
* @param {object} [options] - Configuration options.
|
|
1109
|
+
* @param {'object'|'css'} [options.format='css'] - Output format.
|
|
1110
|
+
* @returns {{ h:number, s:number, l:number }|string}
|
|
1111
|
+
*/
|
|
1112
|
+
static hsl(options = {}) {
|
|
1113
|
+
const h = _int(0, 360), s = _int(20, 100), l = _int(20, 80);
|
|
1114
|
+
if (options.format === 'object') return { h, s, l };
|
|
1115
|
+
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// -- Helpers ------------------------------------------------------------
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Pick a random element from an array.
|
|
1122
|
+
*
|
|
1123
|
+
* @template T
|
|
1124
|
+
* @param {T[]} arr - Input array.
|
|
1125
|
+
* @returns {T} A random element from the array.
|
|
1126
|
+
*/
|
|
1127
|
+
static pick(arr) { return _pick(arr); }
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Pick n random elements from an array (no duplicates).
|
|
1131
|
+
*
|
|
1132
|
+
* @template T
|
|
1133
|
+
* @param {T[]} arr - Input array.
|
|
1134
|
+
* @param {number} n - Count.
|
|
1135
|
+
* @returns {T[]} An array of n distinct random elements.
|
|
1136
|
+
*/
|
|
1137
|
+
static pickMany(arr, n) { return _pickMany(arr, n); }
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Shuffle an array in-place using Fisher-Yates and return it.
|
|
1141
|
+
*
|
|
1142
|
+
* @template T
|
|
1143
|
+
* @param {T[]} arr - Input array.
|
|
1144
|
+
* @returns {T[]} The shuffled array (mutated in-place).
|
|
1145
|
+
*/
|
|
1146
|
+
static shuffle(arr) {
|
|
1147
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
1148
|
+
const j = Math.floor(rand() * (i + 1));
|
|
1149
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1150
|
+
}
|
|
1151
|
+
return arr;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Random JSON-safe object (useful as a quick fixture value).
|
|
1156
|
+
*
|
|
1157
|
+
* @returns {{ key: string, value: string, count: number, active: boolean }}
|
|
1158
|
+
*/
|
|
1159
|
+
static json() {
|
|
1160
|
+
return {
|
|
1161
|
+
key: Fake.word(),
|
|
1162
|
+
value: Fake.sentence(3),
|
|
1163
|
+
count: _int(1, 100),
|
|
1164
|
+
active: Fake.boolean(),
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Random element from an enum-like array, validated at call time.
|
|
1170
|
+
*
|
|
1171
|
+
* @template T
|
|
1172
|
+
* @param {readonly T[]} values - Array of values.
|
|
1173
|
+
* @returns {T} A random element from the values array.
|
|
1174
|
+
*/
|
|
1175
|
+
static enumValue(values) {
|
|
1176
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
1177
|
+
throw new Error('Fake.enumValue: requires a non-empty array');
|
|
1178
|
+
}
|
|
1179
|
+
return _pick(values);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Static uniqueness tracker — shared across all calls in the process lifetime
|
|
1184
|
+
Fake._tracker = new UniqueTracker();
|
|
1185
|
+
|
|
1186
|
+
module.exports = { Fake };
|