@flusys/nestjs-core 1.1.0-beta → 1.2.0-rc.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.
Files changed (61) hide show
  1. package/README.md +136 -162
  2. package/cjs/config/env-config.service.js +14 -10
  3. package/cjs/docs/docs.config.js +99 -15
  4. package/cjs/docs/index.js +0 -1
  5. package/cjs/index.js +0 -1
  6. package/cjs/interfaces/base-entity.interface.js +5 -3
  7. package/cjs/interfaces/database.interface.js +1 -3
  8. package/cjs/migration/datasource.factory.js +1 -3
  9. package/cjs/migration/index.js +0 -12
  10. package/cjs/migration/migration.cli.js +1 -17
  11. package/cjs/migration/migration.runner.js +37 -65
  12. package/cjs/utils/datasource-config.builder.js +2 -14
  13. package/config/env-config.service.d.ts +3 -1
  14. package/docs/docs.config.d.ts +8 -0
  15. package/docs/index.d.ts +0 -1
  16. package/fesm/config/env-config.service.js +14 -10
  17. package/fesm/docs/docs.config.js +89 -15
  18. package/fesm/docs/index.js +0 -1
  19. package/fesm/index.js +0 -2
  20. package/fesm/interfaces/app-config.interfaces.js +1 -3
  21. package/fesm/interfaces/base-entity.interface.js +5 -5
  22. package/fesm/interfaces/database.interface.js +1 -5
  23. package/fesm/migration/cli.js +1 -20
  24. package/fesm/migration/datasource.factory.js +3 -20
  25. package/fesm/migration/index.js +0 -14
  26. package/fesm/migration/migration.cli.js +1 -17
  27. package/fesm/migration/migration.runner.js +43 -132
  28. package/fesm/utils/datasource-config.builder.js +2 -13
  29. package/index.d.ts +0 -1
  30. package/interfaces/app-config.interfaces.d.ts +1 -0
  31. package/interfaces/base-entity.interface.d.ts +2 -2
  32. package/interfaces/database.interface.d.ts +2 -1
  33. package/package.json +9 -9
  34. package/utils/datasource-config.builder.d.ts +0 -1
  35. package/cjs/docs/docs.setup.js +0 -14
  36. package/cjs/seeders/base-seeder.js +0 -95
  37. package/cjs/seeders/cli.js +0 -398
  38. package/cjs/seeders/data-generator.js +0 -247
  39. package/cjs/seeders/entity-reader.js +0 -168
  40. package/cjs/seeders/index.js +0 -77
  41. package/cjs/seeders/seed-config.js +0 -101
  42. package/cjs/seeders/seed-runner.js +0 -320
  43. package/cjs/seeders/template-generator.js +0 -297
  44. package/docs/docs.setup.d.ts +0 -3
  45. package/fesm/docs/docs.setup.js +0 -4
  46. package/fesm/seeders/base-seeder.js +0 -110
  47. package/fesm/seeders/cli.js +0 -366
  48. package/fesm/seeders/data-generator.js +0 -249
  49. package/fesm/seeders/entity-reader.js +0 -170
  50. package/fesm/seeders/index.js +0 -11
  51. package/fesm/seeders/seed-config.js +0 -85
  52. package/fesm/seeders/seed-runner.js +0 -323
  53. package/fesm/seeders/template-generator.js +0 -257
  54. package/seeders/base-seeder.d.ts +0 -14
  55. package/seeders/cli.d.ts +0 -3
  56. package/seeders/data-generator.d.ts +0 -15
  57. package/seeders/entity-reader.d.ts +0 -45
  58. package/seeders/index.d.ts +0 -7
  59. package/seeders/seed-config.d.ts +0 -11
  60. package/seeders/seed-runner.d.ts +0 -47
  61. package/seeders/template-generator.d.ts +0 -16
