@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,213 @@
|
|
|
1
|
+
import { FieldBuilder, ForeignKeyBuilder } from './field-builder';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type builder API for defining database schemas with a fluent interface.
|
|
5
|
+
* Provides methods for creating various SQL-like field types with constraints.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { t } from '@doviui/dev-db';
|
|
10
|
+
*
|
|
11
|
+
* const schema = {
|
|
12
|
+
* User: {
|
|
13
|
+
* $count: 100,
|
|
14
|
+
* id: t.bigserial().primaryKey(),
|
|
15
|
+
* email: t.varchar(255).unique().notNull().generate('internet.email'),
|
|
16
|
+
* age: t.integer().min(18).max(100),
|
|
17
|
+
* created_at: t.timestamptz().default('now')
|
|
18
|
+
* }
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const t = {
|
|
23
|
+
// Numeric types
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a big integer field (64-bit).
|
|
27
|
+
* @returns FieldBuilder instance for method chaining
|
|
28
|
+
*/
|
|
29
|
+
bigint(): FieldBuilder {
|
|
30
|
+
return new FieldBuilder('bigint');
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates an auto-incrementing big integer field.
|
|
35
|
+
* Commonly used for primary keys.
|
|
36
|
+
* @returns FieldBuilder instance for method chaining
|
|
37
|
+
*/
|
|
38
|
+
bigserial(): FieldBuilder {
|
|
39
|
+
return new FieldBuilder('bigserial');
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a standard integer field (32-bit).
|
|
44
|
+
* @returns FieldBuilder instance for method chaining
|
|
45
|
+
*/
|
|
46
|
+
integer(): FieldBuilder {
|
|
47
|
+
return new FieldBuilder('integer');
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a small integer field (16-bit).
|
|
52
|
+
* @returns FieldBuilder instance for method chaining
|
|
53
|
+
*/
|
|
54
|
+
smallint(): FieldBuilder {
|
|
55
|
+
return new FieldBuilder('smallint');
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates an auto-incrementing integer field.
|
|
60
|
+
* @returns FieldBuilder instance for method chaining
|
|
61
|
+
*/
|
|
62
|
+
serial(): FieldBuilder {
|
|
63
|
+
return new FieldBuilder('serial');
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a decimal/numeric field with specified precision and scale.
|
|
68
|
+
* @param precision - Total number of digits
|
|
69
|
+
* @param scale - Number of digits after the decimal point
|
|
70
|
+
* @returns FieldBuilder instance for method chaining
|
|
71
|
+
* @example t.decimal(10, 2) // e.g., 12345678.90
|
|
72
|
+
*/
|
|
73
|
+
decimal(precision: number, scale: number): FieldBuilder {
|
|
74
|
+
return new FieldBuilder('decimal', undefined, precision, scale);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a numeric field (alias for decimal).
|
|
79
|
+
* @param precision - Total number of digits
|
|
80
|
+
* @param scale - Number of digits after the decimal point
|
|
81
|
+
* @returns FieldBuilder instance for method chaining
|
|
82
|
+
*/
|
|
83
|
+
numeric(precision: number, scale: number): FieldBuilder {
|
|
84
|
+
return new FieldBuilder('numeric', undefined, precision, scale);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a single-precision floating point field.
|
|
89
|
+
* @returns FieldBuilder instance for method chaining
|
|
90
|
+
*/
|
|
91
|
+
real(): FieldBuilder {
|
|
92
|
+
return new FieldBuilder('real');
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a double-precision floating point field.
|
|
97
|
+
* @returns FieldBuilder instance for method chaining
|
|
98
|
+
*/
|
|
99
|
+
double(): FieldBuilder {
|
|
100
|
+
return new FieldBuilder('double');
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// String types
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a variable-length character field.
|
|
107
|
+
* @param length - Maximum length of the string
|
|
108
|
+
* @returns FieldBuilder instance for method chaining
|
|
109
|
+
* @example t.varchar(255)
|
|
110
|
+
*/
|
|
111
|
+
varchar(length: number): FieldBuilder {
|
|
112
|
+
return new FieldBuilder('varchar', length);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a fixed-length character field.
|
|
117
|
+
* @param length - Fixed length of the string
|
|
118
|
+
* @returns FieldBuilder instance for method chaining
|
|
119
|
+
*/
|
|
120
|
+
char(length: number): FieldBuilder {
|
|
121
|
+
return new FieldBuilder('char', length);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates an unlimited text field.
|
|
126
|
+
* @returns FieldBuilder instance for method chaining
|
|
127
|
+
*/
|
|
128
|
+
text(): FieldBuilder {
|
|
129
|
+
return new FieldBuilder('text');
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Date/Time types
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a date-only field (no time component).
|
|
136
|
+
* @returns FieldBuilder instance for method chaining
|
|
137
|
+
*/
|
|
138
|
+
date(): FieldBuilder {
|
|
139
|
+
return new FieldBuilder('date');
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Creates a time-only field (no date component).
|
|
144
|
+
* @returns FieldBuilder instance for method chaining
|
|
145
|
+
*/
|
|
146
|
+
time(): FieldBuilder {
|
|
147
|
+
return new FieldBuilder('time');
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Creates a timestamp field (date and time without timezone).
|
|
152
|
+
* @returns FieldBuilder instance for method chaining
|
|
153
|
+
*/
|
|
154
|
+
timestamp(): FieldBuilder {
|
|
155
|
+
return new FieldBuilder('timestamp');
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates a timestamp field with timezone support.
|
|
160
|
+
* @returns FieldBuilder instance for method chaining
|
|
161
|
+
*/
|
|
162
|
+
timestamptz(): FieldBuilder {
|
|
163
|
+
return new FieldBuilder('timestamptz');
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Other types
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Creates a boolean field (true/false).
|
|
170
|
+
* @returns FieldBuilder instance for method chaining
|
|
171
|
+
*/
|
|
172
|
+
boolean(): FieldBuilder {
|
|
173
|
+
return new FieldBuilder('boolean');
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Creates a UUID field (universally unique identifier).
|
|
178
|
+
* @returns FieldBuilder instance for method chaining
|
|
179
|
+
*/
|
|
180
|
+
uuid(): FieldBuilder {
|
|
181
|
+
return new FieldBuilder('uuid');
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Creates a JSON field for storing structured data.
|
|
186
|
+
* @returns FieldBuilder instance for method chaining
|
|
187
|
+
*/
|
|
188
|
+
json(): FieldBuilder {
|
|
189
|
+
return new FieldBuilder('json');
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Creates a binary JSON field (JSONB).
|
|
194
|
+
* More efficient for querying than regular JSON.
|
|
195
|
+
* @returns FieldBuilder instance for method chaining
|
|
196
|
+
*/
|
|
197
|
+
jsonb(): FieldBuilder {
|
|
198
|
+
return new FieldBuilder('jsonb');
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
// Relationships
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Creates a foreign key reference to another table.
|
|
205
|
+
* @param table - The referenced table name
|
|
206
|
+
* @param column - The referenced column name
|
|
207
|
+
* @returns ForeignKeyBuilder instance for method chaining
|
|
208
|
+
* @example t.foreignKey('User', 'id').notNull()
|
|
209
|
+
*/
|
|
210
|
+
foreignKey(table: string, column: string): ForeignKeyBuilder {
|
|
211
|
+
return new ForeignKeyBuilder(table, column);
|
|
212
|
+
},
|
|
213
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Faker } from '@faker-js/faker';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A generator function or Faker.js method path for generating field values.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // Using Faker.js method path
|
|
9
|
+
* 'internet.email'
|
|
10
|
+
*
|
|
11
|
+
* // Using custom function
|
|
12
|
+
* (faker) => faker.helpers.arrayElement(['red', 'blue', 'green'])
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export type Generator = string | ((faker: Faker) => any);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration for a database field, defining its type, constraints, and generation rules.
|
|
19
|
+
*/
|
|
20
|
+
export interface FieldConfig {
|
|
21
|
+
/** The SQL-like type of the field (e.g., 'varchar', 'integer', 'uuid') */
|
|
22
|
+
type: string;
|
|
23
|
+
|
|
24
|
+
/** Maximum length for string types */
|
|
25
|
+
length?: number;
|
|
26
|
+
|
|
27
|
+
/** Precision for numeric types */
|
|
28
|
+
precision?: number;
|
|
29
|
+
|
|
30
|
+
/** Scale (decimal places) for numeric types */
|
|
31
|
+
scale?: number;
|
|
32
|
+
|
|
33
|
+
/** Whether this field is a primary key */
|
|
34
|
+
primaryKey?: boolean;
|
|
35
|
+
|
|
36
|
+
/** Whether values must be unique across all records */
|
|
37
|
+
unique?: boolean;
|
|
38
|
+
|
|
39
|
+
/** Whether null values are not allowed */
|
|
40
|
+
notNull?: boolean;
|
|
41
|
+
|
|
42
|
+
/** Whether null values are allowed (default: true) */
|
|
43
|
+
nullable?: boolean;
|
|
44
|
+
|
|
45
|
+
/** Default value for the field. Use 'now' for current timestamp */
|
|
46
|
+
default?: any;
|
|
47
|
+
|
|
48
|
+
/** Minimum value for numeric types */
|
|
49
|
+
min?: number;
|
|
50
|
+
|
|
51
|
+
/** Maximum value for numeric types */
|
|
52
|
+
max?: number;
|
|
53
|
+
|
|
54
|
+
/** Array of allowed values for enum fields */
|
|
55
|
+
enum?: any[];
|
|
56
|
+
|
|
57
|
+
/** Foreign key reference to another table */
|
|
58
|
+
foreignKey?: { table: string; column: string };
|
|
59
|
+
|
|
60
|
+
/** Custom generator function or Faker.js method path */
|
|
61
|
+
generator?: Generator;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Configuration for a database table, including field definitions and record count.
|
|
66
|
+
*/
|
|
67
|
+
export interface TableConfig {
|
|
68
|
+
/** Number of records to generate for this table */
|
|
69
|
+
$count?: number;
|
|
70
|
+
|
|
71
|
+
/** Field definitions - can be FieldConfig objects or FieldBuilder instances */
|
|
72
|
+
[fieldName: string]: FieldConfig | FieldBuilderLike | number | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Interface for objects that can be converted to FieldConfig.
|
|
77
|
+
* Allows FieldBuilder instances to be used directly in schemas.
|
|
78
|
+
*/
|
|
79
|
+
export interface FieldBuilderLike {
|
|
80
|
+
/** Converts the builder to a FieldConfig object */
|
|
81
|
+
toConfig(): FieldConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Complete schema definition mapping table names to their configurations.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const schema: Schema = {
|
|
90
|
+
* User: {
|
|
91
|
+
* $count: 100,
|
|
92
|
+
* id: t.bigserial().primaryKey(),
|
|
93
|
+
* email: t.varchar(255).unique().notNull(),
|
|
94
|
+
* age: t.integer().min(18).max(100)
|
|
95
|
+
* },
|
|
96
|
+
* Post: {
|
|
97
|
+
* $count: 500,
|
|
98
|
+
* id: t.bigserial().primaryKey(),
|
|
99
|
+
* user_id: t.foreignKey('User', 'id'),
|
|
100
|
+
* title: t.varchar(200)
|
|
101
|
+
* }
|
|
102
|
+
* };
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export interface Schema {
|
|
106
|
+
[tableName: string]: TableConfig;
|
|
107
|
+
}
|
package/src/validator.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Schema, FieldConfig, TableConfig } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a validation error found in a schema.
|
|
5
|
+
*/
|
|
6
|
+
export interface ValidationError {
|
|
7
|
+
/** The table where the error occurred */
|
|
8
|
+
table: string;
|
|
9
|
+
|
|
10
|
+
/** The field where the error occurred (if applicable) */
|
|
11
|
+
field?: string;
|
|
12
|
+
|
|
13
|
+
/** Human-readable error message */
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validates database schemas for common errors and inconsistencies.
|
|
19
|
+
*
|
|
20
|
+
* Checks for:
|
|
21
|
+
* - Missing primary keys
|
|
22
|
+
* - Invalid foreign key references
|
|
23
|
+
* - Circular dependencies
|
|
24
|
+
* - Invalid constraints (e.g., min > max)
|
|
25
|
+
* - Empty enums
|
|
26
|
+
* - Invalid $count values
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const validator = new SchemaValidator();
|
|
31
|
+
* const errors = validator.validate(schema);
|
|
32
|
+
*
|
|
33
|
+
* if (errors.length > 0) {
|
|
34
|
+
* console.error('Schema validation failed:');
|
|
35
|
+
* errors.forEach(err => {
|
|
36
|
+
* console.error(` ${err.table}.${err.field}: ${err.message}`);
|
|
37
|
+
* });
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class SchemaValidator {
|
|
42
|
+
private errors: ValidationError[] = [];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates a complete schema and returns any errors found.
|
|
46
|
+
*
|
|
47
|
+
* @param schema - The schema to validate
|
|
48
|
+
* @returns Array of validation errors (empty if valid)
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const errors = validator.validate(schema);
|
|
53
|
+
* if (errors.length === 0) {
|
|
54
|
+
* console.log('Schema is valid!');
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
validate(schema: Schema): ValidationError[] {
|
|
59
|
+
this.errors = [];
|
|
60
|
+
|
|
61
|
+
// Check for empty schema
|
|
62
|
+
if (Object.keys(schema).length === 0) {
|
|
63
|
+
this.errors.push({
|
|
64
|
+
table: '',
|
|
65
|
+
message: 'Schema is empty',
|
|
66
|
+
});
|
|
67
|
+
return this.errors;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate each table
|
|
71
|
+
for (const [tableName, tableConfig] of Object.entries(schema)) {
|
|
72
|
+
this.validateTable(tableName, tableConfig, schema);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for circular dependencies
|
|
76
|
+
this.checkCircularDependencies(schema);
|
|
77
|
+
|
|
78
|
+
return this.errors;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private validateTable(tableName: string, tableConfig: TableConfig, schema: Schema): void {
|
|
82
|
+
// Check if $count is valid
|
|
83
|
+
if (tableConfig.$count !== undefined && (tableConfig.$count < 0 || !Number.isInteger(tableConfig.$count))) {
|
|
84
|
+
this.errors.push({
|
|
85
|
+
table: tableName,
|
|
86
|
+
message: `Invalid $count: ${tableConfig.$count}. Must be a non-negative integer.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fields = Object.entries(tableConfig).filter(([key]) => key !== '$count');
|
|
91
|
+
|
|
92
|
+
if (fields.length === 0) {
|
|
93
|
+
this.errors.push({
|
|
94
|
+
table: tableName,
|
|
95
|
+
message: 'Table has no fields defined',
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if there's at least one primary key
|
|
101
|
+
const hasPrimaryKey = fields.some(([_, config]) => {
|
|
102
|
+
const fieldConfig = this.toFieldConfig(config);
|
|
103
|
+
return fieldConfig?.primaryKey;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!hasPrimaryKey) {
|
|
107
|
+
this.errors.push({
|
|
108
|
+
table: tableName,
|
|
109
|
+
message: 'Table must have at least one primary key',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validate each field
|
|
114
|
+
for (const [fieldName, fieldConfig] of fields) {
|
|
115
|
+
const config = this.toFieldConfig(fieldConfig);
|
|
116
|
+
if (config) {
|
|
117
|
+
this.validateField(tableName, fieldName, config, schema);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private toFieldConfig(field: any): FieldConfig | null {
|
|
123
|
+
if (!field) return null;
|
|
124
|
+
if (typeof field === 'object' && 'toConfig' in field) {
|
|
125
|
+
return field.toConfig();
|
|
126
|
+
}
|
|
127
|
+
if (typeof field === 'object' && 'type' in field) {
|
|
128
|
+
return field;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private validateField(tableName: string, fieldName: string, fieldConfig: FieldConfig, schema: Schema): void {
|
|
134
|
+
// Validate foreign key references
|
|
135
|
+
if (fieldConfig.foreignKey) {
|
|
136
|
+
const { table: refTable, column: refColumn } = fieldConfig.foreignKey;
|
|
137
|
+
|
|
138
|
+
if (!schema[refTable]) {
|
|
139
|
+
this.errors.push({
|
|
140
|
+
table: tableName,
|
|
141
|
+
field: fieldName,
|
|
142
|
+
message: `Foreign key references non-existent table '${refTable}'`,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
const refTableConfig = schema[refTable];
|
|
146
|
+
const refFields = Object.entries(refTableConfig).filter(([key]) => key !== '$count');
|
|
147
|
+
const refFieldExists = refFields.some(([name]) => name === refColumn);
|
|
148
|
+
|
|
149
|
+
if (!refFieldExists) {
|
|
150
|
+
this.errors.push({
|
|
151
|
+
table: tableName,
|
|
152
|
+
field: fieldName,
|
|
153
|
+
message: `Foreign key references non-existent column '${refColumn}' in table '${refTable}'`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validate min/max constraints
|
|
160
|
+
if (fieldConfig.min !== undefined && fieldConfig.max !== undefined) {
|
|
161
|
+
if (fieldConfig.min > fieldConfig.max) {
|
|
162
|
+
this.errors.push({
|
|
163
|
+
table: tableName,
|
|
164
|
+
field: fieldName,
|
|
165
|
+
message: `Min value (${fieldConfig.min}) cannot be greater than max value (${fieldConfig.max})`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Validate enum
|
|
171
|
+
if (fieldConfig.enum && fieldConfig.enum.length === 0) {
|
|
172
|
+
this.errors.push({
|
|
173
|
+
table: tableName,
|
|
174
|
+
field: fieldName,
|
|
175
|
+
message: 'Enum must have at least one value',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Validate conflicting constraints
|
|
180
|
+
if (fieldConfig.notNull && fieldConfig.nullable) {
|
|
181
|
+
this.errors.push({
|
|
182
|
+
table: tableName,
|
|
183
|
+
field: fieldName,
|
|
184
|
+
message: 'Field cannot be both notNull and nullable',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private checkCircularDependencies(schema: Schema): void {
|
|
190
|
+
const visited = new Set<string>();
|
|
191
|
+
const recursionStack = new Set<string>();
|
|
192
|
+
|
|
193
|
+
const hasCycle = (tableName: string, path: string[]): boolean => {
|
|
194
|
+
visited.add(tableName);
|
|
195
|
+
recursionStack.add(tableName);
|
|
196
|
+
|
|
197
|
+
const tableConfig = schema[tableName];
|
|
198
|
+
if (!tableConfig) return false;
|
|
199
|
+
|
|
200
|
+
const fields = Object.entries(tableConfig).filter(([key]) => key !== '$count');
|
|
201
|
+
|
|
202
|
+
for (const [_, fieldConfig] of fields) {
|
|
203
|
+
const config = this.toFieldConfig(fieldConfig);
|
|
204
|
+
if (config?.foreignKey) {
|
|
205
|
+
const refTable = config.foreignKey.table;
|
|
206
|
+
|
|
207
|
+
if (!visited.has(refTable)) {
|
|
208
|
+
if (hasCycle(refTable, [...path, refTable])) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
} else if (recursionStack.has(refTable)) {
|
|
212
|
+
this.errors.push({
|
|
213
|
+
table: tableName,
|
|
214
|
+
message: `Circular dependency detected: ${[...path, refTable].join(' -> ')}`,
|
|
215
|
+
});
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
recursionStack.delete(tableName);
|
|
222
|
+
return false;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
for (const tableName of Object.keys(schema)) {
|
|
226
|
+
if (!visited.has(tableName)) {
|
|
227
|
+
hasCycle(tableName, [tableName]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|