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