@doviui/dev-db 0.1.0
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 +7 -0
- package/README.md +532 -0
- package/index.ts +39 -0
- package/package.json +49 -0
- package/src/field-builder.ts +188 -0
- package/src/generator.ts +360 -0
- package/src/schema-builder.ts +213 -0
- package/src/types.ts +107 -0
- package/src/validator.ts +231 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { Faker } from '@faker-js/faker';
|
|
2
|
+
import type { FieldConfig, FieldBuilderLike } from './types';
|
|
3
|
+
|
|
4
|
+
export type Generator = string | ((faker: Faker) => any);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builder class for defining database field configurations with a fluent API.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const field = new FieldBuilder('varchar', 255)
|
|
12
|
+
* .unique()
|
|
13
|
+
* .notNull()
|
|
14
|
+
* .generate('internet.email');
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class FieldBuilder implements FieldBuilderLike {
|
|
18
|
+
protected config: FieldConfig;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new FieldBuilder instance.
|
|
22
|
+
*
|
|
23
|
+
* @param type - The SQL-like type of the field
|
|
24
|
+
* @param length - Optional length for string types
|
|
25
|
+
* @param precision - Optional precision for numeric types
|
|
26
|
+
* @param scale - Optional scale for numeric types
|
|
27
|
+
*/
|
|
28
|
+
constructor(type: string, length?: number, precision?: number, scale?: number) {
|
|
29
|
+
this.config = {
|
|
30
|
+
type,
|
|
31
|
+
length,
|
|
32
|
+
precision,
|
|
33
|
+
scale,
|
|
34
|
+
nullable: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Marks this field as a primary key.
|
|
40
|
+
* Automatically sets notNull to true and nullable to false.
|
|
41
|
+
*
|
|
42
|
+
* @returns The builder instance for chaining
|
|
43
|
+
*/
|
|
44
|
+
primaryKey(): this {
|
|
45
|
+
this.config.primaryKey = true;
|
|
46
|
+
this.config.notNull = true;
|
|
47
|
+
this.config.nullable = false;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Enforces uniqueness constraint on this field.
|
|
53
|
+
* All generated values will be unique.
|
|
54
|
+
*
|
|
55
|
+
* @returns The builder instance for chaining
|
|
56
|
+
*/
|
|
57
|
+
unique(): this {
|
|
58
|
+
this.config.unique = true;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Marks this field as NOT NULL.
|
|
64
|
+
* Generated values will never be null.
|
|
65
|
+
*
|
|
66
|
+
* @returns The builder instance for chaining
|
|
67
|
+
*/
|
|
68
|
+
notNull(): this {
|
|
69
|
+
this.config.notNull = true;
|
|
70
|
+
this.config.nullable = false;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Marks this field as nullable.
|
|
76
|
+
* Generated values may occasionally be null (10% chance by default).
|
|
77
|
+
*
|
|
78
|
+
* @returns The builder instance for chaining
|
|
79
|
+
*/
|
|
80
|
+
nullable(): this {
|
|
81
|
+
this.config.nullable = true;
|
|
82
|
+
this.config.notNull = false;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sets a default value for this field.
|
|
88
|
+
*
|
|
89
|
+
* @param value - The default value. Use 'now' for current timestamp.
|
|
90
|
+
* @returns The builder instance for chaining
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* t.boolean().default(true)
|
|
95
|
+
* t.timestamptz().default('now')
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
default(value: any): this {
|
|
99
|
+
this.config.default = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sets the minimum value for numeric fields.
|
|
105
|
+
*
|
|
106
|
+
* @param value - Minimum allowed value
|
|
107
|
+
* @returns The builder instance for chaining
|
|
108
|
+
*/
|
|
109
|
+
min(value: number): this {
|
|
110
|
+
this.config.min = value;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sets the maximum value for numeric fields.
|
|
116
|
+
*
|
|
117
|
+
* @param value - Maximum allowed value
|
|
118
|
+
* @returns The builder instance for chaining
|
|
119
|
+
*/
|
|
120
|
+
max(value: number): this {
|
|
121
|
+
this.config.max = value;
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Restricts field values to a specific set of allowed values.
|
|
127
|
+
*
|
|
128
|
+
* @param values - Array of allowed values
|
|
129
|
+
* @returns The builder instance for chaining
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* t.varchar(20).enum(['draft', 'published', 'archived'])
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
enum(values: any[]): this {
|
|
137
|
+
this.config.enum = values;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sets a custom generator for field values.
|
|
143
|
+
*
|
|
144
|
+
* @param generator - Either a Faker.js method path or a custom function
|
|
145
|
+
* @returns The builder instance for chaining
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* // Using Faker.js method
|
|
150
|
+
* t.varchar(100).generate('internet.email')
|
|
151
|
+
*
|
|
152
|
+
* // Using custom function
|
|
153
|
+
* t.varchar(20).generate((faker) =>
|
|
154
|
+
* faker.helpers.arrayElement(['red', 'blue', 'green'])
|
|
155
|
+
* )
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
generate(generator: Generator): this {
|
|
159
|
+
this.config.generator = generator;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Converts the builder to a plain FieldConfig object.
|
|
165
|
+
*
|
|
166
|
+
* @returns The field configuration
|
|
167
|
+
*/
|
|
168
|
+
toConfig(): FieldConfig {
|
|
169
|
+
return this.config;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Builder class for foreign key fields.
|
|
175
|
+
* Extends FieldBuilder with foreign key-specific configuration.
|
|
176
|
+
*/
|
|
177
|
+
export class ForeignKeyBuilder extends FieldBuilder {
|
|
178
|
+
/**
|
|
179
|
+
* Creates a new foreign key field builder.
|
|
180
|
+
*
|
|
181
|
+
* @param table - The referenced table name
|
|
182
|
+
* @param column - The referenced column name
|
|
183
|
+
*/
|
|
184
|
+
constructor(table: string, column: string) {
|
|
185
|
+
super('foreignKey');
|
|
186
|
+
this.config.foreignKey = { table, column };
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/generator.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { Schema, FieldConfig, TableConfig } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for configuring the mock data generator.
|
|
6
|
+
*/
|
|
7
|
+
export interface GeneratorOptions {
|
|
8
|
+
/** Directory where generated JSON files will be saved (default: './mock-data') */
|
|
9
|
+
outputDir?: string;
|
|
10
|
+
|
|
11
|
+
/** Random seed for reproducible data generation */
|
|
12
|
+
seed?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generated data indexed by table name.
|
|
17
|
+
*/
|
|
18
|
+
interface GeneratedData {
|
|
19
|
+
[tableName: string]: any[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generates realistic mock data from schema definitions.
|
|
24
|
+
*
|
|
25
|
+
* Features:
|
|
26
|
+
* - Topological sorting to handle foreign key dependencies
|
|
27
|
+
* - Unique value generation with retry logic
|
|
28
|
+
* - Custom generators via Faker.js or custom functions
|
|
29
|
+
* - Enum support
|
|
30
|
+
* - Min/max constraints for numeric types
|
|
31
|
+
* - Automatic foreign key resolution
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const generator = new MockDataGenerator(schema, {
|
|
36
|
+
* outputDir: './mock-data',
|
|
37
|
+
* seed: 42 // Optional: for reproducible data
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* const data = await generator.generate();
|
|
41
|
+
* console.log(`Generated ${data.User.length} users`);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export class MockDataGenerator {
|
|
45
|
+
private schema: Schema;
|
|
46
|
+
private options: GeneratorOptions;
|
|
47
|
+
private generatedData: GeneratedData = {};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new mock data generator.
|
|
51
|
+
*
|
|
52
|
+
* @param schema - The schema definition
|
|
53
|
+
* @param options - Generator options
|
|
54
|
+
*/
|
|
55
|
+
constructor(schema: Schema, options: GeneratorOptions = {}) {
|
|
56
|
+
this.schema = schema;
|
|
57
|
+
this.options = {
|
|
58
|
+
outputDir: options.outputDir || './mock-data',
|
|
59
|
+
seed: options.seed,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (this.options.seed !== undefined) {
|
|
63
|
+
faker.seed(this.options.seed);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generates mock data for all tables in the schema.
|
|
69
|
+
*
|
|
70
|
+
* Tables are processed in dependency order (based on foreign keys).
|
|
71
|
+
* Generated data is written to JSON files in the output directory.
|
|
72
|
+
*
|
|
73
|
+
* @returns Promise resolving to the generated data indexed by table name
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const data = await generator.generate();
|
|
78
|
+
* // Files created: ./mock-data/User.json, ./mock-data/Post.json, etc.
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
async generate(): Promise<GeneratedData> {
|
|
82
|
+
// Topologically sort tables based on foreign key dependencies
|
|
83
|
+
const sortedTables = this.topologicalSort();
|
|
84
|
+
|
|
85
|
+
// Generate data for each table in order
|
|
86
|
+
for (const tableName of sortedTables) {
|
|
87
|
+
const tableConfig = this.schema[tableName];
|
|
88
|
+
if (!tableConfig) continue;
|
|
89
|
+
|
|
90
|
+
const count = tableConfig.$count || 10;
|
|
91
|
+
|
|
92
|
+
this.generatedData[tableName] = this.generateTableData(tableName, tableConfig, count);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Write data to files
|
|
96
|
+
await this.writeDataToFiles();
|
|
97
|
+
|
|
98
|
+
return this.generatedData;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private topologicalSort(): string[] {
|
|
102
|
+
const tables = Object.keys(this.schema);
|
|
103
|
+
const visited = new Set<string>();
|
|
104
|
+
const result: string[] = [];
|
|
105
|
+
|
|
106
|
+
const visit = (tableName: string) => {
|
|
107
|
+
if (visited.has(tableName)) return;
|
|
108
|
+
visited.add(tableName);
|
|
109
|
+
|
|
110
|
+
const tableConfig = this.schema[tableName];
|
|
111
|
+
if (!tableConfig) return;
|
|
112
|
+
|
|
113
|
+
const fields = Object.entries(tableConfig).filter(([key]) => key !== '$count');
|
|
114
|
+
|
|
115
|
+
// Visit dependencies first
|
|
116
|
+
for (const [_, field] of fields) {
|
|
117
|
+
const fieldConfig = this.toFieldConfig(field);
|
|
118
|
+
if (fieldConfig?.foreignKey) {
|
|
119
|
+
const refTable = fieldConfig.foreignKey.table;
|
|
120
|
+
if (refTable !== tableName) { // Avoid self-references
|
|
121
|
+
visit(refTable);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
result.push(tableName);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const tableName of tables) {
|
|
130
|
+
visit(tableName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private generateTableData(tableName: string, tableConfig: TableConfig, count: number): any[] {
|
|
137
|
+
const records: any[] = [];
|
|
138
|
+
const uniqueValues = new Map<string, Set<any>>();
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < count; i++) {
|
|
141
|
+
const record: any = {};
|
|
142
|
+
const fields = Object.entries(tableConfig).filter(([key]) => key !== '$count');
|
|
143
|
+
|
|
144
|
+
for (const [fieldName, field] of fields) {
|
|
145
|
+
const fieldConfig = this.toFieldConfig(field);
|
|
146
|
+
if (fieldConfig) {
|
|
147
|
+
record[fieldName] = this.generateFieldValue(
|
|
148
|
+
fieldName,
|
|
149
|
+
fieldConfig,
|
|
150
|
+
i,
|
|
151
|
+
uniqueValues,
|
|
152
|
+
tableName
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
records.push(record);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return records;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private toFieldConfig(field: any): FieldConfig | null {
|
|
164
|
+
if (!field) return null;
|
|
165
|
+
if (typeof field === 'object' && 'toConfig' in field) {
|
|
166
|
+
return field.toConfig();
|
|
167
|
+
}
|
|
168
|
+
if (typeof field === 'object' && 'type' in field) {
|
|
169
|
+
return field;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private generateFieldValue(
|
|
175
|
+
fieldName: string,
|
|
176
|
+
fieldConfig: FieldConfig,
|
|
177
|
+
index: number,
|
|
178
|
+
uniqueValues: Map<string, Set<any>>,
|
|
179
|
+
tableName: string
|
|
180
|
+
): any {
|
|
181
|
+
// Handle default values
|
|
182
|
+
if (fieldConfig.default !== undefined) {
|
|
183
|
+
if (fieldConfig.default === 'now') {
|
|
184
|
+
return new Date().toISOString();
|
|
185
|
+
}
|
|
186
|
+
return fieldConfig.default;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle foreign keys
|
|
190
|
+
if (fieldConfig.foreignKey) {
|
|
191
|
+
const refTable = fieldConfig.foreignKey.table;
|
|
192
|
+
const refColumn = fieldConfig.foreignKey.column;
|
|
193
|
+
const refData = this.generatedData[refTable];
|
|
194
|
+
|
|
195
|
+
if (!refData || refData.length === 0) {
|
|
196
|
+
if (fieldConfig.nullable) return null;
|
|
197
|
+
throw new Error(`Cannot generate foreign key for ${tableName}.${fieldName}: no data in ${refTable}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const randomRecord = refData[Math.floor(Math.random() * refData.length)];
|
|
201
|
+
return randomRecord[refColumn];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Handle serial/bigserial auto-increment
|
|
205
|
+
if (fieldConfig.type === 'serial' || fieldConfig.type === 'bigserial') {
|
|
206
|
+
return index + 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Handle nullable fields (but not if they have enum, min/max constraints, or custom generators)
|
|
210
|
+
if (
|
|
211
|
+
fieldConfig.nullable &&
|
|
212
|
+
!fieldConfig.notNull &&
|
|
213
|
+
!fieldConfig.enum &&
|
|
214
|
+
!fieldConfig.generator &&
|
|
215
|
+
fieldConfig.min === undefined &&
|
|
216
|
+
fieldConfig.max === undefined &&
|
|
217
|
+
Math.random() < 0.1
|
|
218
|
+
) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Generate value with uniqueness check
|
|
223
|
+
let value: any;
|
|
224
|
+
let attempts = 0;
|
|
225
|
+
const maxAttempts = 1000;
|
|
226
|
+
|
|
227
|
+
do {
|
|
228
|
+
value = this.generateValueByType(fieldConfig);
|
|
229
|
+
attempts++;
|
|
230
|
+
|
|
231
|
+
if (attempts >= maxAttempts) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Could not generate unique value for ${tableName}.${fieldName} after ${maxAttempts} attempts`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
} while (
|
|
237
|
+
fieldConfig.unique &&
|
|
238
|
+
uniqueValues.get(fieldName)?.has(JSON.stringify(value))
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Track unique values
|
|
242
|
+
if (fieldConfig.unique) {
|
|
243
|
+
if (!uniqueValues.has(fieldName)) {
|
|
244
|
+
uniqueValues.set(fieldName, new Set());
|
|
245
|
+
}
|
|
246
|
+
uniqueValues.get(fieldName)!.add(JSON.stringify(value));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private generateValueByType(fieldConfig: FieldConfig): any {
|
|
253
|
+
// Use custom generator if provided
|
|
254
|
+
if (fieldConfig.generator) {
|
|
255
|
+
if (typeof fieldConfig.generator === 'string') {
|
|
256
|
+
return this.callFakerMethod(fieldConfig.generator);
|
|
257
|
+
} else {
|
|
258
|
+
return fieldConfig.generator(faker);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Use enum if provided
|
|
263
|
+
if (fieldConfig.enum && fieldConfig.enum.length > 0) {
|
|
264
|
+
return fieldConfig.enum[Math.floor(Math.random() * fieldConfig.enum.length)];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Generate based on type
|
|
268
|
+
switch (fieldConfig.type) {
|
|
269
|
+
case 'uuid':
|
|
270
|
+
return faker.string.uuid();
|
|
271
|
+
|
|
272
|
+
case 'boolean':
|
|
273
|
+
return faker.datatype.boolean();
|
|
274
|
+
|
|
275
|
+
case 'integer':
|
|
276
|
+
case 'bigint':
|
|
277
|
+
case 'smallint':
|
|
278
|
+
return faker.number.int({
|
|
279
|
+
min: fieldConfig.min ?? 1,
|
|
280
|
+
max: fieldConfig.max ?? 100000,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
case 'decimal':
|
|
284
|
+
case 'numeric':
|
|
285
|
+
case 'real':
|
|
286
|
+
case 'double':
|
|
287
|
+
return faker.number.float({
|
|
288
|
+
min: fieldConfig.min ?? 0,
|
|
289
|
+
max: fieldConfig.max ?? 10000,
|
|
290
|
+
fractionDigits: fieldConfig.scale ?? 2,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
case 'varchar':
|
|
294
|
+
case 'char':
|
|
295
|
+
return faker.lorem.word().substring(0, fieldConfig.length || 255);
|
|
296
|
+
|
|
297
|
+
case 'text':
|
|
298
|
+
return faker.lorem.paragraph();
|
|
299
|
+
|
|
300
|
+
case 'date':
|
|
301
|
+
return faker.date.past().toISOString().split('T')[0];
|
|
302
|
+
|
|
303
|
+
case 'time':
|
|
304
|
+
return faker.date.recent().toISOString().split('T')[1]?.split('.')[0] ?? '00:00:00';
|
|
305
|
+
|
|
306
|
+
case 'timestamp':
|
|
307
|
+
case 'timestamptz':
|
|
308
|
+
return faker.date.recent().toISOString();
|
|
309
|
+
|
|
310
|
+
case 'json':
|
|
311
|
+
case 'jsonb':
|
|
312
|
+
return { data: faker.lorem.word() };
|
|
313
|
+
|
|
314
|
+
default:
|
|
315
|
+
return faker.lorem.word();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private callFakerMethod(method: string): any {
|
|
320
|
+
const parts = method.split('.');
|
|
321
|
+
let current: any = faker;
|
|
322
|
+
|
|
323
|
+
for (const part of parts) {
|
|
324
|
+
current = current[part];
|
|
325
|
+
if (current === undefined) {
|
|
326
|
+
throw new Error(`Faker method not found: ${method}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (typeof current === 'function') {
|
|
331
|
+
return current();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return current;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async writeDataToFiles(): Promise<void> {
|
|
338
|
+
const outputDir = this.options.outputDir ?? './mock-data';
|
|
339
|
+
|
|
340
|
+
// Create output directory if it doesn't exist
|
|
341
|
+
await Bun.write(`${outputDir}/.gitkeep`, '');
|
|
342
|
+
|
|
343
|
+
// Write each table to a JSON file
|
|
344
|
+
for (const [tableName, data] of Object.entries(this.generatedData)) {
|
|
345
|
+
const filePath = `${outputDir}/${tableName}.json`;
|
|
346
|
+
await Bun.write(filePath, JSON.stringify(data, null, 2));
|
|
347
|
+
console.log(` Generated ${data.length} records for ${tableName} -> ${filePath}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Gets the generated data without triggering generation.
|
|
353
|
+
* Must be called after generate().
|
|
354
|
+
*
|
|
355
|
+
* @returns The generated data indexed by table name
|
|
356
|
+
*/
|
|
357
|
+
getGeneratedData(): GeneratedData {
|
|
358
|
+
return this.generatedData;
|
|
359
|
+
}
|
|
360
|
+
}
|