@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.
@@ -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
+ }
@@ -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
+ }