webspresso 0.0.6 → 0.0.8
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 +506 -2
- package/bin/webspresso.js +357 -10
- package/core/applySchema.js +1 -0
- package/core/compileSchema.js +1 -0
- package/core/orm/eager-loader.js +232 -0
- package/core/orm/index.js +148 -0
- package/core/orm/migrations/index.js +205 -0
- package/core/orm/migrations/scaffold.js +312 -0
- package/core/orm/model.js +178 -0
- package/core/orm/query-builder.js +430 -0
- package/core/orm/repository.js +346 -0
- package/core/orm/schema-helpers.js +416 -0
- package/core/orm/scopes.js +183 -0
- package/core/orm/seeder.js +585 -0
- package/core/orm/transaction.js +69 -0
- package/core/orm/types.js +237 -0
- package/core/orm/utils.js +127 -0
- package/index.js +13 -1
- package/package.json +24 -3
- package/src/plugin-manager.js +1 -0
- package/utils/schemaCache.js +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM
|
|
3
|
+
* Minimal, Eloquent-inspired ORM with Knex and Zod
|
|
4
|
+
* @module core/orm
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { createSchemaHelpers, extractColumnsFromSchema, getColumnMeta } = require('./schema-helpers');
|
|
8
|
+
const { defineModel, getModel, getAllModels, hasModel, clearRegistry } = require('./model');
|
|
9
|
+
const { createRepository } = require('./repository');
|
|
10
|
+
const { createQueryBuilder, QueryBuilder } = require('./query-builder');
|
|
11
|
+
const { runTransaction, createTransactionContext } = require('./transaction');
|
|
12
|
+
const { createMigrationManager } = require('./migrations');
|
|
13
|
+
const { scaffoldMigration, scaffoldAlterMigration, scaffoldDropMigration } = require('./migrations/scaffold');
|
|
14
|
+
const { createScopeContext } = require('./scopes');
|
|
15
|
+
const { createSeeder } = require('./seeder');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a database instance
|
|
19
|
+
* @param {import('./types').DatabaseConfig} config - Database configuration
|
|
20
|
+
* @returns {import('./types').DatabaseInstance}
|
|
21
|
+
*/
|
|
22
|
+
function createDatabase(config) {
|
|
23
|
+
// Lazy load knex to avoid requiring it if ORM is not used
|
|
24
|
+
let knex;
|
|
25
|
+
try {
|
|
26
|
+
knex = require('knex');
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error('Knex is required for ORM. Install it with: npm install knex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create Knex instance
|
|
32
|
+
const knexInstance = knex(config);
|
|
33
|
+
|
|
34
|
+
// Create migration manager
|
|
35
|
+
const migrationConfig = config.migrations || {};
|
|
36
|
+
const migrate = createMigrationManager(knexInstance, migrationConfig);
|
|
37
|
+
|
|
38
|
+
// Default scope context
|
|
39
|
+
let globalScopeContext = createScopeContext();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set global tenant ID
|
|
43
|
+
* @param {*} tenantId - Tenant ID
|
|
44
|
+
* @returns {DatabaseInstance}
|
|
45
|
+
*/
|
|
46
|
+
function forTenant(tenantId) {
|
|
47
|
+
globalScopeContext.tenantId = tenantId;
|
|
48
|
+
return db;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a repository for a model
|
|
53
|
+
* @param {import('./types').ModelDefinition} model - Model definition
|
|
54
|
+
* @returns {import('./types').Repository}
|
|
55
|
+
*/
|
|
56
|
+
function createRepo(model) {
|
|
57
|
+
return createRepository(model, knexInstance, { ...globalScopeContext });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run a callback within a transaction
|
|
62
|
+
* @param {function(import('./types').TransactionContext): Promise<*>} callback
|
|
63
|
+
* @returns {Promise<*>}
|
|
64
|
+
*/
|
|
65
|
+
function transaction(callback) {
|
|
66
|
+
return runTransaction(knexInstance, callback, { ...globalScopeContext });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get raw Knex instance for advanced queries
|
|
71
|
+
* @returns {import('knex').Knex}
|
|
72
|
+
*/
|
|
73
|
+
function raw() {
|
|
74
|
+
return knexInstance;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Close all database connections
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
async function destroy() {
|
|
82
|
+
await knexInstance.destroy();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a seeder instance
|
|
87
|
+
* @param {Object} faker - Faker instance (@faker-js/faker)
|
|
88
|
+
* @returns {Object} Seeder API
|
|
89
|
+
*/
|
|
90
|
+
function seeder(faker) {
|
|
91
|
+
return createSeeder(faker, knexInstance);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const db = {
|
|
95
|
+
knex: knexInstance,
|
|
96
|
+
createRepository: createRepo,
|
|
97
|
+
transaction,
|
|
98
|
+
migrate,
|
|
99
|
+
seeder,
|
|
100
|
+
forTenant,
|
|
101
|
+
raw,
|
|
102
|
+
destroy,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return db;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Export everything
|
|
109
|
+
module.exports = {
|
|
110
|
+
// Main factory
|
|
111
|
+
createDatabase,
|
|
112
|
+
|
|
113
|
+
// Schema helpers
|
|
114
|
+
createSchemaHelpers,
|
|
115
|
+
extractColumnsFromSchema,
|
|
116
|
+
getColumnMeta,
|
|
117
|
+
|
|
118
|
+
// Model
|
|
119
|
+
defineModel,
|
|
120
|
+
getModel,
|
|
121
|
+
getAllModels,
|
|
122
|
+
hasModel,
|
|
123
|
+
clearRegistry,
|
|
124
|
+
|
|
125
|
+
// Repository (for direct use if needed)
|
|
126
|
+
createRepository,
|
|
127
|
+
|
|
128
|
+
// Query builder
|
|
129
|
+
createQueryBuilder,
|
|
130
|
+
QueryBuilder,
|
|
131
|
+
|
|
132
|
+
// Transaction
|
|
133
|
+
runTransaction,
|
|
134
|
+
createTransactionContext,
|
|
135
|
+
|
|
136
|
+
// Migrations
|
|
137
|
+
createMigrationManager,
|
|
138
|
+
scaffoldMigration,
|
|
139
|
+
scaffoldAlterMigration,
|
|
140
|
+
scaffoldDropMigration,
|
|
141
|
+
|
|
142
|
+
// Scopes
|
|
143
|
+
createScopeContext,
|
|
144
|
+
|
|
145
|
+
// Seeder
|
|
146
|
+
createSeeder,
|
|
147
|
+
};
|
|
148
|
+
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Migration Manager
|
|
3
|
+
* Wraps Knex migrations API
|
|
4
|
+
* @module core/orm/migrations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { generateMigrationTimestamp } = require('../utils');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a migration manager
|
|
11
|
+
* @param {import('knex').Knex} knex - Knex instance
|
|
12
|
+
* @param {import('../types').MigrationConfig} config - Migration configuration
|
|
13
|
+
* @returns {import('../types').MigrationManager}
|
|
14
|
+
*/
|
|
15
|
+
function createMigrationManager(knex, config = {}) {
|
|
16
|
+
const {
|
|
17
|
+
directory = './migrations',
|
|
18
|
+
tableName = 'knex_migrations',
|
|
19
|
+
} = config;
|
|
20
|
+
|
|
21
|
+
const migrationConfig = {
|
|
22
|
+
directory,
|
|
23
|
+
tableName,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
/**
|
|
28
|
+
* Run all pending migrations
|
|
29
|
+
* @returns {Promise<import('../types').MigrationResult>}
|
|
30
|
+
*/
|
|
31
|
+
async latest() {
|
|
32
|
+
const [batch, migrations] = await knex.migrate.latest(migrationConfig);
|
|
33
|
+
return {
|
|
34
|
+
batch,
|
|
35
|
+
migrations,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Rollback migrations
|
|
41
|
+
* @param {Object} [options={}]
|
|
42
|
+
* @param {boolean} [options.all=false] - Rollback all migrations
|
|
43
|
+
* @returns {Promise<import('../types').MigrationResult>}
|
|
44
|
+
*/
|
|
45
|
+
async rollback(options = {}) {
|
|
46
|
+
const rollbackConfig = {
|
|
47
|
+
...migrationConfig,
|
|
48
|
+
...(options.all ? { all: true } : {}),
|
|
49
|
+
};
|
|
50
|
+
const [batch, migrations] = await knex.migrate.rollback(rollbackConfig);
|
|
51
|
+
return {
|
|
52
|
+
batch,
|
|
53
|
+
migrations,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get current migration version
|
|
59
|
+
* @returns {Promise<string>}
|
|
60
|
+
*/
|
|
61
|
+
async currentVersion() {
|
|
62
|
+
return knex.migrate.currentVersion(migrationConfig);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get migration status
|
|
67
|
+
* @returns {Promise<import('../types').MigrationStatus[]>}
|
|
68
|
+
*/
|
|
69
|
+
async status() {
|
|
70
|
+
// Get completed migrations from database
|
|
71
|
+
const completedResult = await knex.migrate.list(migrationConfig);
|
|
72
|
+
const [completed, pending] = completedResult;
|
|
73
|
+
|
|
74
|
+
const statuses = [];
|
|
75
|
+
|
|
76
|
+
// Add completed migrations
|
|
77
|
+
for (const migration of completed) {
|
|
78
|
+
statuses.push({
|
|
79
|
+
name: migration.name || migration,
|
|
80
|
+
completed: true,
|
|
81
|
+
ran_at: migration.migration_time || null,
|
|
82
|
+
batch: migration.batch || null,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Add pending migrations
|
|
87
|
+
for (const migration of pending) {
|
|
88
|
+
statuses.push({
|
|
89
|
+
name: migration.name || migration,
|
|
90
|
+
completed: false,
|
|
91
|
+
ran_at: null,
|
|
92
|
+
batch: null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Sort by name
|
|
97
|
+
statuses.sort((a, b) => a.name.localeCompare(b.name));
|
|
98
|
+
|
|
99
|
+
return statuses;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a new migration file
|
|
104
|
+
* @param {string} name - Migration name
|
|
105
|
+
* @param {Object} [options={}]
|
|
106
|
+
* @param {string} [options.content] - Custom migration content
|
|
107
|
+
* @returns {Promise<string>} Created file path
|
|
108
|
+
*/
|
|
109
|
+
async make(name, options = {}) {
|
|
110
|
+
const { content } = options;
|
|
111
|
+
|
|
112
|
+
if (content) {
|
|
113
|
+
// Use custom stub with content
|
|
114
|
+
const timestamp = generateMigrationTimestamp();
|
|
115
|
+
const filename = `${timestamp}_${name}.js`;
|
|
116
|
+
|
|
117
|
+
// Knex's make doesn't support custom content directly,
|
|
118
|
+
// so we return the filename and content for the CLI to write
|
|
119
|
+
return {
|
|
120
|
+
filename,
|
|
121
|
+
filepath: `${directory}/${filename}`,
|
|
122
|
+
content,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Use default Knex make
|
|
127
|
+
const result = await knex.migrate.make(name, migrationConfig);
|
|
128
|
+
return {
|
|
129
|
+
filename: result.split('/').pop(),
|
|
130
|
+
filepath: result,
|
|
131
|
+
content: null,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run specific migration up
|
|
137
|
+
* @param {string} name - Migration name
|
|
138
|
+
* @returns {Promise<void>}
|
|
139
|
+
*/
|
|
140
|
+
async up(name) {
|
|
141
|
+
await knex.migrate.up({ ...migrationConfig, name });
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Run specific migration down
|
|
146
|
+
* @param {string} name - Migration name
|
|
147
|
+
* @returns {Promise<void>}
|
|
148
|
+
*/
|
|
149
|
+
async down(name) {
|
|
150
|
+
await knex.migrate.down({ ...migrationConfig, name });
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the migration configuration
|
|
155
|
+
* @returns {Object}
|
|
156
|
+
*/
|
|
157
|
+
getConfig() {
|
|
158
|
+
return { ...migrationConfig };
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if migrations table exists
|
|
163
|
+
* @returns {Promise<boolean>}
|
|
164
|
+
*/
|
|
165
|
+
async hasTable() {
|
|
166
|
+
return knex.schema.hasTable(tableName);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Unlock stuck migrations
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async unlock() {
|
|
174
|
+
await knex.migrate.forceFreeMigrationsLock(migrationConfig);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Default migration template
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
function getDefaultMigrationTemplate() {
|
|
184
|
+
return `/**
|
|
185
|
+
* Migration:
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
exports.up = function(knex) {
|
|
189
|
+
return knex.schema.createTable('table_name', (table) => {
|
|
190
|
+
table.bigIncrements('id').primary();
|
|
191
|
+
table.timestamps(true, true);
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
exports.down = function(knex) {
|
|
196
|
+
return knex.schema.dropTableIfExists('table_name');
|
|
197
|
+
};
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
createMigrationManager,
|
|
203
|
+
getDefaultMigrationTemplate,
|
|
204
|
+
};
|
|
205
|
+
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Migration Scaffolding
|
|
3
|
+
* Generate migration code from model schema
|
|
4
|
+
* @module core/orm/migrations/scaffold
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { getColumnMeta } = require('../schema-helpers');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate migration code from a model definition
|
|
11
|
+
* @param {import('../types').ModelDefinition} model - Model definition
|
|
12
|
+
* @returns {string} Migration file content
|
|
13
|
+
*/
|
|
14
|
+
function scaffoldMigration(model) {
|
|
15
|
+
const { table, columns, scopes } = model;
|
|
16
|
+
|
|
17
|
+
const columnLines = [];
|
|
18
|
+
const indexLines = [];
|
|
19
|
+
const foreignKeyLines = [];
|
|
20
|
+
|
|
21
|
+
// Process each column
|
|
22
|
+
for (const [columnName, meta] of columns.entries()) {
|
|
23
|
+
const { line, indexLine, fkLine } = generateColumnLine(columnName, meta);
|
|
24
|
+
columnLines.push(line);
|
|
25
|
+
if (indexLine) indexLines.push(indexLine);
|
|
26
|
+
if (fkLine) foreignKeyLines.push(fkLine);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Generate the migration content
|
|
30
|
+
const lines = [
|
|
31
|
+
'/**',
|
|
32
|
+
` * Migration: Create ${table} table`,
|
|
33
|
+
' * Auto-generated from model schema',
|
|
34
|
+
' */',
|
|
35
|
+
'',
|
|
36
|
+
'exports.up = function(knex) {',
|
|
37
|
+
` return knex.schema.createTable('${table}', (table) => {`,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// Add column definitions
|
|
41
|
+
for (const line of columnLines) {
|
|
42
|
+
lines.push(` ${line}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add indexes
|
|
46
|
+
if (indexLines.length > 0) {
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(' // Indexes');
|
|
49
|
+
for (const line of indexLines) {
|
|
50
|
+
lines.push(` ${line}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Add foreign keys
|
|
55
|
+
if (foreignKeyLines.length > 0) {
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(' // Foreign keys');
|
|
58
|
+
for (const line of foreignKeyLines) {
|
|
59
|
+
lines.push(` ${line}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lines.push(' });');
|
|
64
|
+
lines.push('};');
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push('exports.down = function(knex) {');
|
|
67
|
+
lines.push(` return knex.schema.dropTableIfExists('${table}');`);
|
|
68
|
+
lines.push('};');
|
|
69
|
+
lines.push('');
|
|
70
|
+
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate a single column line for migration
|
|
76
|
+
* @param {string} columnName - Column name
|
|
77
|
+
* @param {import('../types').ColumnMeta} meta - Column metadata
|
|
78
|
+
* @returns {{ line: string, indexLine: string|null, fkLine: string|null }}
|
|
79
|
+
*/
|
|
80
|
+
function generateColumnLine(columnName, meta) {
|
|
81
|
+
const parts = [];
|
|
82
|
+
let indexLine = null;
|
|
83
|
+
let fkLine = null;
|
|
84
|
+
|
|
85
|
+
// Determine column type and method
|
|
86
|
+
switch (meta.type) {
|
|
87
|
+
case 'bigint':
|
|
88
|
+
if (meta.primary && meta.autoIncrement) {
|
|
89
|
+
parts.push(`table.bigIncrements('${columnName}')`);
|
|
90
|
+
} else if (meta.references) {
|
|
91
|
+
parts.push(`table.bigInteger('${columnName}').unsigned()`);
|
|
92
|
+
fkLine = `table.foreign('${columnName}').references('${meta.referenceColumn || 'id'}').inTable('${meta.references}');`;
|
|
93
|
+
} else {
|
|
94
|
+
parts.push(`table.bigInteger('${columnName}')`);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'integer':
|
|
99
|
+
if (meta.primary && meta.autoIncrement) {
|
|
100
|
+
parts.push(`table.increments('${columnName}')`);
|
|
101
|
+
} else {
|
|
102
|
+
parts.push(`table.integer('${columnName}')`);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'string':
|
|
107
|
+
const maxLength = meta.maxLength || 255;
|
|
108
|
+
parts.push(`table.string('${columnName}', ${maxLength})`);
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case 'text':
|
|
112
|
+
parts.push(`table.text('${columnName}')`);
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'float':
|
|
116
|
+
parts.push(`table.float('${columnName}')`);
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case 'decimal':
|
|
120
|
+
const precision = meta.precision || 10;
|
|
121
|
+
const scale = meta.scale || 2;
|
|
122
|
+
parts.push(`table.decimal('${columnName}', ${precision}, ${scale})`);
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'boolean':
|
|
126
|
+
parts.push(`table.boolean('${columnName}')`);
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'date':
|
|
130
|
+
parts.push(`table.date('${columnName}')`);
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'datetime':
|
|
134
|
+
parts.push(`table.datetime('${columnName}')`);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'timestamp':
|
|
138
|
+
parts.push(`table.timestamp('${columnName}')`);
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'json':
|
|
142
|
+
parts.push(`table.json('${columnName}')`);
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'enum':
|
|
146
|
+
const enumValues = meta.enumValues || [];
|
|
147
|
+
const valuesStr = enumValues.map(v => `'${v}'`).join(', ');
|
|
148
|
+
parts.push(`table.enum('${columnName}', [${valuesStr}])`);
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'uuid':
|
|
152
|
+
if (meta.primary) {
|
|
153
|
+
parts.push(`table.uuid('${columnName}')`);
|
|
154
|
+
} else if (meta.references) {
|
|
155
|
+
parts.push(`table.uuid('${columnName}')`);
|
|
156
|
+
fkLine = `table.foreign('${columnName}').references('${meta.referenceColumn || 'id'}').inTable('${meta.references}');`;
|
|
157
|
+
} else {
|
|
158
|
+
parts.push(`table.uuid('${columnName}')`);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
default:
|
|
163
|
+
parts.push(`table.string('${columnName}')`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Add constraints
|
|
167
|
+
if (meta.primary && !meta.autoIncrement) {
|
|
168
|
+
parts.push('.primary()');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (meta.unique) {
|
|
172
|
+
parts.push('.unique()');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (meta.nullable) {
|
|
176
|
+
parts.push('.nullable()');
|
|
177
|
+
} else if (!meta.primary) {
|
|
178
|
+
parts.push('.notNullable()');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (meta.default !== undefined) {
|
|
182
|
+
if (typeof meta.default === 'string') {
|
|
183
|
+
parts.push(`.defaultTo('${meta.default}')`);
|
|
184
|
+
} else if (meta.default === null) {
|
|
185
|
+
parts.push('.defaultTo(null)');
|
|
186
|
+
} else {
|
|
187
|
+
parts.push(`.defaultTo(${meta.default})`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Auto timestamps get default to knex.fn.now()
|
|
192
|
+
if (meta.auto === 'create' || meta.auto === 'update') {
|
|
193
|
+
parts.push('.defaultTo(knex.fn.now())');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Generate index line if needed
|
|
197
|
+
if (meta.index && !meta.unique && !meta.primary) {
|
|
198
|
+
indexLine = `table.index(['${columnName}']);`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
line: parts.join('') + ';',
|
|
203
|
+
indexLine,
|
|
204
|
+
fkLine,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate migration code for adding columns to existing table
|
|
210
|
+
* @param {string} tableName - Table name
|
|
211
|
+
* @param {Map<string, import('../types').ColumnMeta>} columns - Columns to add
|
|
212
|
+
* @returns {string} Migration file content
|
|
213
|
+
*/
|
|
214
|
+
function scaffoldAlterMigration(tableName, columns) {
|
|
215
|
+
const columnLines = [];
|
|
216
|
+
const indexLines = [];
|
|
217
|
+
const foreignKeyLines = [];
|
|
218
|
+
const dropLines = [];
|
|
219
|
+
|
|
220
|
+
for (const [columnName, meta] of columns.entries()) {
|
|
221
|
+
const { line, indexLine, fkLine } = generateColumnLine(columnName, meta);
|
|
222
|
+
columnLines.push(line);
|
|
223
|
+
if (indexLine) indexLines.push(indexLine);
|
|
224
|
+
if (fkLine) foreignKeyLines.push(fkLine);
|
|
225
|
+
dropLines.push(`table.dropColumn('${columnName}');`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const lines = [
|
|
229
|
+
'/**',
|
|
230
|
+
` * Migration: Alter ${tableName} table`,
|
|
231
|
+
' * Auto-generated',
|
|
232
|
+
' */',
|
|
233
|
+
'',
|
|
234
|
+
'exports.up = function(knex) {',
|
|
235
|
+
` return knex.schema.alterTable('${tableName}', (table) => {`,
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
for (const line of columnLines) {
|
|
239
|
+
lines.push(` ${line}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (indexLines.length > 0) {
|
|
243
|
+
lines.push('');
|
|
244
|
+
for (const line of indexLines) {
|
|
245
|
+
lines.push(` ${line}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (foreignKeyLines.length > 0) {
|
|
250
|
+
lines.push('');
|
|
251
|
+
for (const line of foreignKeyLines) {
|
|
252
|
+
lines.push(` ${line}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
lines.push(' });');
|
|
257
|
+
lines.push('};');
|
|
258
|
+
lines.push('');
|
|
259
|
+
lines.push('exports.down = function(knex) {');
|
|
260
|
+
lines.push(` return knex.schema.alterTable('${tableName}', (table) => {`);
|
|
261
|
+
|
|
262
|
+
for (const line of dropLines) {
|
|
263
|
+
lines.push(` ${line}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
lines.push(' });');
|
|
267
|
+
lines.push('};');
|
|
268
|
+
lines.push('');
|
|
269
|
+
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generate migration code for a drop table
|
|
275
|
+
* @param {string} tableName - Table name
|
|
276
|
+
* @returns {string} Migration file content
|
|
277
|
+
*/
|
|
278
|
+
function scaffoldDropMigration(tableName) {
|
|
279
|
+
return `/**
|
|
280
|
+
* Migration: Drop ${tableName} table
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
exports.up = function(knex) {
|
|
284
|
+
return knex.schema.dropTableIfExists('${tableName}');
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
exports.down = function(knex) {
|
|
288
|
+
// Note: This down migration is empty because we don't know the original schema.
|
|
289
|
+
// If you need to restore the table, please add the schema manually.
|
|
290
|
+
return Promise.resolve();
|
|
291
|
+
};
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Generate migration name from model
|
|
297
|
+
* @param {import('../types').ModelDefinition} model - Model definition
|
|
298
|
+
* @param {string} [action='create'] - Action (create, alter, drop)
|
|
299
|
+
* @returns {string}
|
|
300
|
+
*/
|
|
301
|
+
function generateMigrationName(model, action = 'create') {
|
|
302
|
+
return `${action}_${model.table}_table`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
scaffoldMigration,
|
|
307
|
+
scaffoldAlterMigration,
|
|
308
|
+
scaffoldDropMigration,
|
|
309
|
+
generateColumnLine,
|
|
310
|
+
generateMigrationName,
|
|
311
|
+
};
|
|
312
|
+
|