@doviui/dev-db 0.2.1 → 0.3.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/README.md +171 -4
- package/cli.ts +18 -0
- package/index.ts +2 -0
- package/package.json +1 -1
- package/src/field-builder.ts +24 -1
- package/src/generator.ts +22 -1
- package/src/schema-builder.ts +45 -8
- package/src/types-generator.ts +250 -0
- package/src/types.ts +10 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ dev-db eliminates the friction of setting up databases during development. Defin
|
|
|
15
15
|
## Key Features
|
|
16
16
|
|
|
17
17
|
- **Type-Safe Schema Definition** - Fluent TypeScript API with full IntelliSense support
|
|
18
|
+
- **TypeScript Types Generation** - Auto-generate type definitions from schemas for type-safe data consumption
|
|
18
19
|
- **Automatic Relationship Resolution** - Foreign keys handled intelligently with topological sorting
|
|
19
20
|
- **Production-Quality Mock Data** - Powered by Faker.js for realistic, diverse datasets
|
|
20
21
|
- **Built-In Validation** - Detect circular dependencies, missing tables, and constraint conflicts before generation
|
|
@@ -145,12 +146,73 @@ function getPostsByUserId(userId: number) {
|
|
|
145
146
|
app.get('/users/:id', (req, res) => {
|
|
146
147
|
const user = getUserById(parseInt(req.params.id))
|
|
147
148
|
if (!user) return res.status(404).json({ error: 'User not found' })
|
|
148
|
-
|
|
149
|
+
|
|
149
150
|
const userPosts = getPostsByUserId(user.id)
|
|
150
151
|
res.json({ ...user, posts: userPosts })
|
|
151
152
|
})
|
|
152
153
|
```
|
|
153
154
|
|
|
155
|
+
### Type-Safe Data Consumption
|
|
156
|
+
|
|
157
|
+
Generate TypeScript type definitions from your schemas for full type safety when consuming the mock data:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Generate types alongside JSON data
|
|
161
|
+
bunx @doviui/dev-db schema.ts --types
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This creates a `types.ts` file with interfaces matching your schema:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// mock-data/types.ts (auto-generated)
|
|
168
|
+
export interface User {
|
|
169
|
+
id: number;
|
|
170
|
+
username: string;
|
|
171
|
+
email: string;
|
|
172
|
+
age: number | null;
|
|
173
|
+
created_at?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface Post {
|
|
177
|
+
id: number;
|
|
178
|
+
user_id: number; // Correctly typed based on User.id
|
|
179
|
+
title: string;
|
|
180
|
+
content: string | null;
|
|
181
|
+
created_at?: string;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Import and use the types in your application:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// server.ts
|
|
189
|
+
import users from './mock-data/User.json'
|
|
190
|
+
import posts from './mock-data/Post.json'
|
|
191
|
+
import type { User, Post } from './mock-data/types'
|
|
192
|
+
|
|
193
|
+
// Fully type-safe!
|
|
194
|
+
function getUserById(id: number): User | undefined {
|
|
195
|
+
return users.find(u => u.id === id)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getPostsByUserId(userId: number): Post[] {
|
|
199
|
+
return posts.filter(p => p.user_id === userId)
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Programmatic API:**
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { TypesGenerator } from '@doviui/dev-db'
|
|
207
|
+
|
|
208
|
+
const typesGenerator = new TypesGenerator(schema, {
|
|
209
|
+
outputDir: './mock-data',
|
|
210
|
+
fileName: 'types.ts' // Optional, defaults to 'types.ts'
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await typesGenerator.generate()
|
|
214
|
+
```
|
|
215
|
+
|
|
154
216
|
## API Reference
|
|
155
217
|
|
|
156
218
|
### Data Types
|
|
@@ -189,8 +251,20 @@ t.timestamptz() // Date and time with timezone
|
|
|
189
251
|
```typescript
|
|
190
252
|
t.boolean() // True/false
|
|
191
253
|
t.uuid() // UUID v4
|
|
192
|
-
|
|
193
|
-
|
|
254
|
+
|
|
255
|
+
// JSON types with optional structured schemas
|
|
256
|
+
t.json() // JSON object (unstructured)
|
|
257
|
+
t.jsonb() // JSON binary (unstructured)
|
|
258
|
+
|
|
259
|
+
// Structured JSON with type-safe schema
|
|
260
|
+
t.json({
|
|
261
|
+
id: t.integer(),
|
|
262
|
+
name: t.varchar(100),
|
|
263
|
+
preferences: t.json({
|
|
264
|
+
theme: t.varchar(20),
|
|
265
|
+
notifications: t.boolean()
|
|
266
|
+
})
|
|
267
|
+
})
|
|
194
268
|
```
|
|
195
269
|
|
|
196
270
|
#### Relationships
|
|
@@ -332,6 +406,95 @@ Schema validation failed:
|
|
|
332
406
|
Post.user_id: Foreign key references non-existent table 'User'
|
|
333
407
|
```
|
|
334
408
|
|
|
409
|
+
### Structured JSON Fields
|
|
410
|
+
|
|
411
|
+
Define type-safe JSON schemas for complex nested data structures. This generates proper TypeScript types instead of `any`, and creates realistic structured data:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { t } from '@doviui/dev-db'
|
|
415
|
+
|
|
416
|
+
export default {
|
|
417
|
+
User: {
|
|
418
|
+
$count: 100,
|
|
419
|
+
id: t.bigserial().primaryKey(),
|
|
420
|
+
email: t.varchar(255).unique().notNull(),
|
|
421
|
+
|
|
422
|
+
// Structured JSON field with nested schema
|
|
423
|
+
profile: t.json({
|
|
424
|
+
firstName: t.varchar(50).generate('person.firstName'),
|
|
425
|
+
lastName: t.varchar(50).generate('person.lastName'),
|
|
426
|
+
age: t.integer().min(18).max(90),
|
|
427
|
+
|
|
428
|
+
// Deeply nested structures
|
|
429
|
+
preferences: t.json({
|
|
430
|
+
theme: t.varchar(20).enum(['light', 'dark', 'auto']).default('auto'),
|
|
431
|
+
notifications: t.json({
|
|
432
|
+
email: t.boolean().default(true),
|
|
433
|
+
push: t.boolean().default(false),
|
|
434
|
+
frequency: t.varchar(20).enum(['realtime', 'daily', 'weekly'])
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
}),
|
|
438
|
+
|
|
439
|
+
// Simple unstructured JSON (fallback)
|
|
440
|
+
metadata: t.jsonb()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Generated TypeScript types:**
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
// mock-data/types.ts
|
|
449
|
+
export interface User {
|
|
450
|
+
id: number;
|
|
451
|
+
email: string;
|
|
452
|
+
profile: {
|
|
453
|
+
firstName: string;
|
|
454
|
+
lastName: string;
|
|
455
|
+
age: number;
|
|
456
|
+
preferences: {
|
|
457
|
+
theme?: string; // Optional because it has a default
|
|
458
|
+
notifications: {
|
|
459
|
+
email?: boolean;
|
|
460
|
+
push?: boolean;
|
|
461
|
+
frequency: string;
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
metadata: any; // Unstructured JSON falls back to 'any'
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**Generated JSON data:**
|
|
470
|
+
|
|
471
|
+
```json
|
|
472
|
+
{
|
|
473
|
+
"id": 1,
|
|
474
|
+
"email": "john@example.com",
|
|
475
|
+
"profile": {
|
|
476
|
+
"firstName": "John",
|
|
477
|
+
"lastName": "Doe",
|
|
478
|
+
"age": 34,
|
|
479
|
+
"preferences": {
|
|
480
|
+
"theme": "dark",
|
|
481
|
+
"notifications": {
|
|
482
|
+
"email": true,
|
|
483
|
+
"push": false,
|
|
484
|
+
"frequency": "daily"
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
"metadata": { "data": "sample" }
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Benefits:**
|
|
493
|
+
- ✅ Full TypeScript type safety for nested JSON structures
|
|
494
|
+
- ✅ All field modifiers work (`.nullable()`, `.default()`, `.enum()`, `.generate()`, etc.)
|
|
495
|
+
- ✅ Supports unlimited nesting depth
|
|
496
|
+
- ✅ Works with both `t.json()` and `t.jsonb()`
|
|
497
|
+
|
|
335
498
|
### Custom Data Generators
|
|
336
499
|
|
|
337
500
|
Leverage any [Faker.js](https://fakerjs.dev/) method for realistic data generation:
|
|
@@ -370,6 +533,7 @@ Arguments:
|
|
|
370
533
|
Options:
|
|
371
534
|
-o, --output <dir> Output directory for generated JSON files (default: ./mock-data)
|
|
372
535
|
-s, --seed <number> Random seed for reproducible data generation
|
|
536
|
+
-t, --types Generate TypeScript type definitions alongside JSON
|
|
373
537
|
-h, --help Show help message
|
|
374
538
|
```
|
|
375
539
|
|
|
@@ -388,8 +552,11 @@ bunx @doviui/dev-db ./schemas -o ./data
|
|
|
388
552
|
# Reproducible generation with seed
|
|
389
553
|
bunx @doviui/dev-db schema.ts -s 42
|
|
390
554
|
|
|
555
|
+
# Generate TypeScript types
|
|
556
|
+
bunx @doviui/dev-db schema.ts --types
|
|
557
|
+
|
|
391
558
|
# All options combined
|
|
392
|
-
bunx @doviui/dev-db ./schemas --output ./database --seed 12345
|
|
559
|
+
bunx @doviui/dev-db ./schemas --output ./database --seed 12345 --types
|
|
393
560
|
```
|
|
394
561
|
|
|
395
562
|
**Schema File Format:**
|
package/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { MockDataGenerator } from './src/generator';
|
|
8
8
|
import { SchemaValidator } from './src/validator';
|
|
9
|
+
import { TypesGenerator } from './src/types-generator';
|
|
9
10
|
import type { Schema } from './src/types';
|
|
10
11
|
import { readdirSync, statSync } from 'fs';
|
|
11
12
|
import { join, extname, resolve } from 'path';
|
|
@@ -22,6 +23,7 @@ ARGUMENTS:
|
|
|
22
23
|
OPTIONS:
|
|
23
24
|
-o, --output <dir> Output directory for generated JSON files (default: ./mock-data)
|
|
24
25
|
-s, --seed <number> Random seed for reproducible data generation
|
|
26
|
+
-t, --types Generate TypeScript type definitions alongside JSON
|
|
25
27
|
-h, --help Show this help message
|
|
26
28
|
|
|
27
29
|
EXAMPLES:
|
|
@@ -29,6 +31,7 @@ EXAMPLES:
|
|
|
29
31
|
dev-db ./schemas
|
|
30
32
|
dev-db schema.ts -o ./data -s 42
|
|
31
33
|
dev-db ./schemas --output ./database --seed 12345
|
|
34
|
+
dev-db schema.ts --types
|
|
32
35
|
|
|
33
36
|
SCHEMA FILE FORMAT:
|
|
34
37
|
Schema files must export a default object or named 'schema' export:
|
|
@@ -53,12 +56,14 @@ interface CLIOptions {
|
|
|
53
56
|
schemaPath?: string;
|
|
54
57
|
outputDir: string;
|
|
55
58
|
seed?: number;
|
|
59
|
+
generateTypes: boolean;
|
|
56
60
|
help: boolean;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
function parseArgs(args: string[]): CLIOptions {
|
|
60
64
|
const options: CLIOptions = {
|
|
61
65
|
outputDir: './mock-data',
|
|
66
|
+
generateTypes: false,
|
|
62
67
|
help: false,
|
|
63
68
|
};
|
|
64
69
|
|
|
@@ -74,6 +79,8 @@ function parseArgs(args: string[]): CLIOptions {
|
|
|
74
79
|
} else if (arg === '-s' || arg === '--seed') {
|
|
75
80
|
const nextArg = args[++i];
|
|
76
81
|
if (nextArg !== undefined) options.seed = parseInt(nextArg, 10);
|
|
82
|
+
} else if (arg === '-t' || arg === '--types') {
|
|
83
|
+
options.generateTypes = true;
|
|
77
84
|
} else if (!arg.startsWith('-')) {
|
|
78
85
|
options.schemaPath = arg;
|
|
79
86
|
}
|
|
@@ -189,6 +196,17 @@ async function main() {
|
|
|
189
196
|
await generator.generate();
|
|
190
197
|
|
|
191
198
|
console.log(`Generated mock data in: ${absoluteOutputDir}`);
|
|
199
|
+
|
|
200
|
+
// Generate TypeScript types if requested
|
|
201
|
+
if (options.generateTypes) {
|
|
202
|
+
const typesGenerator = new TypesGenerator(schema, {
|
|
203
|
+
outputDir: absoluteOutputDir,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await typesGenerator.generate();
|
|
207
|
+
|
|
208
|
+
console.log(`Generated TypeScript types in: ${absoluteOutputDir}/types.ts`);
|
|
209
|
+
}
|
|
192
210
|
} catch (error) {
|
|
193
211
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
194
212
|
process.exit(1);
|
package/index.ts
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
export { t } from './src/schema-builder';
|
|
35
35
|
export { MockDataGenerator } from './src/generator';
|
|
36
36
|
export { SchemaValidator } from './src/validator';
|
|
37
|
+
export { TypesGenerator } from './src/types-generator';
|
|
37
38
|
export type { Schema, TableConfig, FieldConfig, Generator } from './src/types';
|
|
38
39
|
export type { ValidationError } from './src/validator';
|
|
39
40
|
export type { GeneratorOptions } from './src/generator';
|
|
41
|
+
export type { TypesGeneratorOptions } from './src/types-generator';
|
package/package.json
CHANGED
package/src/field-builder.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Faker } from '@faker-js/faker';
|
|
2
|
-
import type { FieldConfig, FieldBuilderLike } from './types';
|
|
2
|
+
import type { FieldConfig, FieldBuilderLike, JsonSchema } from './types';
|
|
3
3
|
|
|
4
4
|
export type Generator = string | ((faker: Faker) => any);
|
|
5
5
|
|
|
@@ -160,6 +160,29 @@ export class FieldBuilder implements FieldBuilderLike {
|
|
|
160
160
|
return this;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Sets a nested schema for JSON/JSONB fields.
|
|
165
|
+
* Defines the structure and types of properties within the JSON object.
|
|
166
|
+
*
|
|
167
|
+
* @param schema - The JSON schema defining nested fields
|
|
168
|
+
* @returns The builder instance for chaining
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* t.json({
|
|
173
|
+
* userId: t.integer(),
|
|
174
|
+
* preferences: t.json({
|
|
175
|
+
* theme: t.varchar(20),
|
|
176
|
+
* notifications: t.boolean()
|
|
177
|
+
* })
|
|
178
|
+
* })
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
withJsonSchema(schema: JsonSchema): this {
|
|
182
|
+
this.config.jsonSchema = schema;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
163
186
|
/**
|
|
164
187
|
* Converts the builder to a plain FieldConfig object.
|
|
165
188
|
*
|
package/src/generator.ts
CHANGED
|
@@ -206,12 +206,13 @@ export class MockDataGenerator {
|
|
|
206
206
|
return index + 1;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
// Handle nullable fields (but not if they have enum, min/max constraints, or
|
|
209
|
+
// Handle nullable fields (but not if they have enum, min/max constraints, custom generators, or JSON schemas)
|
|
210
210
|
if (
|
|
211
211
|
fieldConfig.nullable &&
|
|
212
212
|
!fieldConfig.notNull &&
|
|
213
213
|
!fieldConfig.enum &&
|
|
214
214
|
!fieldConfig.generator &&
|
|
215
|
+
!fieldConfig.jsonSchema &&
|
|
215
216
|
fieldConfig.min === undefined &&
|
|
216
217
|
fieldConfig.max === undefined &&
|
|
217
218
|
Math.random() < 0.1
|
|
@@ -309,6 +310,11 @@ export class MockDataGenerator {
|
|
|
309
310
|
|
|
310
311
|
case 'json':
|
|
311
312
|
case 'jsonb':
|
|
313
|
+
// If there's a nested schema, generate structured data
|
|
314
|
+
if (fieldConfig.jsonSchema) {
|
|
315
|
+
return this.generateJsonFromSchema(fieldConfig.jsonSchema);
|
|
316
|
+
}
|
|
317
|
+
// Otherwise, generate a simple object with sample data
|
|
312
318
|
return { data: faker.lorem.word() };
|
|
313
319
|
|
|
314
320
|
default:
|
|
@@ -334,6 +340,21 @@ export class MockDataGenerator {
|
|
|
334
340
|
return current;
|
|
335
341
|
}
|
|
336
342
|
|
|
343
|
+
private generateJsonFromSchema(jsonSchema: any): any {
|
|
344
|
+
const result: any = {};
|
|
345
|
+
|
|
346
|
+
for (const [fieldName, field] of Object.entries(jsonSchema)) {
|
|
347
|
+
const fieldConfig = this.toFieldConfig(field);
|
|
348
|
+
if (!fieldConfig) continue;
|
|
349
|
+
|
|
350
|
+
// Generate value based on field config
|
|
351
|
+
// Note: We don't track unique values for nested JSON fields
|
|
352
|
+
result[fieldName] = this.generateValueByType(fieldConfig);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
337
358
|
private async writeDataToFiles(): Promise<void> {
|
|
338
359
|
const outputDir = this.options.outputDir ?? './mock-data';
|
|
339
360
|
|
package/src/schema-builder.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FieldBuilder, ForeignKeyBuilder } from './field-builder';
|
|
2
|
+
import type { JsonSchema } from './types';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Type builder API for defining database schemas with a fluent interface.
|
|
@@ -183,19 +184,55 @@ export const t = {
|
|
|
183
184
|
|
|
184
185
|
/**
|
|
185
186
|
* Creates a JSON field for storing structured data.
|
|
186
|
-
* @
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
* @param schema - Optional nested schema defining the JSON structure
|
|
188
|
+
* @returns FieldBuilder instance for method chaining
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* // Simple JSON field
|
|
192
|
+
* t.json()
|
|
193
|
+
*
|
|
194
|
+
* // Structured JSON field with schema
|
|
195
|
+
* t.json({
|
|
196
|
+
* id: t.integer(),
|
|
197
|
+
* preferences: t.json({
|
|
198
|
+
* dark: t.boolean()
|
|
199
|
+
* })
|
|
200
|
+
* })
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
json(schema?: JsonSchema): FieldBuilder {
|
|
204
|
+
const builder = new FieldBuilder('json');
|
|
205
|
+
if (schema) {
|
|
206
|
+
builder.withJsonSchema(schema);
|
|
207
|
+
}
|
|
208
|
+
return builder;
|
|
190
209
|
},
|
|
191
210
|
|
|
192
211
|
/**
|
|
193
212
|
* Creates a binary JSON field (JSONB).
|
|
194
213
|
* More efficient for querying than regular JSON.
|
|
195
|
-
* @
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
* @param schema - Optional nested schema defining the JSON structure
|
|
215
|
+
* @returns FieldBuilder instance for method chaining
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* // Simple JSONB field
|
|
219
|
+
* t.jsonb()
|
|
220
|
+
*
|
|
221
|
+
* // Structured JSONB field with schema
|
|
222
|
+
* t.jsonb({
|
|
223
|
+
* userId: t.integer(),
|
|
224
|
+
* metadata: t.json({
|
|
225
|
+
* tags: t.varchar(255)
|
|
226
|
+
* })
|
|
227
|
+
* })
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
jsonb(schema?: JsonSchema): FieldBuilder {
|
|
231
|
+
const builder = new FieldBuilder('jsonb');
|
|
232
|
+
if (schema) {
|
|
233
|
+
builder.withJsonSchema(schema);
|
|
234
|
+
}
|
|
235
|
+
return builder;
|
|
199
236
|
},
|
|
200
237
|
|
|
201
238
|
// Relationships
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { Schema, FieldConfig, TableConfig } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for configuring the TypeScript types generator.
|
|
5
|
+
*/
|
|
6
|
+
export interface TypesGeneratorOptions {
|
|
7
|
+
/** Directory where the types file will be saved (default: './mock-data') */
|
|
8
|
+
outputDir?: string;
|
|
9
|
+
|
|
10
|
+
/** Name of the generated types file (default: 'types.ts') */
|
|
11
|
+
fileName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generates TypeScript type definitions from dev-db schemas.
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - Converts field types to TypeScript types
|
|
19
|
+
* - Handles nullable and optional fields
|
|
20
|
+
* - Generates interfaces for each table
|
|
21
|
+
* - Creates a single types file for easy importing
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const generator = new TypesGenerator(schema, {
|
|
26
|
+
* outputDir: './mock-data',
|
|
27
|
+
* fileName: 'types.ts'
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* await generator.generate();
|
|
31
|
+
* // Creates ./mock-data/types.ts with all interfaces
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class TypesGenerator {
|
|
35
|
+
private schema: Schema;
|
|
36
|
+
private options: TypesGeneratorOptions;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new TypeScript types generator.
|
|
40
|
+
*
|
|
41
|
+
* @param schema - The schema definition
|
|
42
|
+
* @param options - Generator options
|
|
43
|
+
*/
|
|
44
|
+
constructor(schema: Schema, options: TypesGeneratorOptions = {}) {
|
|
45
|
+
this.schema = schema;
|
|
46
|
+
this.options = {
|
|
47
|
+
outputDir: options.outputDir || './mock-data',
|
|
48
|
+
fileName: options.fileName || 'types.ts',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generates TypeScript type definitions for all tables in the schema.
|
|
54
|
+
*
|
|
55
|
+
* Creates a single .ts file with interface definitions that match
|
|
56
|
+
* the structure of the generated JSON data.
|
|
57
|
+
*
|
|
58
|
+
* @returns Promise resolving when the types file is written
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* await generator.generate();
|
|
63
|
+
* // Creates ./mock-data/types.ts
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
async generate(): Promise<void> {
|
|
67
|
+
const interfaces: string[] = [];
|
|
68
|
+
|
|
69
|
+
// Generate interface for each table
|
|
70
|
+
for (const [tableName, tableConfig] of Object.entries(this.schema)) {
|
|
71
|
+
const interfaceCode = this.generateInterface(tableName, tableConfig);
|
|
72
|
+
interfaces.push(interfaceCode);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Combine all interfaces into a single file
|
|
76
|
+
const fileContent = interfaces.join('\n\n');
|
|
77
|
+
|
|
78
|
+
// Write to file
|
|
79
|
+
const outputPath = `${this.options.outputDir}/${this.options.fileName}`;
|
|
80
|
+
await Bun.write(outputPath, fileContent);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private generateInterface(tableName: string, tableConfig: TableConfig): string {
|
|
84
|
+
const fields: string[] = [];
|
|
85
|
+
|
|
86
|
+
// Process each field
|
|
87
|
+
for (const [fieldName, field] of Object.entries(tableConfig)) {
|
|
88
|
+
// Skip the $count property
|
|
89
|
+
if (fieldName === '$count') continue;
|
|
90
|
+
|
|
91
|
+
const fieldConfig = this.toFieldConfig(field);
|
|
92
|
+
if (!fieldConfig) continue;
|
|
93
|
+
|
|
94
|
+
const fieldDeclaration = this.generateFieldDeclaration(fieldName, fieldConfig);
|
|
95
|
+
fields.push(` ${fieldDeclaration};`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Generate interface
|
|
99
|
+
return `export interface ${tableName} {\n${fields.join('\n')}\n}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private generateFieldDeclaration(fieldName: string, fieldConfig: FieldConfig): string {
|
|
103
|
+
const tsType = this.mapFieldTypeToTypeScript(fieldConfig);
|
|
104
|
+
const isOptional = this.isFieldOptional(fieldConfig);
|
|
105
|
+
const isNullable = this.isFieldNullable(fieldConfig);
|
|
106
|
+
|
|
107
|
+
let declaration = fieldName;
|
|
108
|
+
|
|
109
|
+
// Add optional marker if needed
|
|
110
|
+
if (isOptional) {
|
|
111
|
+
declaration += '?';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
declaration += ': ';
|
|
115
|
+
|
|
116
|
+
// Add type
|
|
117
|
+
declaration += tsType;
|
|
118
|
+
|
|
119
|
+
// Add null union if nullable (but only if not already optional with a default)
|
|
120
|
+
if (isNullable && !(isOptional && fieldConfig.default !== undefined)) {
|
|
121
|
+
declaration += ' | null';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return declaration;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private mapFieldTypeToTypeScript(fieldConfig: FieldConfig): string {
|
|
128
|
+
// Handle foreign keys by looking up the referenced column type
|
|
129
|
+
if (fieldConfig.foreignKey) {
|
|
130
|
+
const refTable = fieldConfig.foreignKey.table;
|
|
131
|
+
const refColumn = fieldConfig.foreignKey.column;
|
|
132
|
+
|
|
133
|
+
const refTableConfig = this.schema[refTable];
|
|
134
|
+
if (refTableConfig) {
|
|
135
|
+
const refFieldConfig = this.toFieldConfig(refTableConfig[refColumn]);
|
|
136
|
+
if (refFieldConfig) {
|
|
137
|
+
return this.mapBasicTypeToTypeScript(refFieldConfig.type);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fallback if reference not found
|
|
142
|
+
return 'any';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle JSON/JSONB with nested schema
|
|
146
|
+
if ((fieldConfig.type === 'json' || fieldConfig.type === 'jsonb') && fieldConfig.jsonSchema) {
|
|
147
|
+
return this.generateJsonSchemaType(fieldConfig.jsonSchema);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.mapBasicTypeToTypeScript(fieldConfig.type);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private generateJsonSchemaType(jsonSchema: any): string {
|
|
154
|
+
const fields: string[] = [];
|
|
155
|
+
|
|
156
|
+
for (const [fieldName, field] of Object.entries(jsonSchema)) {
|
|
157
|
+
const fieldConfig = this.toFieldConfig(field);
|
|
158
|
+
if (!fieldConfig) continue;
|
|
159
|
+
|
|
160
|
+
const tsType = this.mapFieldTypeToTypeScript(fieldConfig);
|
|
161
|
+
const isNullable = this.isFieldNullable(fieldConfig);
|
|
162
|
+
const isOptional = this.isFieldOptional(fieldConfig);
|
|
163
|
+
|
|
164
|
+
let declaration = `${fieldName}`;
|
|
165
|
+
|
|
166
|
+
if (isOptional) {
|
|
167
|
+
declaration += '?';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
declaration += `: ${tsType}`;
|
|
171
|
+
|
|
172
|
+
if (isNullable && !(isOptional && fieldConfig.default !== undefined)) {
|
|
173
|
+
declaration += ' | null';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fields.push(` ${declaration}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return `{\n${fields.join(';\n')}${fields.length > 0 ? ';' : ''}\n }`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private mapBasicTypeToTypeScript(type: string): string {
|
|
183
|
+
switch (type) {
|
|
184
|
+
case 'bigint':
|
|
185
|
+
case 'bigserial':
|
|
186
|
+
case 'integer':
|
|
187
|
+
case 'smallint':
|
|
188
|
+
case 'serial':
|
|
189
|
+
case 'decimal':
|
|
190
|
+
case 'numeric':
|
|
191
|
+
case 'real':
|
|
192
|
+
case 'double':
|
|
193
|
+
return 'number';
|
|
194
|
+
|
|
195
|
+
case 'varchar':
|
|
196
|
+
case 'char':
|
|
197
|
+
case 'text':
|
|
198
|
+
case 'uuid':
|
|
199
|
+
return 'string';
|
|
200
|
+
|
|
201
|
+
case 'boolean':
|
|
202
|
+
return 'boolean';
|
|
203
|
+
|
|
204
|
+
case 'date':
|
|
205
|
+
case 'time':
|
|
206
|
+
case 'timestamp':
|
|
207
|
+
case 'timestamptz':
|
|
208
|
+
return 'string'; // ISO format strings
|
|
209
|
+
|
|
210
|
+
case 'json':
|
|
211
|
+
case 'jsonb':
|
|
212
|
+
return 'any'; // Could be `object` or a generic type parameter in the future
|
|
213
|
+
|
|
214
|
+
default:
|
|
215
|
+
return 'any';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private isFieldOptional(fieldConfig: FieldConfig): boolean {
|
|
220
|
+
// A field is optional if it has a default value
|
|
221
|
+
// This means it doesn't need to be provided when creating the object
|
|
222
|
+
return fieldConfig.default !== undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private isFieldNullable(fieldConfig: FieldConfig): boolean {
|
|
226
|
+
// A field can be null if:
|
|
227
|
+
// - Explicitly marked as nullable AND not marked as notNull
|
|
228
|
+
if (fieldConfig.notNull) return false;
|
|
229
|
+
if (fieldConfig.nullable === true) return true;
|
|
230
|
+
|
|
231
|
+
// Fields with defaults are not nullable unless explicitly set
|
|
232
|
+
if (fieldConfig.default !== undefined) return false;
|
|
233
|
+
|
|
234
|
+
// Primary keys are not nullable
|
|
235
|
+
if (fieldConfig.primaryKey) return false;
|
|
236
|
+
|
|
237
|
+
return false; // Default to not nullable unless explicitly set
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private toFieldConfig(field: any): FieldConfig | null {
|
|
241
|
+
if (!field) return null;
|
|
242
|
+
if (typeof field === 'object' && 'toConfig' in field) {
|
|
243
|
+
return field.toConfig();
|
|
244
|
+
}
|
|
245
|
+
if (typeof field === 'object' && 'type' in field) {
|
|
246
|
+
return field;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -59,6 +59,16 @@ export interface FieldConfig {
|
|
|
59
59
|
|
|
60
60
|
/** Custom generator function or Faker.js method path */
|
|
61
61
|
generator?: Generator;
|
|
62
|
+
|
|
63
|
+
/** Nested JSON schema for json/jsonb types */
|
|
64
|
+
jsonSchema?: JsonSchema;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Schema definition for JSON fields, mapping property names to field configurations.
|
|
69
|
+
*/
|
|
70
|
+
export interface JsonSchema {
|
|
71
|
+
[fieldName: string]: FieldConfig | FieldBuilderLike;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/**
|