@@ -1,366 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Seed CLI Entry Point
4
- *
5
- * Command-line interface for seed data generation.
6
- *
7
- * Commands:
8
- * - generate: Generate seeder template files
9
- * - run: Execute seeders for all entities
10
- * - run:all: Execute seeders for all tenants (multi-tenant mode)
11
- * - clear: Clear all seeded data
12
- * - status: Show seeding status
13
- *
14
- * Usage:
15
- * npm run seed:generate
16
- * npm run seed:run
17
- * npm run seed:run -- --count=50 --clear
18
- * npm run seed:clear
19
- * npm run seed:status
20
- */ import * as path from 'path';
21
- import { DataSource } from 'typeorm';
22
- import { envConfig } from '../config';
23
- import { EntityReader } from './entity-reader';
24
- import { TemplateGenerator } from './template-generator';
25
- import { SeedRunner } from './seed-runner';
26
- import { shouldSkipEntity } from './seed-config';
27
- /**
28
- * Show help message
29
- */ function showHelp() {
30
- console.log(`
31
- Seed Data CLI
32
-
33
- Usage:
34
- npm run seed:<command> [options]
35
-
36
- Commands:
37
- generate Generate seeder template files
38
- run Execute seeders for all entities
39
- run:all Execute seeders for all tenants (multi-tenant mode)
40
- clear Clear all seeded data
41
- status Show seeding status
42
-
43
- Options:
44
- --config=<path> Migration config file (default: src/persistence/migration.config.ts)
45
- --count=<number> Number of records per entity (default: from seed-config)
46
- --clear Clear existing data before seeding
47
- --entity=<name> Seed only specific entity
48
- --hard Hard delete when clearing (ignore soft delete)
49
- --tenant=<id> Target specific tenant (multi-tenant mode)
50
-
51
- Environment:
52
- MIGRATION_CONFIG Default config path (overrides auto-detection)
53
-
54
- Examples:
55
- npm run seed:generate
56
- npm run seed:run
57
- npm run seed:run -- --count=50 --clear
58
- npm run seed:run -- --entity=User --count=100
59
- npm run seed:run -- --config=src/persistence/migration.config.ts
60
- npm run seed:clear
61
- npm run seed:clear -- --hard
62
- npm run seed:status
63
- npm run seed:run:all
64
- `);
65
- }
66
- /**
67
- * Parse command-line arguments
68
- */ function parseArgs() {
69
- const args = process.argv.slice(2);
70
- let command;
71
- let count;
72
- let clear = false;
73
- let entity;
74
- let hard = false;
75
- let tenant;
76
- let configPath;
77
- for(let i = 0; i < args.length; i++){
78
- const arg = args[i];
79
- if (arg.startsWith('--count=')) {
80
- count = parseInt(arg.split('=')[1], 10);
81
- } else if (arg === '--clear') {
82
- clear = true;
83
- } else if (arg.startsWith('--entity=')) {
84
- entity = arg.split('=')[1];
85
- } else if (arg === '--hard') {
86
- hard = true;
87
- } else if (arg.startsWith('--tenant=')) {
88
- tenant = arg.split('=')[1];
89
- } else if (arg.startsWith('--config=')) {
90
- configPath = arg.split('=')[1];
91
- } else if (!arg.startsWith('-') && !command) {
92
- command = arg;
93
- }
94
- }
95
- return {
96
- command,
97
- count,
98
- clear,
99
- entity,
100
- hard,
101
- tenant,
102
- configPath
103
- };
104
- }
105
- // Resolved config path (set once during CLI initialization)
106
- let resolvedConfigPath;
107
- /**
108
- * Resolve migration config path with auto-detection
109
- */ async function resolveConfigPath(explicitPath) {
110
- if (resolvedConfigPath) return resolvedConfigPath;
111
- // Try paths in order: explicit, env var, then common defaults
112
- const envConfigPath = envConfig.tryGetValue('MIGRATION_CONFIG');
113
- const defaultPaths = [
114
- explicitPath,
115
- envConfigPath,
116
- 'src/persistence/migration.config.ts',
117
- 'src/persistence/migration.config.js',
118
- 'persistence/migration.config.ts',
119
- 'persistence/migration.config.js'
120
- ].filter(Boolean);
121
- for (const tryPath of defaultPaths){
122
- const absolutePath = path.isAbsolute(tryPath) ? tryPath : path.resolve(process.cwd(), tryPath);
123
- try {
124
- await import(absolutePath);
125
- resolvedConfigPath = absolutePath;
126
- return absolutePath;
127
- } catch {
128
- // Try next path
129
- }
130
- }
131
- console.error('❌ Config not found. Use --config=<path> or set MIGRATION_CONFIG env var');
132
- console.error(' Tried paths:', defaultPaths.join(', '));
133
- process.exit(1);
134
- }
135
- /**
136
- * Load DataSource configuration
137
- */ async function loadDataSource(tenantId, configPath) {
138
- const resolvedPath = await resolveConfigPath(configPath);
139
- try {
140
- const module1 = await import(resolvedPath);
141
- const migrationConfig = module1.migrationConfig || module1.default?.migrationConfig || module1;
142
- // Get database config for tenant
143
- const dbConfig = tenantId ? migrationConfig.tenants?.find((t)=>t.tenantId === tenantId || t.id === tenantId) : migrationConfig.defaultDatabaseConfig || migrationConfig.database;
144
- if (!dbConfig) {
145
- throw new Error(`Database configuration not found${tenantId ? ` for tenant: ${tenantId}` : ''}`);
146
- }
147
- // Get entities - handle both function and array
148
- const entities = typeof migrationConfig.entities === 'function' ? migrationConfig.entities() : migrationConfig.entities || [];
149
- // Create DataSource
150
- const dataSource = new DataSource({
151
- type: dbConfig.type || 'mysql',
152
- host: dbConfig.host,
153
- port: dbConfig.port,
154
- username: dbConfig.username,
155
- password: dbConfig.password,
156
- database: dbConfig.database,
157
- entities,
158
- synchronize: false,
159
- logging: false
160
- });
161
- await dataSource.initialize();
162
- return dataSource;
163
- } catch (error) {
164
- console.error('❌ Failed to load DataSource configuration');
165
- console.error(' Error:', error instanceof Error ? error.message : String(error));
166
- throw error;
167
- }
168
- }
169
- /**
170
- * Get all tenant IDs from configuration
171
- */ async function getAllTenantIds(configPath) {
172
- const resolvedPath = await resolveConfigPath(configPath);
173
- try {
174
- const module1 = await import(resolvedPath);
175
- const migrationConfig = module1.migrationConfig || module1.default?.migrationConfig || module1;
176
- if (migrationConfig.bootstrapAppConfig?.databaseMode !== 'multi-tenant' || !migrationConfig.tenants) {
177
- throw new Error('Multi-tenant mode is not configured');
178
- }
179
- return migrationConfig.tenants.map((t)=>t.tenantId || t.id);
180
- } catch (error) {
181
- console.error('❌ Failed to load tenant configuration');
182
- throw error;
183
- }
184
- }
185
- /**
186
- * Generate seeder template files
187
- */ async function generateCommand(configPath) {
188
- console.log('🔍 Discovering entities...\n');
189
- const dataSource = await loadDataSource(undefined, configPath);
190
- try {
191
- const reader = new EntityReader(dataSource);
192
- const generator = new TemplateGenerator();
193
- const entities = reader.getAllEntities();
194
- const outputDir = path.resolve(process.cwd(), 'src/seeders/generators');
195
- console.log(`📝 Generating seeder files in ${outputDir}\n`);
196
- let generatedCount = 0;
197
- const entityInfos = [];
198
- for (const metadata of entities){
199
- if (shouldSkipEntity(metadata.name)) {
200
- continue;
201
- }
202
- try {
203
- const entityInfo = reader.getEntityInfo(metadata.name);
204
- entityInfos.push(entityInfo);
205
- const filePath = generator.generateSeederFile(entityInfo, outputDir);
206
- console.log(`✓ Generated: ${path.basename(filePath)}`);
207
- generatedCount++;
208
- } catch (error) {
209
- console.error(`✗ Failed to generate ${metadata.name}:`, error instanceof Error ? error.message : String(error));
210
- }
211
- }
212
- // Generate index file
213
- const indexPath = generator.generateIndexFile(entityInfos, outputDir);
214
- console.log(`✓ Generated: ${path.basename(indexPath)}`);
215
- console.log(`\n✓ Generated ${generatedCount} seeder files\n`);
216
- } finally{
217
- await dataSource.destroy();
218
- }
219
- }
220
- /**
221
- * Run seeds for single database
222
- */ async function runCommand(options, configPath) {
223
- const dataSource = await loadDataSource(undefined, configPath);
224
- try {
225
- const runner = new SeedRunner(dataSource);
226
- if (options.entity) {
227
- console.log(`🌱 Seeding entity: ${options.entity}\n`);
228
- const result = await runner.runSingle(options.entity, options);
229
- if (result.success) {
230
- console.log(`\n✓ ${result.entity}: ${result.count} records created\n`);
231
- } else {
232
- console.error(`\n✗ ${result.entity}: ${result.error}\n`);
233
- process.exit(1);
234
- }
235
- } else {
236
- const results = await runner.runAll(options);
237
- const failedCount = results.filter((r)=>!r.success).length;
238
- if (failedCount > 0) {
239
- process.exit(1);
240
- }
241
- }
242
- } finally{
243
- await dataSource.destroy();
244
- }
245
- }
246
- /**
247
- * Run seeds for all tenants
248
- */ async function runAllTenantsCommand(options, configPath) {
249
- console.log('🏢 Running seeds for all tenants...\n');
250
- const tenantIds = await getAllTenantIds(configPath);
251
- let successCount = 0;
252
- let failCount = 0;
253
- for (const tenantId of tenantIds){
254
- console.log(`\n📦 Tenant: ${tenantId}`);
255
- console.log('─'.repeat(50));
256
- const dataSource = await loadDataSource(tenantId, configPath);
257
- try {
258
- const runner = new SeedRunner(dataSource);
259
- const results = await runner.runAll(options);
260
- const failed = results.filter((r)=>!r.success).length;
261
- if (failed === 0) {
262
- successCount++;
263
- } else {
264
- failCount++;
265
- }
266
- } catch (error) {
267
- console.error(`✗ Failed to seed tenant ${tenantId}:`, error);
268
- failCount++;
269
- } finally{
270
- await dataSource.destroy();
271
- }
272
- }
273
- console.log('\n' + '='.repeat(50));
274
- console.log(`\n✓ Completed: ${successCount} tenants seeded`);
275
- if (failCount > 0) {
276
- console.log(`✗ Failed: ${failCount} tenants\n`);
277
- process.exit(1);
278
- }
279
- }
280
- /**
281
- * Clear all seeded data
282
- */ async function clearCommand(hard = false, configPath) {
283
- const dataSource = await loadDataSource(undefined, configPath);
284
- try {
285
- const runner = new SeedRunner(dataSource);
286
- await runner.clearAll(hard);
287
- } finally{
288
- await dataSource.destroy();
289
- }
290
- }
291
- /**
292
- * Show seeding status
293
- */ async function statusCommand(configPath) {
294
- const dataSource = await loadDataSource(undefined, configPath);
295
- try {
296
- const runner = new SeedRunner(dataSource);
297
- const status = await runner.getStatus();
298
- console.log('\nSeed Status:');
299
- console.log('┌────────────────────────────┬───────────┬─────────┐');
300
- console.log('│ Entity │ Records │ Status │');
301
- console.log('├────────────────────────────┼───────────┼─────────┤');
302
- for (const item of status){
303
- const entityName = item.entity.padEnd(26);
304
- const count = item.count.toString().padEnd(9);
305
- const statusIcon = item.isEmpty ? '⚠ Empty' : '✓ Ready ';
306
- console.log(`│ ${entityName} │ ${count} │ ${statusIcon} │`);
307
- }
308
- console.log('└────────────────────────────┴───────────┴─────────┘\n');
309
- } finally{
310
- await dataSource.destroy();
311
- }
312
- }
313
- /**
314
- * Main CLI entry point
315
- */ async function main() {
316
- const args = parseArgs();
317
- if (!args.command) {
318
- showHelp();
319
- return;
320
- }
321
- // Initialize config path for all commands
322
- if (args.configPath) {
323
- await resolveConfigPath(args.configPath);
324
- }
325
- try {
326
- switch(args.command){
327
- case 'generate':
328
- await generateCommand(args.configPath);
329
- break;
330
- case 'run':
331
- await runCommand({
332
- count: args.count,
333
- clear: args.clear,
334
- entity: args.entity
335
- }, args.configPath);
336
- break;
337
- case 'run:all':
338
- await runAllTenantsCommand({
339
- count: args.count,
340
- clear: args.clear
341
- }, args.configPath);
342
- break;
343
- case 'clear':
344
- await clearCommand(args.hard, args.configPath);
345
- break;
346
- case 'status':
347
- await statusCommand(args.configPath);
348
- break;
349
- default:
350
- console.error(`❌ Unknown command: ${args.command}`);
351
- showHelp();
352
- process.exit(1);
353
- }
354
- } catch (error) {
355
- console.error('\n❌ CLI Error:', error instanceof Error ? error.message : String(error));
356
- process.exit(1);
357
- }
358
- }
359
- // Run CLI if called directly
360
- if (require.main === module) {
361
- main().catch((error)=>{
362
- console.error('Fatal error:', error);
363
- process.exit(1);
364
- });
365
- }
366
- export { main as runSeedCli };
@@ -1,249 +0,0 @@
1
- import { faker } from '@faker-js/faker';
2
- /**
3
- * Data Generator Service
4
- *
5
- * Generates realistic sample data based on column metadata.
6
- * Uses @faker-js/faker for data generation with intelligent field detection.
7
- *
8
- * Usage:
9
- * ```typescript
10
- * const generator = new DataGenerator();
11
- * const value = generator.generateValue(columnInfo);
12
- * const entity = generator.generateEntity(columns);
13
- * ```
14
- */ export class DataGenerator {
15
- /**
16
- * Generate value for a single column based on its metadata
17
- */ generateValue(column) {
18
- // Skip generated fields
19
- if (column.isGenerated) {
20
- return undefined;
21
- }
22
- // Handle nullable fields (50% chance of null)
23
- if (column.isNullable && faker.datatype.boolean()) {
24
- return null;
25
- }
26
- // Check for enum
27
- if (column.enum && column.enum.length > 0) {
28
- return faker.helpers.arrayElement(column.enum);
29
- }
30
- // Generate based on column name pattern
31
- const nameLower = column.propertyName.toLowerCase();
32
- const value = this.generateByName(nameLower, column);
33
- if (value !== undefined) {
34
- return value;
35
- }
36
- // Generate based on column type
37
- return this.generateByType(column);
38
- }
39
- /**
40
- * Generate entity object from column definitions
41
- */ generateEntity(columns) {
42
- const entity = {};
43
- for (const column of columns){
44
- const value = this.generateValue(column);
45
- if (value !== undefined) {
46
- entity[column.propertyName] = value;
47
- }
48
- }
49
- return entity;
50
- }
51
- /**
52
- * Generate value based on column name patterns
53
- */ generateByName(nameLower, column) {
54
- // Identity fields (skip, handled by database)
55
- if ([
56
- 'id',
57
- 'createdat',
58
- 'updatedat',
59
- 'deletedat'
60
- ].includes(nameLower)) {
61
- return undefined;
62
- }
63
- // Audit fields (skip, handled by interceptors)
64
- if ([
65
- 'createdbyid',
66
- 'updatedbyid',
67
- 'deletedbyid'
68
- ].includes(nameLower)) {
69
- return null;
70
- }
71
- // Name fields
72
- if (nameLower.includes('firstname')) return faker.person.firstName();
73
- if (nameLower.includes('lastname')) return faker.person.lastName();
74
- if (nameLower.includes('fullname') || nameLower === 'name') {
75
- return faker.person.fullName();
76
- }
77
- // Contact fields
78
- if (nameLower.includes('email')) return faker.internet.email().toLowerCase();
79
- if (nameLower.includes('phone')) return faker.phone.number();
80
- if (nameLower.includes('mobile')) return faker.phone.number();
81
- // Location fields
82
- if (nameLower.includes('address')) return faker.location.streetAddress();
83
- if (nameLower.includes('street')) return faker.location.street();
84
- if (nameLower.includes('city')) return faker.location.city();
85
- if (nameLower.includes('state')) return faker.location.state();
86
- if (nameLower.includes('country')) return faker.location.country();
87
- if (nameLower.includes('zipcode') || nameLower.includes('postalcode')) {
88
- return faker.location.zipCode();
89
- }
90
- // URL/Web fields
91
- if (nameLower.includes('url')) return faker.internet.url();
92
- if (nameLower.includes('website')) return faker.internet.url();
93
- if (nameLower.includes('domain')) return faker.internet.domainName();
94
- if (nameLower.includes('slug')) {
95
- return faker.helpers.slugify(faker.lorem.words(3)).toLowerCase();
96
- }
97
- // Text fields
98
- if (nameLower.includes('description')) return faker.lorem.paragraph();
99
- if (nameLower.includes('summary')) return faker.lorem.sentence();
100
- if (nameLower.includes('content')) return faker.lorem.paragraphs(3);
101
- if (nameLower.includes('title')) return faker.lorem.sentence();
102
- // User fields
103
- if (nameLower.includes('username')) return faker.internet.username();
104
- if (nameLower.includes('password')) return '$2b$12$dummy.hashed.password.value'; // Dummy bcrypt hash
105
- // Status/Active fields
106
- if (nameLower.includes('isactive')) return faker.datatype.boolean();
107
- if (nameLower.includes('isenabled')) return faker.datatype.boolean();
108
- if (nameLower.includes('ispublic')) return faker.datatype.boolean();
109
- if (nameLower.includes('isverified')) return faker.datatype.boolean();
110
- // Date fields
111
- if (nameLower.includes('birthdate') || nameLower.includes('dateofbirth')) {
112
- return faker.date.birthdate({
113
- min: 18,
114
- max: 80,
115
- mode: 'age'
116
- });
117
- }
118
- if (nameLower.includes('expiresat') || nameLower.includes('expirydate')) {
119
- return faker.date.future();
120
- }
121
- if (nameLower.includes('verifiedat')) {
122
- return faker.datatype.boolean() ? faker.date.recent() : null;
123
- }
124
- // Serial/Order fields
125
- if (nameLower.includes('serial') || nameLower.includes('order')) {
126
- return faker.number.int({
127
- min: 1,
128
- max: 100
129
- });
130
- }
131
- // Company/Organization
132
- if (nameLower.includes('company') && !nameLower.includes('id')) {
133
- return faker.company.name();
134
- }
135
- return undefined;
136
- }
137
- /**
138
- * Generate value based on column type
139
- */ generateByType(column) {
140
- const type = column.type.toLowerCase();
141
- switch(type){
142
- // String types
143
- case 'varchar':
144
- case 'character varying':
145
- case 'text':
146
- case 'string':
147
- return this.generateString(column);
148
- // Number types
149
- case 'int':
150
- case 'integer':
151
- case 'smallint':
152
- case 'bigint':
153
- return faker.number.int({
154
- min: 1,
155
- max: 1000
156
- });
157
- case 'decimal':
158
- case 'numeric':
159
- case 'float':
160
- case 'double':
161
- case 'real':
162
- return faker.number.float({
163
- min: 0,
164
- max: 10000,
165
- fractionDigits: column.scale || 2
166
- });
167
- // Boolean
168
- case 'boolean':
169
- case 'bool':
170
- return faker.datatype.boolean();
171
- // Date/Time
172
- case 'date':
173
- return faker.date.recent({
174
- days: 30
175
- });
176
- case 'timestamp':
177
- case 'datetime':
178
- return faker.date.recent({
179
- days: 30
180
- });
181
- case 'time':
182
- return faker.date.recent().toTimeString().split(' ')[0];
183
- // UUID
184
- case 'uuid':
185
- return faker.string.uuid();
186
- // JSON
187
- case 'json':
188
- case 'jsonb':
189
- return {
190
- key1: faker.lorem.word(),
191
- key2: faker.number.int({
192
- min: 1,
193
- max: 100
194
- })
195
- };
196
- // Array
197
- case 'array':
198
- return [
199
- faker.lorem.word(),
200
- faker.lorem.word()
201
- ];
202
- // Default
203
- default:
204
- return faker.lorem.word();
205
- }
206
- }
207
- /**
208
- * Generate string value respecting length constraints
209
- */ generateString(column) {
210
- const maxLength = typeof column.length === 'number' ? column.length : 255;
211
- if (maxLength <= 50) {
212
- return faker.lorem.word().substring(0, maxLength);
213
- } else if (maxLength <= 255) {
214
- return faker.lorem.sentence().substring(0, maxLength);
215
- } else {
216
- return faker.lorem.paragraph().substring(0, maxLength);
217
- }
218
- }
219
- /**
220
- * Generate related entity ID (for foreign keys)
221
- * @param relatedEntities Array of entities to pick from
222
- */ generateRelationId(relatedEntities) {
223
- if (relatedEntities.length === 0) {
224
- return null;
225
- }
226
- return faker.helpers.arrayElement(relatedEntities).id;
227
- }
228
- /**
229
- * Generate multiple related entity IDs (for many-to-many)
230
- * @param relatedEntities Array of entities to pick from
231
- * @param min Minimum number of relations
232
- * @param max Maximum number of relations
233
- */ generateRelationIds(relatedEntities, min = 1, max = 3) {
234
- if (relatedEntities.length === 0) {
235
- return [];
236
- }
237
- const count = faker.number.int({
238
- min,
239
- max: Math.min(max, relatedEntities.length)
240
- });
241
- const selected = faker.helpers.arrayElements(relatedEntities, count);
242
- return selected.map((e)=>e.id);
243
- }
244
- constructor(locale = 'en'){
245
- // Note: faker v8+ uses faker.locale instead of setLocale
246
- // For v8+: faker.locale = locale;
247
- // Locale is set globally, not needed for basic usage
248
- }
249
- }