@flusys/nestjs-core 1.1.0 → 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.
- package/README.md +113 -581
- package/cjs/config/env-config.service.js +13 -9
- package/cjs/docs/docs.config.js +25 -15
- package/cjs/index.js +0 -1
- package/config/env-config.service.d.ts +3 -1
- package/docs/docs.config.d.ts +1 -0
- package/fesm/config/env-config.service.js +13 -9
- package/fesm/docs/docs.config.js +27 -21
- package/fesm/index.js +0 -2
- package/index.d.ts +0 -1
- package/interfaces/base-entity.interface.d.ts +0 -3
- package/interfaces/database.interface.d.ts +1 -1
- package/package.json +8 -8
- package/cjs/seeders/base-seeder.js +0 -76
- package/cjs/seeders/cli.js +0 -291
- package/cjs/seeders/data-generator.js +0 -215
- package/cjs/seeders/entity-reader.js +0 -151
- package/cjs/seeders/field-patterns.js +0 -176
- package/cjs/seeders/index.js +0 -85
- package/cjs/seeders/seed-config.js +0 -62
- package/cjs/seeders/seed-runner.js +0 -314
- package/fesm/seeders/base-seeder.js +0 -66
- package/fesm/seeders/cli.js +0 -249
- package/fesm/seeders/data-generator.js +0 -210
- package/fesm/seeders/entity-reader.js +0 -153
- package/fesm/seeders/field-patterns.js +0 -147
- package/fesm/seeders/index.js +0 -7
- package/fesm/seeders/seed-config.js +0 -35
- package/fesm/seeders/seed-runner.js +0 -317
- package/seeders/base-seeder.d.ts +0 -14
- package/seeders/cli.d.ts +0 -3
- package/seeders/data-generator.d.ts +0 -15
- package/seeders/entity-reader.d.ts +0 -44
- package/seeders/field-patterns.d.ts +0 -12
- package/seeders/index.d.ts +0 -7
- package/seeders/seed-config.d.ts +0 -12
- package/seeders/seed-runner.d.ts +0 -48
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
function _define_property(obj, key, value) {
|
|
2
|
-
if (key in obj) {
|
|
3
|
-
Object.defineProperty(obj, key, {
|
|
4
|
-
value: value,
|
|
5
|
-
enumerable: true,
|
|
6
|
-
configurable: true,
|
|
7
|
-
writable: true
|
|
8
|
-
});
|
|
9
|
-
} else {
|
|
10
|
-
obj[key] = value;
|
|
11
|
-
}
|
|
12
|
-
return obj;
|
|
13
|
-
}
|
|
14
|
-
export class BaseSeeder {
|
|
15
|
-
async clear(hard = false) {
|
|
16
|
-
const hasSoftDelete = this.metadata.deleteDateColumn !== undefined;
|
|
17
|
-
if (hard || !hasSoftDelete) {
|
|
18
|
-
await this.repository.clear();
|
|
19
|
-
} else {
|
|
20
|
-
await this.repository.createQueryBuilder().softDelete().where('id IS NOT NULL').execute();
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
async count(includeDeleted = false) {
|
|
24
|
-
const hasSoftDelete = this.metadata.deleteDateColumn !== undefined;
|
|
25
|
-
if (hasSoftDelete && !includeDeleted) {
|
|
26
|
-
return this.repository.count({
|
|
27
|
-
where: {
|
|
28
|
-
deletedAt: null
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return this.repository.count();
|
|
33
|
-
}
|
|
34
|
-
async isEmpty() {
|
|
35
|
-
return await this.count() === 0;
|
|
36
|
-
}
|
|
37
|
-
getEntityName() {
|
|
38
|
-
return this.metadata.tableName;
|
|
39
|
-
}
|
|
40
|
-
async withTransaction(operation) {
|
|
41
|
-
const queryRunner = this.dataSource.createQueryRunner();
|
|
42
|
-
await queryRunner.connect();
|
|
43
|
-
await queryRunner.startTransaction();
|
|
44
|
-
try {
|
|
45
|
-
const transactionalRepo = queryRunner.manager.getRepository(this.entity);
|
|
46
|
-
const result = await operation(transactionalRepo);
|
|
47
|
-
await queryRunner.commitTransaction();
|
|
48
|
-
return result;
|
|
49
|
-
} catch (error) {
|
|
50
|
-
await queryRunner.rollbackTransaction();
|
|
51
|
-
throw error;
|
|
52
|
-
} finally{
|
|
53
|
-
await queryRunner.release();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
constructor(dataSource, entity){
|
|
57
|
-
_define_property(this, "dataSource", void 0);
|
|
58
|
-
_define_property(this, "entity", void 0);
|
|
59
|
-
_define_property(this, "repository", void 0);
|
|
60
|
-
_define_property(this, "metadata", void 0);
|
|
61
|
-
this.dataSource = dataSource;
|
|
62
|
-
this.entity = entity;
|
|
63
|
-
this.repository = dataSource.getRepository(entity);
|
|
64
|
-
this.metadata = this.repository.metadata;
|
|
65
|
-
}
|
|
66
|
-
}
|
package/fesm/seeders/cli.js
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Seed CLI Entry Point
|
|
4
|
-
*
|
|
5
|
-
* Commands:
|
|
6
|
-
* - run: Execute seeders for all entities
|
|
7
|
-
* - run:all: Execute seeders for all tenants (multi-tenant mode)
|
|
8
|
-
* - clear: Clear all seeded data
|
|
9
|
-
* - status: Show seeding status
|
|
10
|
-
*/ import * as path from 'path';
|
|
11
|
-
import { DataSource } from 'typeorm';
|
|
12
|
-
import { envConfig } from '../config';
|
|
13
|
-
import { SeedRunner } from './seed-runner';
|
|
14
|
-
function showHelp() {
|
|
15
|
-
console.log(`
|
|
16
|
-
Seed Data CLI
|
|
17
|
-
|
|
18
|
-
Usage:
|
|
19
|
-
npm run seed:<command> [options]
|
|
20
|
-
|
|
21
|
-
Commands:
|
|
22
|
-
run Execute seeders for all entities
|
|
23
|
-
run:all Execute seeders for all tenants (multi-tenant mode)
|
|
24
|
-
clear Clear all seeded data
|
|
25
|
-
status Show seeding status
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
--config=<path> Migration config file (default: src/persistence/migration.config.ts)
|
|
29
|
-
--count=<number> Number of records per entity (default: from seed-config)
|
|
30
|
-
--clear Clear existing data before seeding
|
|
31
|
-
--entity=<name> Seed only specific entity
|
|
32
|
-
--hard Hard delete when clearing (ignore soft delete)
|
|
33
|
-
--tenant=<id> Target specific tenant (multi-tenant mode)
|
|
34
|
-
|
|
35
|
-
Examples:
|
|
36
|
-
npm run seed:run
|
|
37
|
-
npm run seed:run -- --count=50 --clear
|
|
38
|
-
npm run seed:run -- --entity=User --count=100
|
|
39
|
-
npm run seed:clear
|
|
40
|
-
npm run seed:clear -- --hard
|
|
41
|
-
npm run seed:status
|
|
42
|
-
npm run seed:run:all
|
|
43
|
-
`);
|
|
44
|
-
}
|
|
45
|
-
function parseArgs() {
|
|
46
|
-
const args = process.argv.slice(2);
|
|
47
|
-
let command;
|
|
48
|
-
let count;
|
|
49
|
-
let clear = false;
|
|
50
|
-
let entity;
|
|
51
|
-
let hard = false;
|
|
52
|
-
let tenant;
|
|
53
|
-
let configPath;
|
|
54
|
-
for (const arg of args){
|
|
55
|
-
if (arg.startsWith('--count=')) {
|
|
56
|
-
count = parseInt(arg.split('=')[1], 10);
|
|
57
|
-
} else if (arg === '--clear') {
|
|
58
|
-
clear = true;
|
|
59
|
-
} else if (arg.startsWith('--entity=')) {
|
|
60
|
-
entity = arg.split('=')[1];
|
|
61
|
-
} else if (arg === '--hard') {
|
|
62
|
-
hard = true;
|
|
63
|
-
} else if (arg.startsWith('--tenant=')) {
|
|
64
|
-
tenant = arg.split('=')[1];
|
|
65
|
-
} else if (arg.startsWith('--config=')) {
|
|
66
|
-
configPath = arg.split('=')[1];
|
|
67
|
-
} else if (!arg.startsWith('-') && !command) {
|
|
68
|
-
command = arg;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
command,
|
|
73
|
-
count,
|
|
74
|
-
clear,
|
|
75
|
-
entity,
|
|
76
|
-
hard,
|
|
77
|
-
tenant,
|
|
78
|
-
configPath
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
let resolvedConfigPath;
|
|
82
|
-
async function resolveConfigPath(explicitPath) {
|
|
83
|
-
if (resolvedConfigPath) return resolvedConfigPath;
|
|
84
|
-
const envConfigPath = envConfig.tryGetValue('MIGRATION_CONFIG');
|
|
85
|
-
const defaultPaths = [
|
|
86
|
-
explicitPath,
|
|
87
|
-
envConfigPath,
|
|
88
|
-
'src/persistence/migration.config.ts',
|
|
89
|
-
'src/persistence/migration.config.js'
|
|
90
|
-
].filter(Boolean);
|
|
91
|
-
for (const tryPath of defaultPaths){
|
|
92
|
-
const absolutePath = path.isAbsolute(tryPath) ? tryPath : path.resolve(process.cwd(), tryPath);
|
|
93
|
-
try {
|
|
94
|
-
await import(absolutePath);
|
|
95
|
-
resolvedConfigPath = absolutePath;
|
|
96
|
-
return absolutePath;
|
|
97
|
-
} catch {
|
|
98
|
-
// Try next path
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
console.error('❌ Config not found. Use --config=<path> or set MIGRATION_CONFIG env var');
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
async function loadDataSource(tenantId, configPath) {
|
|
105
|
-
const resolvedPath = await resolveConfigPath(configPath);
|
|
106
|
-
const module1 = await import(resolvedPath);
|
|
107
|
-
const migrationConfig = module1.migrationConfig || module1.default?.migrationConfig || module1;
|
|
108
|
-
const dbConfig = tenantId ? migrationConfig.tenants?.find((t)=>t.tenantId === tenantId || t.id === tenantId) : migrationConfig.defaultDatabaseConfig || migrationConfig.database;
|
|
109
|
-
if (!dbConfig) {
|
|
110
|
-
throw new Error(`Database configuration not found${tenantId ? ` for tenant: ${tenantId}` : ''}`);
|
|
111
|
-
}
|
|
112
|
-
const entities = typeof migrationConfig.entities === 'function' ? migrationConfig.entities() : migrationConfig.entities || [];
|
|
113
|
-
const dataSource = new DataSource({
|
|
114
|
-
type: dbConfig.type || 'mysql',
|
|
115
|
-
host: dbConfig.host,
|
|
116
|
-
port: dbConfig.port,
|
|
117
|
-
username: dbConfig.username,
|
|
118
|
-
password: dbConfig.password,
|
|
119
|
-
database: dbConfig.database,
|
|
120
|
-
entities,
|
|
121
|
-
synchronize: false,
|
|
122
|
-
logging: false
|
|
123
|
-
});
|
|
124
|
-
await dataSource.initialize();
|
|
125
|
-
return dataSource;
|
|
126
|
-
}
|
|
127
|
-
async function getAllTenantIds(configPath) {
|
|
128
|
-
const resolvedPath = await resolveConfigPath(configPath);
|
|
129
|
-
const module1 = await import(resolvedPath);
|
|
130
|
-
const migrationConfig = module1.migrationConfig || module1.default?.migrationConfig || module1;
|
|
131
|
-
if (migrationConfig.bootstrapAppConfig?.databaseMode !== 'multi-tenant' || !migrationConfig.tenants) {
|
|
132
|
-
throw new Error('Multi-tenant mode is not configured');
|
|
133
|
-
}
|
|
134
|
-
return migrationConfig.tenants.map((t)=>t.tenantId || t.id);
|
|
135
|
-
}
|
|
136
|
-
async function runCommand(options, configPath) {
|
|
137
|
-
const dataSource = await loadDataSource(undefined, configPath);
|
|
138
|
-
try {
|
|
139
|
-
const runner = new SeedRunner(dataSource);
|
|
140
|
-
if (options.entity) {
|
|
141
|
-
console.log(`🌱 Seeding entity: ${options.entity}\n`);
|
|
142
|
-
const result = await runner.runSingle(options.entity, options);
|
|
143
|
-
if (result.success) {
|
|
144
|
-
console.log(`\n✓ ${result.entity}: ${result.count} records created\n`);
|
|
145
|
-
} else {
|
|
146
|
-
console.error(`\n✗ ${result.entity}: ${result.error}\n`);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
const results = await runner.runAll(options);
|
|
151
|
-
if (results.some((r)=>!r.success)) process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
} finally{
|
|
154
|
-
await dataSource.destroy();
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
async function runAllTenantsCommand(options, configPath) {
|
|
158
|
-
console.log('🏢 Running seeds for all tenants...\n');
|
|
159
|
-
const tenantIds = await getAllTenantIds(configPath);
|
|
160
|
-
let successCount = 0;
|
|
161
|
-
let failCount = 0;
|
|
162
|
-
for (const tenantId of tenantIds){
|
|
163
|
-
console.log(`\n📦 Tenant: ${tenantId}`);
|
|
164
|
-
console.log('─'.repeat(50));
|
|
165
|
-
const dataSource = await loadDataSource(tenantId, configPath);
|
|
166
|
-
try {
|
|
167
|
-
const runner = new SeedRunner(dataSource);
|
|
168
|
-
const results = await runner.runAll(options);
|
|
169
|
-
results.some((r)=>!r.success) ? failCount++ : successCount++;
|
|
170
|
-
} catch (error) {
|
|
171
|
-
console.error(`✗ Failed to seed tenant ${tenantId}:`, error);
|
|
172
|
-
failCount++;
|
|
173
|
-
} finally{
|
|
174
|
-
await dataSource.destroy();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
console.log('\n' + '='.repeat(50));
|
|
178
|
-
console.log(`\n✓ Completed: ${successCount} tenants seeded`);
|
|
179
|
-
if (failCount > 0) {
|
|
180
|
-
console.log(`✗ Failed: ${failCount} tenants\n`);
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
async function clearCommand(hard = false, configPath) {
|
|
185
|
-
const dataSource = await loadDataSource(undefined, configPath);
|
|
186
|
-
try {
|
|
187
|
-
await new SeedRunner(dataSource).clearAll(hard);
|
|
188
|
-
} finally{
|
|
189
|
-
await dataSource.destroy();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
async function statusCommand(configPath) {
|
|
193
|
-
const dataSource = await loadDataSource(undefined, configPath);
|
|
194
|
-
try {
|
|
195
|
-
const status = await new SeedRunner(dataSource).getStatus();
|
|
196
|
-
console.log('\nSeed Status:');
|
|
197
|
-
console.log('┌────────────────────────────┬───────────┬─────────┐');
|
|
198
|
-
console.log('│ Entity │ Records │ Status │');
|
|
199
|
-
console.log('├────────────────────────────┼───────────┼─────────┤');
|
|
200
|
-
for (const item of status){
|
|
201
|
-
console.log(`│ ${item.entity.padEnd(26)} │ ${item.count.toString().padEnd(9)} │ ${item.isEmpty ? '⚠ Empty' : '✓ Ready '} │`);
|
|
202
|
-
}
|
|
203
|
-
console.log('└────────────────────────────┴───────────┴─────────┘\n');
|
|
204
|
-
} finally{
|
|
205
|
-
await dataSource.destroy();
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
async function main() {
|
|
209
|
-
const args = parseArgs();
|
|
210
|
-
if (!args.command) {
|
|
211
|
-
showHelp();
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (args.configPath) {
|
|
215
|
-
await resolveConfigPath(args.configPath);
|
|
216
|
-
}
|
|
217
|
-
switch(args.command){
|
|
218
|
-
case 'run':
|
|
219
|
-
await runCommand({
|
|
220
|
-
count: args.count,
|
|
221
|
-
clear: args.clear,
|
|
222
|
-
entity: args.entity
|
|
223
|
-
}, args.configPath);
|
|
224
|
-
break;
|
|
225
|
-
case 'run:all':
|
|
226
|
-
await runAllTenantsCommand({
|
|
227
|
-
count: args.count,
|
|
228
|
-
clear: args.clear
|
|
229
|
-
}, args.configPath);
|
|
230
|
-
break;
|
|
231
|
-
case 'clear':
|
|
232
|
-
await clearCommand(args.hard, args.configPath);
|
|
233
|
-
break;
|
|
234
|
-
case 'status':
|
|
235
|
-
await statusCommand(args.configPath);
|
|
236
|
-
break;
|
|
237
|
-
default:
|
|
238
|
-
console.error(`❌ Unknown command: ${args.command}`);
|
|
239
|
-
showHelp();
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (require.main === module) {
|
|
244
|
-
main().catch((error)=>{
|
|
245
|
-
console.error('Fatal error:', error);
|
|
246
|
-
process.exit(1);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
export { main as runSeedCli };
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { faker } from '@faker-js/faker';
|
|
2
|
-
import { detectFieldPattern, detectTypeCategory, getStringLengthCategory, getTokenLength } from './field-patterns';
|
|
3
|
-
/**
|
|
4
|
-
* Data Generator Service
|
|
5
|
-
*
|
|
6
|
-
* Generates realistic sample data based on column metadata.
|
|
7
|
-
* Uses @faker-js/faker for data generation with intelligent field detection.
|
|
8
|
-
*/ export class DataGenerator {
|
|
9
|
-
/**
|
|
10
|
-
* Generate value for a single column based on its metadata
|
|
11
|
-
*/ generateValue(column) {
|
|
12
|
-
if (column.isGenerated) return undefined;
|
|
13
|
-
// Handle nullable fields (50% chance of null)
|
|
14
|
-
if (column.isNullable && faker.datatype.boolean()) return null;
|
|
15
|
-
// Check for enum
|
|
16
|
-
if (column.enum && column.enum.length > 0) {
|
|
17
|
-
return faker.helpers.arrayElement(column.enum);
|
|
18
|
-
}
|
|
19
|
-
// Generate based on field pattern
|
|
20
|
-
const pattern = detectFieldPattern(column);
|
|
21
|
-
if (pattern) {
|
|
22
|
-
const value = this.generateByPattern(pattern, column);
|
|
23
|
-
if (value !== undefined) return value;
|
|
24
|
-
}
|
|
25
|
-
// Generate based on column type
|
|
26
|
-
return this.generateByType(column);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Generate entity object from column definitions
|
|
30
|
-
*/ generateEntity(columns) {
|
|
31
|
-
const entity = {};
|
|
32
|
-
for (const column of columns){
|
|
33
|
-
const value = this.generateValue(column);
|
|
34
|
-
if (value !== undefined) {
|
|
35
|
-
entity[column.propertyName] = value;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return entity;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Generate value based on detected field pattern
|
|
42
|
-
*/ generateByPattern(pattern, column) {
|
|
43
|
-
switch(pattern){
|
|
44
|
-
case 'skip':
|
|
45
|
-
return undefined;
|
|
46
|
-
case 'null':
|
|
47
|
-
return null;
|
|
48
|
-
case 'boolean':
|
|
49
|
-
return faker.datatype.boolean();
|
|
50
|
-
case 'token':
|
|
51
|
-
return faker.string.alphanumeric(getTokenLength(column));
|
|
52
|
-
case 'firstName':
|
|
53
|
-
return faker.person.firstName();
|
|
54
|
-
case 'lastName':
|
|
55
|
-
return faker.person.lastName();
|
|
56
|
-
case 'fullName':
|
|
57
|
-
return faker.person.fullName();
|
|
58
|
-
case 'email':
|
|
59
|
-
return faker.internet.email().toLowerCase();
|
|
60
|
-
case 'phone':
|
|
61
|
-
return faker.phone.number();
|
|
62
|
-
case 'address':
|
|
63
|
-
return faker.location.streetAddress();
|
|
64
|
-
case 'street':
|
|
65
|
-
return faker.location.street();
|
|
66
|
-
case 'city':
|
|
67
|
-
return faker.location.city();
|
|
68
|
-
case 'state':
|
|
69
|
-
return faker.location.state();
|
|
70
|
-
case 'country':
|
|
71
|
-
return faker.location.country();
|
|
72
|
-
case 'zipCode':
|
|
73
|
-
return faker.location.zipCode();
|
|
74
|
-
case 'url':
|
|
75
|
-
return faker.internet.url();
|
|
76
|
-
case 'domain':
|
|
77
|
-
return faker.internet.domainName();
|
|
78
|
-
case 'slug':
|
|
79
|
-
return faker.helpers.slugify(faker.lorem.words(3)).toLowerCase();
|
|
80
|
-
case 'description':
|
|
81
|
-
return faker.lorem.paragraph();
|
|
82
|
-
case 'summary':
|
|
83
|
-
return faker.lorem.sentence();
|
|
84
|
-
case 'content':
|
|
85
|
-
return faker.lorem.paragraphs(3);
|
|
86
|
-
case 'title':
|
|
87
|
-
return faker.lorem.sentence();
|
|
88
|
-
case 'username':
|
|
89
|
-
return faker.internet.username();
|
|
90
|
-
case 'password':
|
|
91
|
-
return '$2b$12$dummy.hashed.password.value';
|
|
92
|
-
case 'birthdate':
|
|
93
|
-
return faker.date.birthdate({
|
|
94
|
-
min: 18,
|
|
95
|
-
max: 80,
|
|
96
|
-
mode: 'age'
|
|
97
|
-
});
|
|
98
|
-
case 'futureDate':
|
|
99
|
-
return faker.date.future();
|
|
100
|
-
case 'recentDateOrNull':
|
|
101
|
-
return faker.datatype.boolean() ? faker.date.recent() : null;
|
|
102
|
-
case 'serial':
|
|
103
|
-
return faker.number.int({
|
|
104
|
-
min: 1,
|
|
105
|
-
max: 100
|
|
106
|
-
});
|
|
107
|
-
case 'company':
|
|
108
|
-
return faker.company.name();
|
|
109
|
-
case 'emailProvider':
|
|
110
|
-
return faker.helpers.arrayElement([
|
|
111
|
-
'smtp',
|
|
112
|
-
'sendgrid',
|
|
113
|
-
'mailgun',
|
|
114
|
-
'ses',
|
|
115
|
-
'postmark'
|
|
116
|
-
]);
|
|
117
|
-
case 'formAccessType':
|
|
118
|
-
return faker.helpers.arrayElement([
|
|
119
|
-
'public',
|
|
120
|
-
'authenticated',
|
|
121
|
-
'permission'
|
|
122
|
-
]);
|
|
123
|
-
default:
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Generate value based on column type
|
|
129
|
-
*/ generateByType(column) {
|
|
130
|
-
const category = detectTypeCategory(column.type);
|
|
131
|
-
switch(category){
|
|
132
|
-
case 'string':
|
|
133
|
-
return this.generateString(column);
|
|
134
|
-
case 'integer':
|
|
135
|
-
return faker.number.int({
|
|
136
|
-
min: 1,
|
|
137
|
-
max: 1000
|
|
138
|
-
});
|
|
139
|
-
case 'decimal':
|
|
140
|
-
return faker.number.float({
|
|
141
|
-
min: 0,
|
|
142
|
-
max: 10000,
|
|
143
|
-
fractionDigits: column.scale || 2
|
|
144
|
-
});
|
|
145
|
-
case 'boolean':
|
|
146
|
-
return faker.datatype.boolean();
|
|
147
|
-
case 'date':
|
|
148
|
-
case 'timestamp':
|
|
149
|
-
return faker.date.recent({
|
|
150
|
-
days: 30
|
|
151
|
-
});
|
|
152
|
-
case 'time':
|
|
153
|
-
return faker.date.recent().toTimeString().split(' ')[0];
|
|
154
|
-
case 'uuid':
|
|
155
|
-
return faker.string.uuid();
|
|
156
|
-
case 'json':
|
|
157
|
-
return {
|
|
158
|
-
key1: faker.lorem.word(),
|
|
159
|
-
key2: faker.number.int({
|
|
160
|
-
min: 1,
|
|
161
|
-
max: 100
|
|
162
|
-
})
|
|
163
|
-
};
|
|
164
|
-
case 'array':
|
|
165
|
-
return [
|
|
166
|
-
faker.lorem.word(),
|
|
167
|
-
faker.lorem.word()
|
|
168
|
-
];
|
|
169
|
-
default:
|
|
170
|
-
return faker.lorem.word();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Generate string value respecting length constraints
|
|
175
|
-
*/ generateString(column) {
|
|
176
|
-
const maxLength = typeof column.length === 'number' ? column.length : 255;
|
|
177
|
-
const category = getStringLengthCategory(column.length);
|
|
178
|
-
switch(category){
|
|
179
|
-
case 'word':
|
|
180
|
-
return faker.lorem.word().substring(0, maxLength);
|
|
181
|
-
case 'sentence':
|
|
182
|
-
return faker.lorem.sentence().substring(0, maxLength);
|
|
183
|
-
case 'paragraph':
|
|
184
|
-
return faker.lorem.paragraph().substring(0, maxLength);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Generate related entity ID (for foreign keys)
|
|
189
|
-
*/ generateRelationId(relatedEntities) {
|
|
190
|
-
if (relatedEntities.length === 0) return null;
|
|
191
|
-
return faker.helpers.arrayElement(relatedEntities).id;
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Generate multiple related entity IDs (for many-to-many)
|
|
195
|
-
*/ generateRelationIds(relatedEntities, min = 1, max = 3) {
|
|
196
|
-
if (relatedEntities.length === 0) return [];
|
|
197
|
-
const count = faker.number.int({
|
|
198
|
-
min,
|
|
199
|
-
max: Math.min(max, relatedEntities.length)
|
|
200
|
-
});
|
|
201
|
-
return faker.helpers.arrayElements(relatedEntities, count).map((e)=>e.id);
|
|
202
|
-
}
|
|
203
|
-
constructor(locale = 'en'){
|
|
204
|
-
faker.setDefaultRefDate(new Date());
|
|
205
|
-
if (locale !== 'en') {
|
|
206
|
-
// @ts-expect-error - faker locale property access
|
|
207
|
-
faker.locale = locale;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
function _define_property(obj, key, value) {
|
|
2
|
-
if (key in obj) {
|
|
3
|
-
Object.defineProperty(obj, key, {
|
|
4
|
-
value: value,
|
|
5
|
-
enumerable: true,
|
|
6
|
-
configurable: true,
|
|
7
|
-
writable: true
|
|
8
|
-
});
|
|
9
|
-
} else {
|
|
10
|
-
obj[key] = value;
|
|
11
|
-
}
|
|
12
|
-
return obj;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Entity Reader Service
|
|
16
|
-
*
|
|
17
|
-
* Extracts metadata from TypeORM entities for seed data generation.
|
|
18
|
-
* Discovers columns, types, constraints, and relationships.
|
|
19
|
-
*
|
|
20
|
-
* Usage:
|
|
21
|
-
* ```typescript
|
|
22
|
-
* const reader = new EntityReader(dataSource);
|
|
23
|
-
* const entities = reader.getAllEntities();
|
|
24
|
-
* const userInfo = reader.getEntityInfo('User');
|
|
25
|
-
* ```
|
|
26
|
-
*/ export class EntityReader {
|
|
27
|
-
/**
|
|
28
|
-
* Get all entity metadata from DataSource
|
|
29
|
-
*/ getAllEntities() {
|
|
30
|
-
return this.dataSource.entityMetadatas;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Get detailed entity information
|
|
34
|
-
* @param entityName Entity class name or table name
|
|
35
|
-
*/ getEntityInfo(entityName) {
|
|
36
|
-
const metadata = this.findEntityMetadata(entityName);
|
|
37
|
-
if (!metadata) {
|
|
38
|
-
throw new Error(`Entity ${entityName} not found in DataSource`);
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
name: metadata.name,
|
|
42
|
-
tableName: metadata.tableName,
|
|
43
|
-
columns: this.extractColumns(metadata),
|
|
44
|
-
relations: this.extractRelations(metadata),
|
|
45
|
-
hasSoftDelete: metadata.deleteDateColumn !== undefined,
|
|
46
|
-
hasCompany: this.hasCompanyColumn(metadata)
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Get seeding order based on foreign key dependencies
|
|
51
|
-
* Returns entity names sorted by dependency (independent entities first)
|
|
52
|
-
*/ getSeedingOrder(skipEntities = []) {
|
|
53
|
-
const entities = this.getAllEntities().filter((e)=>!skipEntities.includes(e.name) && !skipEntities.includes(e.tableName));
|
|
54
|
-
const graph = this.buildDependencyGraph(entities);
|
|
55
|
-
return this.topologicalSort(graph);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Find entity metadata by name or table name
|
|
59
|
-
*/ findEntityMetadata(entityName) {
|
|
60
|
-
return this.getAllEntities().find((e)=>e.name === entityName || e.tableName === entityName);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Extract column information from entity metadata
|
|
64
|
-
*/ extractColumns(metadata) {
|
|
65
|
-
return metadata.columns.map((col)=>({
|
|
66
|
-
name: col.databaseName,
|
|
67
|
-
propertyName: col.propertyName,
|
|
68
|
-
type: this.getColumnTypeString(col.type),
|
|
69
|
-
length: col.length,
|
|
70
|
-
isNullable: col.isNullable,
|
|
71
|
-
isUnique: col.isUnique === true || false,
|
|
72
|
-
isPrimary: col.isPrimary,
|
|
73
|
-
isGenerated: col.isGenerated,
|
|
74
|
-
default: col.default,
|
|
75
|
-
enum: col.enum ? Object.values(col.enum) : undefined,
|
|
76
|
-
precision: col.precision ?? undefined,
|
|
77
|
-
scale: col.scale ?? undefined
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Extract relation information from entity metadata
|
|
82
|
-
*/ extractRelations(metadata) {
|
|
83
|
-
return metadata.relations.map((rel)=>({
|
|
84
|
-
propertyName: rel.propertyName,
|
|
85
|
-
type: rel.relationType,
|
|
86
|
-
targetEntity: rel.inverseEntityMetadata.name,
|
|
87
|
-
isNullable: rel.isNullable,
|
|
88
|
-
joinColumnName: rel.joinColumns?.[0]?.databaseName
|
|
89
|
-
}));
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Check if entity has company-related columns
|
|
93
|
-
*/ hasCompanyColumn(metadata) {
|
|
94
|
-
return metadata.columns.some((col)=>col.propertyName === 'companyId' || col.databaseName === 'company_id');
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Convert TypeORM column type to string
|
|
98
|
-
*/ getColumnTypeString(type) {
|
|
99
|
-
if (typeof type === 'string') {
|
|
100
|
-
return type;
|
|
101
|
-
}
|
|
102
|
-
if (typeof type === 'function') {
|
|
103
|
-
return type.name.toLowerCase();
|
|
104
|
-
}
|
|
105
|
-
return 'unknown';
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Build dependency graph for entities
|
|
109
|
-
*/ buildDependencyGraph(entities) {
|
|
110
|
-
const graph = new Map();
|
|
111
|
-
for (const entity of entities){
|
|
112
|
-
const dependencies = [];
|
|
113
|
-
for (const relation of entity.relations){
|
|
114
|
-
if (relation.relationType === 'many-to-one' && !relation.isNullable) {
|
|
115
|
-
dependencies.push(relation.inverseEntityMetadata.name);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
graph.set(entity.name, dependencies);
|
|
119
|
-
}
|
|
120
|
-
return graph;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Topological sort for dependency resolution
|
|
124
|
-
*/ topologicalSort(graph) {
|
|
125
|
-
const sorted = [];
|
|
126
|
-
const visited = new Set();
|
|
127
|
-
const visiting = new Set();
|
|
128
|
-
const visit = (node)=>{
|
|
129
|
-
if (visited.has(node)) return;
|
|
130
|
-
if (visiting.has(node)) {
|
|
131
|
-
throw new Error(`Circular dependency detected involving ${node}`);
|
|
132
|
-
}
|
|
133
|
-
visiting.add(node);
|
|
134
|
-
const dependencies = graph.get(node) || [];
|
|
135
|
-
for (const dep of dependencies){
|
|
136
|
-
if (graph.has(dep)) {
|
|
137
|
-
visit(dep);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
visiting.delete(node);
|
|
141
|
-
visited.add(node);
|
|
142
|
-
sorted.push(node);
|
|
143
|
-
};
|
|
144
|
-
for (const node of graph.keys()){
|
|
145
|
-
visit(node);
|
|
146
|
-
}
|
|
147
|
-
return sorted;
|
|
148
|
-
}
|
|
149
|
-
constructor(dataSource){
|
|
150
|
-
_define_property(this, "dataSource", void 0);
|
|
151
|
-
this.dataSource = dataSource;
|
|
152
|
-
}
|
|
153
|
-
}
|