webspresso 0.0.6 → 0.0.7
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 +494 -0
- package/bin/webspresso.js +255 -0
- 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,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
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Model Definition
|
|
3
|
+
* Define models and maintain a registry
|
|
4
|
+
* @module core/orm/model
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { extractColumnsFromSchema } = require('./schema-helpers');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Global model registry
|
|
11
|
+
* @type {Map<string, import('./types').ModelDefinition>}
|
|
12
|
+
*/
|
|
13
|
+
const modelRegistry = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Define a new model
|
|
17
|
+
* @param {import('./types').ModelOptions} options - Model configuration
|
|
18
|
+
* @returns {import('./types').ModelDefinition}
|
|
19
|
+
*/
|
|
20
|
+
function defineModel(options) {
|
|
21
|
+
const {
|
|
22
|
+
name,
|
|
23
|
+
table,
|
|
24
|
+
schema,
|
|
25
|
+
primaryKey = 'id',
|
|
26
|
+
relations = {},
|
|
27
|
+
scopes = {},
|
|
28
|
+
} = options;
|
|
29
|
+
|
|
30
|
+
// Validate required fields
|
|
31
|
+
if (!name || typeof name !== 'string') {
|
|
32
|
+
throw new Error('Model name is required and must be a string');
|
|
33
|
+
}
|
|
34
|
+
if (!table || typeof table !== 'string') {
|
|
35
|
+
throw new Error('Model table is required and must be a string');
|
|
36
|
+
}
|
|
37
|
+
if (!schema || typeof schema.parse !== 'function') {
|
|
38
|
+
throw new Error('Model schema is required and must be a Zod schema');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for duplicate registration
|
|
42
|
+
if (modelRegistry.has(name)) {
|
|
43
|
+
throw new Error(`Model "${name}" is already defined`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract column metadata from schema
|
|
47
|
+
const columns = extractColumnsFromSchema(schema);
|
|
48
|
+
|
|
49
|
+
// Validate relations
|
|
50
|
+
for (const [relationName, relation] of Object.entries(relations)) {
|
|
51
|
+
if (!['belongsTo', 'hasMany', 'hasOne'].includes(relation.type)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Invalid relation type "${relation.type}" for "${relationName}" in model "${name}"`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (typeof relation.model !== 'function') {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Relation "${relationName}" in model "${name}" must have a model function`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!relation.foreignKey || typeof relation.foreignKey !== 'string') {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Relation "${relationName}" in model "${name}" must have a foreignKey string`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create model definition
|
|
69
|
+
const model = {
|
|
70
|
+
name,
|
|
71
|
+
table,
|
|
72
|
+
schema,
|
|
73
|
+
primaryKey,
|
|
74
|
+
relations,
|
|
75
|
+
scopes: {
|
|
76
|
+
softDelete: scopes.softDelete || false,
|
|
77
|
+
timestamps: scopes.timestamps || false,
|
|
78
|
+
tenant: scopes.tenant || null,
|
|
79
|
+
},
|
|
80
|
+
columns,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Register model
|
|
84
|
+
modelRegistry.set(name, model);
|
|
85
|
+
|
|
86
|
+
return model;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get a model by name
|
|
91
|
+
* @param {string} name - Model name
|
|
92
|
+
* @returns {import('./types').ModelDefinition|undefined}
|
|
93
|
+
*/
|
|
94
|
+
function getModel(name) {
|
|
95
|
+
return modelRegistry.get(name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get all registered models
|
|
100
|
+
* @returns {Map<string, import('./types').ModelDefinition>}
|
|
101
|
+
*/
|
|
102
|
+
function getAllModels() {
|
|
103
|
+
return new Map(modelRegistry);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a model exists
|
|
108
|
+
* @param {string} name - Model name
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
function hasModel(name) {
|
|
112
|
+
return modelRegistry.has(name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clear the model registry (useful for testing)
|
|
117
|
+
*/
|
|
118
|
+
function clearRegistry() {
|
|
119
|
+
modelRegistry.clear();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Unregister a model by name
|
|
124
|
+
* @param {string} name - Model name
|
|
125
|
+
* @returns {boolean} Whether the model was removed
|
|
126
|
+
*/
|
|
127
|
+
function unregisterModel(name) {
|
|
128
|
+
return modelRegistry.delete(name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a relation's model (handles lazy loading)
|
|
133
|
+
* @param {import('./types').RelationDefinition} relation - Relation definition
|
|
134
|
+
* @returns {import('./types').ModelDefinition}
|
|
135
|
+
*/
|
|
136
|
+
function resolveRelationModel(relation) {
|
|
137
|
+
const model = relation.model();
|
|
138
|
+
if (!model || !model.name) {
|
|
139
|
+
throw new Error('Invalid relation model reference');
|
|
140
|
+
}
|
|
141
|
+
return model;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the foreign key column info for a relation
|
|
146
|
+
* @param {import('./types').ModelDefinition} model - Parent model
|
|
147
|
+
* @param {string} relationName - Relation name
|
|
148
|
+
* @returns {{ localKey: string, foreignKey: string, relatedModel: import('./types').ModelDefinition }}
|
|
149
|
+
*/
|
|
150
|
+
function getRelationKeys(model, relationName) {
|
|
151
|
+
const relation = model.relations[relationName];
|
|
152
|
+
if (!relation) {
|
|
153
|
+
throw new Error(`Relation "${relationName}" not found on model "${model.name}"`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const relatedModel = resolveRelationModel(relation);
|
|
157
|
+
const localKey = relation.localKey || model.primaryKey;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
localKey,
|
|
161
|
+
foreignKey: relation.foreignKey,
|
|
162
|
+
relatedModel,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
defineModel,
|
|
168
|
+
getModel,
|
|
169
|
+
getAllModels,
|
|
170
|
+
hasModel,
|
|
171
|
+
clearRegistry,
|
|
172
|
+
unregisterModel,
|
|
173
|
+
resolveRelationModel,
|
|
174
|
+
getRelationKeys,
|
|
175
|
+
// Export registry for testing
|
|
176
|
+
_registry: modelRegistry,
|
|
177
|
+
};
|
|
178
|
+
|