@doviui/dev-db 0.2.0 → 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 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
- t.json() // JSON object
193
- t.jsonb() // JSON binary
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,9 +6,10 @@
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
- import { join, extname } from 'path';
12
+ import { join, extname, resolve } from 'path';
12
13
 
13
14
  const HELP_TEXT = `
14
15
  @doviui/dev-db - Generate realistic mock JSON databases
@@ -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
  }
@@ -84,12 +91,10 @@ function parseArgs(args: string[]): CLIOptions {
84
91
 
85
92
  async function loadSchemaFromFile(filePath: string): Promise<Schema> {
86
93
  try {
87
- // Resolve path relative to current working directory
88
- const resolvedPath = filePath.startsWith('.') || filePath.startsWith('/')
89
- ? filePath
90
- : `./${filePath}`;
94
+ // Resolve path to absolute path based on current working directory
95
+ const absolutePath = resolve(process.cwd(), filePath);
91
96
 
92
- const module = await import(resolvedPath);
97
+ const module = await import(absolutePath);
93
98
  const schema = module.default || module.schema;
94
99
 
95
100
  if (!schema) {
@@ -109,11 +114,10 @@ async function loadSchemaFromFile(filePath: string): Promise<Schema> {
109
114
 
110
115
  async function loadSchemaFromDirectory(dirPath: string): Promise<Schema> {
111
116
  try {
112
- const resolvedDir = dirPath.startsWith('.') || dirPath.startsWith('/')
113
- ? dirPath
114
- : `./${dirPath}`;
117
+ // Resolve path to absolute path based on current working directory
118
+ const absoluteDir = resolve(process.cwd(), dirPath);
115
119
 
116
- const files = readdirSync(resolvedDir);
120
+ const files = readdirSync(absoluteDir);
117
121
  const schemaFiles = files.filter(file => {
118
122
  const ext = extname(file);
119
123
  return ext === '.ts' || ext === '.js';
@@ -126,7 +130,7 @@ async function loadSchemaFromDirectory(dirPath: string): Promise<Schema> {
126
130
  const mergedSchema: Schema = {};
127
131
 
128
132
  for (const file of schemaFiles) {
129
- const filePath = join(resolvedDir, file);
133
+ const filePath = join(absoluteDir, file);
130
134
  const schema = await loadSchemaFromFile(filePath);
131
135
 
132
136
  // Merge schemas
@@ -143,11 +147,10 @@ async function loadSchemaFromDirectory(dirPath: string): Promise<Schema> {
143
147
  }
144
148
 
145
149
  async function loadSchema(path: string): Promise<Schema> {
146
- const resolvedPath = path.startsWith('.') || path.startsWith('/')
147
- ? path
148
- : `./${path}`;
150
+ // Resolve path to absolute path based on current working directory
151
+ const absolutePath = resolve(process.cwd(), path);
149
152
 
150
- const stats = statSync(resolvedPath);
153
+ const stats = statSync(absolutePath);
151
154
 
152
155
  if (stats.isDirectory()) {
153
156
  return loadSchemaFromDirectory(path);
@@ -182,14 +185,28 @@ async function main() {
182
185
  process.exit(1);
183
186
  }
184
187
 
188
+ // Resolve output directory to absolute path
189
+ const absoluteOutputDir = resolve(process.cwd(), options.outputDir);
190
+
185
191
  const generator = new MockDataGenerator(schema, {
186
- outputDir: options.outputDir,
192
+ outputDir: absoluteOutputDir,
187
193
  seed: options.seed,
188
194
  });
189
195
 
190
196
  await generator.generate();
191
197
 
192
- console.log(`Generated mock data in: ${options.outputDir}`);
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
+ }
193
210
  } catch (error) {
194
211
  console.error('Error:', error instanceof Error ? error.message : error);
195
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doviui/dev-db",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript-first mock database generator for rapid application development",
5
5
  "main": "index.ts",
6
6
  "module": "index.ts",
@@ -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 custom generators)
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
 
@@ -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
- * @returns FieldBuilder instance for method chaining
187
- */
188
- json(): FieldBuilder {
189
- return new FieldBuilder('json');
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
- * @returns FieldBuilder instance for method chaining
196
- */
197
- jsonb(): FieldBuilder {
198
- return new FieldBuilder('jsonb');
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
  /**