webspresso 0.0.17 → 0.0.19

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 CHANGED
@@ -808,13 +808,16 @@ const User = defineModel({
808
808
  scopes: { softDelete: true, timestamps: true },
809
809
  });
810
810
 
811
- // 4. Create database and use
811
+ // 4. Create database (models auto-loaded from ./models directory)
812
812
  const db = createDatabase({
813
813
  client: 'pg',
814
814
  connection: process.env.DATABASE_URL,
815
+ models: './models', // Optional, defaults to './models'
815
816
  });
816
817
 
817
- const UserRepo = db.createRepository(User);
818
+ // Models are automatically loaded from models/ directory
819
+ // Use getRepository with model name
820
+ const UserRepo = db.getRepository('User');
818
821
  const user = await UserRepo.findById(1, { with: ['company', 'posts'] });
819
822
  ```
820
823
 
@@ -880,11 +883,48 @@ const User = defineModel({
880
883
  });
881
884
  ```
882
885
 
886
+ ### Auto-Loading Models
887
+
888
+ Models are automatically loaded from the `models/` directory when you create a database instance:
889
+
890
+ ```javascript
891
+ // models/User.js
892
+ const { defineModel, zdb } = require('webspresso');
893
+
894
+ module.exports = defineModel({
895
+ name: 'User',
896
+ table: 'users',
897
+ schema: zdb.schema({
898
+ id: zdb.id(),
899
+ email: zdb.string({ unique: true }),
900
+ name: zdb.string(),
901
+ created_at: zdb.timestamp({ auto: 'create' }),
902
+ updated_at: zdb.timestamp({ auto: 'update' }),
903
+ }),
904
+ });
905
+
906
+ // In your application code
907
+ const db = createDatabase({
908
+ client: 'pg',
909
+ connection: process.env.DATABASE_URL,
910
+ models: './models', // Optional, defaults to './models'
911
+ });
912
+
913
+ // Models are automatically loaded, use getRepository with model name
914
+ const UserRepo = db.getRepository('User');
915
+ ```
916
+
917
+ **Model File Structure:**
918
+ - Place model files in `models/` directory (or custom path via `config.models`)
919
+ - Each file should export a model defined with `defineModel()`
920
+ - Files starting with `_` are ignored (useful for shared utilities)
921
+ - Models are loaded in alphabetical order
922
+
883
923
  ### Repository API
884
924
 
885
925
  ```javascript
886
926
  const db = createDatabase({ client: 'pg', connection: '...' });
887
- const UserRepo = db.createRepository(User);
927
+ const UserRepo = db.getRepository('User'); // Use model name string
888
928
 
889
929
  // Find by ID (with eager loading)
890
930
  const user = await UserRepo.findById(1, { with: ['company', 'posts'] });
@@ -974,8 +1014,8 @@ await UserRepo.query().forTenant(tenantId).list();
974
1014
 
975
1015
  ```javascript
976
1016
  await db.transaction(async (trx) => {
977
- const userRepo = trx.createRepository(User);
978
- const postRepo = trx.createRepository(Post);
1017
+ const userRepo = trx.getRepository('User'); // Use model name
1018
+ const postRepo = trx.getRepository('Post');
979
1019
 
980
1020
  const user = await userRepo.create({ email: 'new@test.com', name: 'New' });
981
1021
  await postRepo.create({ title: 'First Post', user_id: user.id });
@@ -6,6 +6,26 @@
6
6
  const fs = require('fs');
7
7
  const { spawn } = require('child_process');
8
8
 
9
+ /**
10
+ * Build node --watch arguments with additional watch paths
11
+ * @returns {string[]} Node arguments
12
+ */
13
+ function buildWatchArgs() {
14
+ const args = ['--watch'];
15
+
16
+ // Add watch paths for common directories
17
+ const watchPaths = ['pages', 'models', 'views'];
18
+
19
+ for (const dir of watchPaths) {
20
+ if (fs.existsSync(dir)) {
21
+ args.push(`--watch-path=./${dir}`);
22
+ }
23
+ }
24
+
25
+ args.push('server.js');
26
+ return args;
27
+ }
28
+
9
29
  function registerCommand(program) {
10
30
  program
11
31
  .command('dev')
@@ -24,9 +44,13 @@ function registerCommand(program) {
24
44
  const hasTailwind = fs.existsSync('tailwind.config.js') && fs.existsSync('src/input.css');
25
45
  const shouldWatchCss = hasTailwind && options.css !== false;
26
46
 
47
+ // Build watch arguments
48
+ const watchArgs = buildWatchArgs();
49
+ const watchDirs = watchArgs.filter(a => a.startsWith('--watch-path')).map(a => a.split('=')[1]);
50
+
27
51
  if (shouldWatchCss) {
28
52
  console.log(`\nšŸš€ Starting development server on port ${options.port}...`);
29
- console.log(' Watching CSS and server files...\n');
53
+ console.log(` Watching: server.js${watchDirs.length ? ', ' + watchDirs.join(', ') : ''}, CSS\n`);
30
54
 
31
55
  // Start CSS watch
32
56
  const cssWatch = spawn('npm', ['run', 'watch:css'], {
@@ -34,8 +58,8 @@ function registerCommand(program) {
34
58
  shell: true
35
59
  });
36
60
 
37
- // Start server
38
- const server = spawn('node', ['--watch', 'server.js'], {
61
+ // Start server with watch paths
62
+ const server = spawn('node', watchArgs, {
39
63
  stdio: 'inherit',
40
64
  shell: true,
41
65
  env: { ...process.env, PORT: options.port, NODE_ENV: 'development' }
@@ -54,10 +78,10 @@ function registerCommand(program) {
54
78
  cssWatch.on('exit', cleanup);
55
79
  server.on('exit', cleanup);
56
80
  } else {
57
- console.log(`\nšŸš€ Starting development server on port ${options.port}...\n`);
81
+ console.log(`\nšŸš€ Starting development server on port ${options.port}...`);
82
+ console.log(` Watching: server.js${watchDirs.length ? ', ' + watchDirs.join(', ') : ''}\n`);
58
83
 
59
- const { spawn } = require('child_process');
60
- const child = spawn('node', ['--watch', 'server.js'], {
84
+ const child = spawn('node', watchArgs, {
61
85
  stdio: 'inherit',
62
86
  shell: true,
63
87
  env: { ...process.env, PORT: options.port, NODE_ENV: 'development' }
@@ -172,7 +172,7 @@ function registerCommand(program) {
172
172
  description: 'Webspresso project',
173
173
  main: 'server.js',
174
174
  scripts: {
175
- dev: 'node --watch server.js',
175
+ dev: 'webspresso dev',
176
176
  start: 'NODE_ENV=production node server.js'
177
177
  },
178
178
  dependencies: {
@@ -545,7 +545,7 @@ module.exports = {
545
545
 
546
546
  updatedPackageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
547
547
  updatedPackageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
548
- updatedPackageJson.scripts.dev = 'npm run watch:css & node --watch server.js';
548
+ updatedPackageJson.scripts.dev = 'webspresso dev';
549
549
  updatedPackageJson.scripts.start = 'npm run build:css && NODE_ENV=production node server.js';
550
550
 
551
551
  fs.writeFileSync(
package/core/orm/index.js CHANGED
@@ -5,27 +5,13 @@
5
5
  */
6
6
 
7
7
  const path = require('path');
8
+ const fs = require('fs');
8
9
  const { createSchemaHelpers, extractColumnsFromSchema, getColumnMeta } = require('./schema-helpers');
9
10
  const { defineModel, getModel, getAllModels, hasModel, clearRegistry } = require('./model');
10
-
11
- // Create zdb instance with zod (zod is a dependency)
12
- let z;
13
- try {
14
- z = require('zod');
15
- } catch {
16
- // Zod not installed, zdb will be undefined
17
- z = null;
18
- }
19
-
20
- // Export zdb instance directly
21
- const zdb = z ? createSchemaHelpers(z) : null;
22
11
  const { createRepository } = require('./repository');
23
- const { createQueryBuilder, QueryBuilder } = require('./query-builder');
24
- const { runTransaction, createTransactionContext } = require('./transaction');
25
12
  const { createMigrationManager } = require('./migrations');
26
- const { scaffoldMigration, scaffoldAlterMigration, scaffoldDropMigration } = require('./migrations/scaffold');
27
- const { createScopeContext } = require('./scopes');
28
13
  const { createSeeder } = require('./seeder');
14
+ const { createScopeContext } = require('./scopes');
29
15
 
30
16
  /**
31
17
  * Create a database instance
@@ -52,74 +38,38 @@ function createDatabase(config) {
52
38
  };
53
39
 
54
40
  const driverName = driverMap[client] || client;
41
+ const projectNodeModules = path.join(process.cwd(), 'node_modules');
55
42
 
56
- // Try to resolve and pre-load the driver from project's node_modules
57
- // This ensures Knex can find it when it tries to load it
58
- let driverPath = null;
59
- const resolvePaths = [
60
- path.join(process.cwd(), 'node_modules'),
61
- process.cwd(),
62
- ];
63
-
64
- // Also try to resolve from parent directories (for nested projects)
65
- let currentPath = process.cwd();
66
- for (let i = 0; i < 5; i++) {
67
- resolvePaths.push(path.join(currentPath, 'node_modules'));
68
- const parent = path.dirname(currentPath);
69
- if (parent === currentPath) break; // Reached root
70
- currentPath = parent;
71
- }
72
-
73
- for (const resolvePath of resolvePaths) {
74
- try {
75
- driverPath = require.resolve(driverName, { paths: [resolvePath] });
76
- // Pre-load the driver so Knex can find it in Module._cache
77
- // This is critical: Knex uses require() internally, so we need to
78
- // load it into the cache first
79
- require(driverPath);
80
- break;
81
- } catch (e) {
82
- // Continue to next path
83
- }
84
- }
85
-
86
- // If still not found, try default resolution (might be in webspresso's node_modules)
87
- if (!driverPath) {
88
- try {
89
- // Try to find it anywhere in the module resolution path
90
- driverPath = require.resolve(driverName);
91
- require(driverPath);
92
- } catch (e) {
93
- // Driver not found anywhere
94
- const installCmd = driverName === 'better-sqlite3'
95
- ? 'npm install better-sqlite3 --save'
96
- : driverName === 'pg'
97
- ? 'npm install pg --save'
98
- : driverName === 'mysql2'
99
- ? 'npm install mysql2 --save'
100
- : `npm install ${driverName} --save`;
101
-
102
- throw new Error(
103
- `Database driver "${driverName}" is not installed in your project. ` +
104
- `Please install it with: ${installCmd}\n` +
105
- `Note: Database drivers are peer dependencies and must be installed in your project's node_modules, not globally.\n` +
106
- `Current working directory: ${process.cwd()}\n` +
107
- `Make sure you run "${installCmd}" in your project directory.`
108
- );
109
- }
43
+ // Try to find and pre-load driver from project's node_modules
44
+ try {
45
+ const driverPath = require.resolve(driverName, { paths: [projectNodeModules] });
46
+ require(driverPath); // Pre-load into Module._cache
47
+ } catch (e) {
48
+ // Driver not found in project
49
+ const installCmd = driverName === 'better-sqlite3'
50
+ ? 'npm install better-sqlite3 --save'
51
+ : driverName === 'pg'
52
+ ? 'npm install pg --save'
53
+ : driverName === 'mysql2'
54
+ ? 'npm install mysql2 --save'
55
+ : `npm install ${driverName} --save`;
56
+
57
+ throw new Error(
58
+ `Database driver "${driverName}" is not installed in your project.\n` +
59
+ `Please install it with: ${installCmd}\n` +
60
+ `Current working directory: ${process.cwd()}`
61
+ );
110
62
  }
111
63
  }
112
64
 
113
65
  // Create Knex instance
114
- // Knex will try to load the driver from its own node_modules
115
- // We need to ensure the driver is available in the project's node_modules
116
66
  let knexInstance;
117
67
  try {
118
68
  knexInstance = knex(config);
119
69
  } catch (e) {
120
- // If knex throws an error about missing driver, provide better message
121
- if (e.message && (e.message.includes('Cannot find module') || e.message.includes('run') || e.message.includes('npm install'))) {
122
- const driverName = config.client;
70
+ // Provide helpful error message
71
+ if (e.message && (e.message.includes('Cannot find module') || e.message.includes('npm install'))) {
72
+ const driverName = driverMap[config.client] || config.client;
123
73
  const installCmd = driverName === 'better-sqlite3'
124
74
  ? 'npm install better-sqlite3 --save'
125
75
  : driverName === 'pg'
@@ -128,61 +78,10 @@ function createDatabase(config) {
128
78
  ? 'npm install mysql2 --save'
129
79
  : `npm install ${driverName} --save`;
130
80
 
131
- // Check if driver exists in project's node_modules
132
- let driverExists = false;
133
- let foundDriverPath = null;
134
- const checkPaths = [
135
- path.join(process.cwd(), 'node_modules'),
136
- process.cwd(),
137
- ];
138
-
139
- // Also check parent directories
140
- let currentPath = process.cwd();
141
- for (let i = 0; i < 5; i++) {
142
- checkPaths.push(path.join(currentPath, 'node_modules'));
143
- const parent = path.dirname(currentPath);
144
- if (parent === currentPath) break;
145
- currentPath = parent;
146
- }
147
-
148
- for (const checkPath of checkPaths) {
149
- try {
150
- foundDriverPath = require.resolve(driverName, { paths: [checkPath] });
151
- driverExists = true;
152
- break;
153
- } catch (resolveError) {
154
- // Continue checking
155
- }
156
- }
157
-
158
- if (!driverExists) {
159
- throw new Error(
160
- `Database driver "${driverName}" is not installed in your project. ` +
161
- `Please install it with: ${installCmd}\n` +
162
- `Note: Database drivers are peer dependencies and must be installed in your project's node_modules, not globally.\n` +
163
- `Current working directory: ${process.cwd()}\n` +
164
- `Make sure you run "${installCmd}" in your project directory.`
165
- );
166
- } else {
167
- // Driver exists but Knex can't find it - try to manually require it
168
- try {
169
- // Force load the driver into Module._cache
170
- require(foundDriverPath);
171
- // Retry Knex initialization
172
- knexInstance = knex(config);
173
- } catch (retryError) {
174
- throw new Error(
175
- `Database driver "${driverName}" is installed but Knex cannot find it. ` +
176
- `This might be a module resolution issue. Try:\n` +
177
- `1. Delete node_modules and package-lock.json\n` +
178
- `2. Run "npm install" again\n` +
179
- `3. Make sure "${driverName}" is in your package.json dependencies\n` +
180
- `4. Verify the driver is accessible: node -e "require('${driverName}')"\n` +
181
- `Driver found at: ${foundDriverPath}\n` +
182
- `Original error: ${e.message}`
183
- );
184
- }
185
- }
81
+ throw new Error(
82
+ `Failed to initialize database: ${e.message}\n` +
83
+ `Make sure "${driverName}" is installed: ${installCmd}`
84
+ );
186
85
  }
187
86
  throw e;
188
87
  }
@@ -191,115 +90,185 @@ function createDatabase(config) {
191
90
  const migrationConfig = config.migrations || {};
192
91
  const migrate = createMigrationManager(knexInstance, migrationConfig);
193
92
 
194
- // Default scope context
195
- let globalScopeContext = createScopeContext();
93
+ // Auto-load models from models directory
94
+ const modelsDir = config.models || './models';
95
+ const absoluteModelsDir = path.resolve(process.cwd(), modelsDir);
96
+
97
+ if (fs.existsSync(absoluteModelsDir)) {
98
+ const modelFiles = fs.readdirSync(absoluteModelsDir)
99
+ .filter(file => file.endsWith('.js') && !file.startsWith('_'));
100
+
101
+ for (const file of modelFiles) {
102
+ try {
103
+ const modelPath = path.join(absoluteModelsDir, file);
104
+ require(modelPath);
105
+ } catch (error) {
106
+ console.error(`Error loading model from ${file}:`, error.message);
107
+ // Continue loading other models even if one fails
108
+ }
109
+ }
110
+ }
111
+
112
+ // Model registry (use global registry, but keep local for backward compatibility)
113
+ const models = new Map();
114
+
115
+ // Sync global registry to local registry
116
+ const globalModels = getAllModels();
117
+ for (const [name, model] of globalModels) {
118
+ models.set(name, model);
119
+ }
196
120
 
197
121
  /**
198
- * Set global tenant ID
199
- * @param {*} tenantId - Tenant ID
200
- * @returns {DatabaseInstance}
122
+ * Get a model by name
123
+ * @param {string} name - Model name
124
+ * @returns {import('./types').ModelDefinition}
201
125
  */
202
- function forTenant(tenantId) {
203
- globalScopeContext.tenantId = tenantId;
204
- return db;
126
+ function getModelInstance(name) {
127
+ // First check local registry
128
+ let model = models.get(name);
129
+
130
+ // If not found, check global registry (for models loaded after database creation)
131
+ if (!model) {
132
+ model = getModel(name);
133
+ if (model) {
134
+ models.set(name, model); // Cache in local registry
135
+ }
136
+ }
137
+
138
+ if (!model) {
139
+ throw new Error(`Model "${name}" is not defined. Make sure you've called defineModel() first or the model file exists in ${modelsDir}.`);
140
+ }
141
+ return model;
142
+ }
143
+
144
+ /**
145
+ * Check if a model exists
146
+ * @param {string} name - Model name
147
+ * @returns {boolean}
148
+ */
149
+ function hasModelInstance(name) {
150
+ return models.has(name);
205
151
  }
206
152
 
207
153
  /**
208
- * Create a repository for a model
154
+ * Get all registered models
155
+ * @returns {Array<import('./types').ModelDefinition>}
156
+ */
157
+ function getAllModelInstances() {
158
+ return Array.from(models.values());
159
+ }
160
+
161
+ /**
162
+ * Register a model
209
163
  * @param {import('./types').ModelDefinition} model - Model definition
164
+ */
165
+ function registerModel(model) {
166
+ models.set(model.name, model);
167
+ // Also ensure it's in global registry (defineModel already does this, but just in case)
168
+ }
169
+
170
+ /**
171
+ * Get repository for a model
172
+ * @param {string} modelName - Model name
173
+ * @param {import('./types').ScopeContext} [scopeContext] - Scope context
210
174
  * @returns {import('./types').Repository}
211
175
  */
212
- function createRepo(model) {
213
- return createRepository(model, knexInstance, { ...globalScopeContext });
176
+ function getRepository(modelName, scopeContext) {
177
+ const model = getModelInstance(modelName);
178
+ // Always create fresh scope context if not provided to avoid shared state
179
+ const ctx = scopeContext || createScopeContext();
180
+ return createRepository(model, knexInstance, ctx);
214
181
  }
215
182
 
216
183
  /**
217
- * Run a callback within a transaction
218
- * @param {function(import('./types').TransactionContext): Promise<*>} callback
219
- * @returns {Promise<*>}
184
+ * Get query builder for a model
185
+ * @param {string} modelName - Model name
186
+ * @param {import('./types').ScopeContext} [scopeContext] - Scope context
187
+ * @returns {import('knex').Knex.QueryBuilder}
220
188
  */
221
- function transaction(callback) {
222
- return runTransaction(knexInstance, callback, { ...globalScopeContext });
189
+ function query(modelName, scopeContext) {
190
+ const model = getModelInstance(modelName);
191
+ const ctx = scopeContext || createScopeContext();
192
+ const repo = createRepository(model, knexInstance, ctx);
193
+ return repo.query();
223
194
  }
224
195
 
225
196
  /**
226
- * Get raw Knex instance for advanced queries
227
- * @returns {import('knex').Knex}
197
+ * Create seeder instance
198
+ * @returns {import('./types').Seeder}
228
199
  */
229
- function raw() {
230
- return knexInstance;
200
+ function createSeederInstance() {
201
+ return createSeeder(knexInstance, models);
231
202
  }
232
203
 
233
204
  /**
234
- * Close all database connections
235
- * @returns {Promise<void>}
205
+ * Create repository from model object (alternative to getRepository)
206
+ * @param {import('./types').ModelDefinition} model - Model definition
207
+ * @param {import('./types').ScopeContext} [scopeContext] - Scope context
208
+ * @returns {import('./types').Repository}
236
209
  */
237
- async function destroy() {
238
- await knexInstance.destroy();
210
+ function createRepositoryFromModel(model, scopeContext) {
211
+ const ctx = scopeContext || createScopeContext();
212
+ return createRepository(model, knexInstance, ctx);
239
213
  }
240
214
 
241
215
  /**
242
- * Create a seeder instance
243
- * @param {Object} faker - Faker instance (@faker-js/faker)
244
- * @returns {Object} Seeder API
216
+ * Run operations in a transaction
217
+ * @param {Function} callback - Callback receiving transaction context
218
+ * @returns {Promise<*>} Result of callback
245
219
  */
246
- function seeder(faker) {
247
- return createSeeder(faker, knexInstance);
220
+ async function transaction(callback) {
221
+ return knexInstance.transaction(async (trx) => {
222
+ // Create transaction context with repository methods
223
+ const trxContext = {
224
+ trx,
225
+ getRepository(modelName, scopeContext) {
226
+ const model = getModelInstance(modelName);
227
+ const ctx = scopeContext || createScopeContext();
228
+ return createRepository(model, trx, ctx);
229
+ },
230
+ createRepository(model, scopeContext) {
231
+ const ctx = scopeContext || createScopeContext();
232
+ return createRepository(model, trx, ctx);
233
+ },
234
+ };
235
+ return callback(trxContext);
236
+ });
248
237
  }
249
238
 
250
- const db = {
239
+ return {
251
240
  knex: knexInstance,
252
- createRepository: createRepo,
253
- transaction,
254
241
  migrate,
255
- seeder,
256
- forTenant,
257
- raw,
258
- destroy,
242
+ getModel: getModelInstance,
243
+ hasModel: hasModelInstance,
244
+ getAllModels: getAllModelInstances,
245
+ registerModel,
246
+ getRepository,
247
+ createRepository: createRepositoryFromModel,
248
+ query,
249
+ transaction,
250
+ createSeeder: createSeederInstance,
251
+ destroy: () => knexInstance.destroy(),
259
252
  };
260
-
261
- return db;
262
253
  }
263
254
 
264
- // Export everything
255
+ // Export zdb instance directly
256
+ const z = require('zod');
257
+ const zdb = z ? createSchemaHelpers(z) : null;
258
+
265
259
  module.exports = {
266
260
  // Main factory
267
261
  createDatabase,
268
-
269
- // Schema helpers - zdb instance (direct export)
262
+ // Schema helpers
270
263
  zdb,
271
264
  createSchemaHelpers,
272
- extractColumnsFromSchema,
273
- getColumnMeta,
274
-
275
- // Model
265
+ // Model utilities
276
266
  defineModel,
277
267
  getModel,
278
268
  getAllModels,
279
269
  hasModel,
280
270
  clearRegistry,
281
-
282
- // Repository (for direct use if needed)
283
- createRepository,
284
-
285
- // Query builder
286
- createQueryBuilder,
287
- QueryBuilder,
288
-
289
- // Transaction
290
- runTransaction,
291
- createTransactionContext,
292
-
293
- // Migrations
294
- createMigrationManager,
295
- scaffoldMigration,
296
- scaffoldAlterMigration,
297
- scaffoldDropMigration,
298
-
299
- // Scopes
300
- createScopeContext,
301
-
302
- // Seeder
303
- createSeeder,
271
+ // Column utilities
272
+ extractColumnsFromSchema,
273
+ getColumnMeta,
304
274
  };
305
-
package/core/orm/model.js CHANGED
@@ -80,7 +80,7 @@ function defineModel(options) {
80
80
  },
81
81
  columns,
82
82
  admin: {
83
- enabled: admin.enabled || false,
83
+ enabled: admin.enabled === true, // Explicit boolean check
84
84
  label: admin.label || name,
85
85
  icon: admin.icon || null,
86
86
  customFields: admin.customFields || {},
package/core/orm/types.js CHANGED
@@ -215,6 +215,7 @@
215
215
 
216
216
  /**
217
217
  * @typedef {Object} DatabaseConfig
218
+ * @property {string} [models] - Path to models directory (default: './models')
218
219
  * @property {string} client - Database client ('pg', 'mysql2', 'better-sqlite3')
219
220
  * @property {string|Object} connection - Connection string or config object
220
221
  * @property {MigrationConfig} [migrations] - Migration configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@faker-js/faker": "^9.0.0",
53
- "better-sqlite3": "^9.0.0",
53
+ "better-sqlite3": "^11.10.0",
54
54
  "dotenv": "^16.0.0",
55
55
  "mysql2": "^3.0.0",
56
56
  "pg": "^8.0.0"
@@ -75,7 +75,7 @@
75
75
  "devDependencies": {
76
76
  "@faker-js/faker": "^9.3.0",
77
77
  "@vitest/coverage-v8": "^1.2.0",
78
- "better-sqlite3": "^11.0.0",
78
+ "better-sqlite3": "^11.10.0",
79
79
  "chokidar": "^3.5.3",
80
80
  "dotenv": "^16.3.1",
81
81
  "release-it": "^17.11.0",
@@ -25,7 +25,7 @@ function createApiHandlers(options) {
25
25
  // Get AdminUser repository
26
26
  let AdminUserRepo = null;
27
27
  if (db && AdminUser) {
28
- AdminUserRepo = db.createRepository(AdminUser);
28
+ AdminUserRepo = db.getRepository(AdminUser.name);
29
29
  }
30
30
 
31
31
  /**
@@ -131,7 +131,7 @@ function createApiHandlers(options) {
131
131
  const adminModels = [];
132
132
 
133
133
  for (const [name, model] of allModels) {
134
- if (model.admin && model.admin.enabled) {
134
+ if (model.admin && model.admin.enabled === true) {
135
135
  adminModels.push({
136
136
  name: model.name,
137
137
  table: model.table,
@@ -162,7 +162,7 @@ function createApiHandlers(options) {
162
162
  return res.status(404).json({ error: 'Model not found' });
163
163
  }
164
164
 
165
- if (!model.admin || !model.admin.enabled) {
165
+ if (!model.admin || model.admin.enabled !== true) {
166
166
  return res.status(403).json({ error: 'Model not enabled in admin panel' });
167
167
  }
168
168
 
@@ -204,11 +204,11 @@ function createApiHandlers(options) {
204
204
  const { model: modelName } = req.params;
205
205
  const model = getModel(modelName);
206
206
 
207
- if (!model || !model.admin || !model.admin.enabled) {
207
+ if (!model || !model.admin || model.admin.enabled !== true) {
208
208
  return res.status(404).json({ error: 'Model not found or not enabled' });
209
209
  }
210
210
 
211
- const repo = db.createRepository(model);
211
+ const repo = db.getRepository(model.name);
212
212
  const page = parseInt(req.query.page) || 1;
213
213
  const perPage = parseInt(req.query.perPage) || 15;
214
214
  const offset = (page - 1) * perPage;
@@ -270,11 +270,11 @@ function createApiHandlers(options) {
270
270
  const { model: modelName, id } = req.params;
271
271
  const model = getModel(modelName);
272
272
 
273
- if (!model || !model.admin || !model.admin.enabled) {
273
+ if (!model || !model.admin || model.admin.enabled !== true) {
274
274
  return res.status(404).json({ error: 'Model not found or not enabled' });
275
275
  }
276
276
 
277
- const repo = db.createRepository(model);
277
+ const repo = db.getRepository(model.name);
278
278
  const record = await repo.findById(id);
279
279
 
280
280
  if (!record) {
@@ -295,11 +295,11 @@ function createApiHandlers(options) {
295
295
  const { model: modelName } = req.params;
296
296
  const model = getModel(modelName);
297
297
 
298
- if (!model || !model.admin || !model.admin.enabled) {
298
+ if (!model || !model.admin || model.admin.enabled !== true) {
299
299
  return res.status(404).json({ error: 'Model not found or not enabled' });
300
300
  }
301
301
 
302
- const repo = db.createRepository(model);
302
+ const repo = db.getRepository(model.name);
303
303
  const record = await repo.create(req.body);
304
304
 
305
305
  res.status(201).json({ data: record });
@@ -316,11 +316,11 @@ function createApiHandlers(options) {
316
316
  const { model: modelName, id } = req.params;
317
317
  const model = getModel(modelName);
318
318
 
319
- if (!model || !model.admin || !model.admin.enabled) {
319
+ if (!model || !model.admin || model.admin.enabled !== true) {
320
320
  return res.status(404).json({ error: 'Model not found or not enabled' });
321
321
  }
322
322
 
323
- const repo = db.createRepository(model);
323
+ const repo = db.getRepository(model.name);
324
324
  const record = await repo.update(id, req.body);
325
325
 
326
326
  if (!record) {
@@ -341,11 +341,11 @@ function createApiHandlers(options) {
341
341
  const { model: modelName, id } = req.params;
342
342
  const model = getModel(modelName);
343
343
 
344
- if (!model || !model.admin || !model.admin.enabled) {
344
+ if (!model || !model.admin || model.admin.enabled !== true) {
345
345
  return res.status(404).json({ error: 'Model not found or not enabled' });
346
346
  }
347
347
 
348
- const repo = db.createRepository(model);
348
+ const repo = db.getRepository(model.name);
349
349
  const deleted = await repo.delete(id);
350
350
 
351
351
  if (!deleted) {
@@ -366,7 +366,7 @@ function createApiHandlers(options) {
366
366
  const { model: modelName, relation: relationName } = req.params;
367
367
  const model = getModel(modelName);
368
368
 
369
- if (!model || !model.admin || !model.admin.enabled) {
369
+ if (!model || !model.admin || model.admin.enabled !== true) {
370
370
  return res.status(404).json({ error: 'Model not found or not enabled' });
371
371
  }
372
372
 
@@ -376,7 +376,7 @@ function createApiHandlers(options) {
376
376
  }
377
377
 
378
378
  const relatedModel = relation.model();
379
- const relatedRepo = db.createRepository(relatedModel);
379
+ const relatedRepo = db.getRepository(relatedModel.name);
380
380
 
381
381
  // Get all related records (for dropdown/select)
382
382
  const records = await relatedRepo.findAll();
@@ -395,7 +395,7 @@ function createApiHandlers(options) {
395
395
  const { model: modelName, query: queryName } = req.params;
396
396
  const model = getModel(modelName);
397
397
 
398
- if (!model || !model.admin || !model.admin.enabled) {
398
+ if (!model || !model.admin || model.admin.enabled !== true) {
399
399
  return res.status(404).json({ error: 'Model not found or not enabled' });
400
400
  }
401
401
 
@@ -404,7 +404,7 @@ function createApiHandlers(options) {
404
404
  return res.status(404).json({ error: 'Query not found' });
405
405
  }
406
406
 
407
- const repo = db.createRepository(model);
407
+ const repo = db.getRepository(model.name);
408
408
  const result = await queryFn(repo);
409
409
 
410
410
  res.json({ data: result });
@@ -44,36 +44,40 @@ function adminPanelPlugin(options = {}) {
44
44
  /**
45
45
  * Register hook - called when plugin is registered
46
46
  */
47
- async register(ctx) {
48
- // Create and register AdminUser model (only if not already registered)
49
- const { getModel } = require('../../core/orm/model');
50
- let AdminUser = getModel('AdminUser');
51
-
52
- if (!AdminUser) {
53
- AdminUser = createAdminUserModel();
54
- }
55
-
56
- // Store in plugin context for later use
57
- this._adminUser = AdminUser;
58
- this._db = db;
59
- this._bcrypt = bcrypt;
60
- this._session = session;
61
- this._adminPath = adminPath;
47
+ register(ctx) {
48
+ // Ensure enabled is set
49
+ this.enabled = enabled;
62
50
  },
63
51
 
64
52
  /**
65
53
  * Routes ready hook - called after routes are mounted
66
54
  */
67
55
  onRoutesReady(ctx) {
68
- if (!this.enabled) {
56
+ // Use closure variable 'enabled' directly (plugin.enabled property)
57
+ if (!enabled) {
69
58
  return;
70
59
  }
71
60
 
72
61
  const { app } = ctx;
73
- const AdminUser = this._adminUser;
74
- const db = this._db;
75
- const bcrypt = this._bcrypt;
76
- const session = this._session;
62
+
63
+ // Create and register AdminUser model
64
+ // Check global registry first (defineModel adds to global registry)
65
+ const { hasModel: hasGlobalModel, getModel: getGlobalModel } = require('../../core/orm/model');
66
+ let AdminUser;
67
+
68
+ if (hasGlobalModel('AdminUser')) {
69
+ // Model exists in global registry
70
+ AdminUser = getGlobalModel('AdminUser');
71
+ // Ensure it's also in db's local registry
72
+ if (!db.hasModel('AdminUser')) {
73
+ db.registerModel(AdminUser);
74
+ }
75
+ } else {
76
+ // Create AdminUser model (adds to global registry)
77
+ AdminUser = createAdminUserModel();
78
+ // Also add to db's local registry
79
+ db.registerModel(AdminUser);
80
+ }
77
81
 
78
82
  // Setup session middleware (only once, even if multiple plugins)
79
83
  // Check if session middleware is already registered
@@ -127,7 +127,7 @@ class PluginManager {
127
127
  }
128
128
 
129
129
  /**
130
- * Register plugins with the manager
130
+ * Register plugins with the manager (async version)
131
131
  * @param {Array} plugins - Array of plugin definitions or factory functions
132
132
  * @param {Object} context - Context object { app, nunjucksEnv, options }
133
133
  */
@@ -155,6 +155,34 @@ class PluginManager {
155
155
  }
156
156
  }
157
157
 
158
+ /**
159
+ * Register plugins with the manager (sync version)
160
+ * @param {Array} plugins - Array of plugin definitions or factory functions
161
+ * @param {Object} context - Context object { app, nunjucksEnv, options }
162
+ */
163
+ registerSync(plugins, context) {
164
+ if (!plugins || !Array.isArray(plugins)) return;
165
+
166
+ this.app = context.app;
167
+ this.nunjucksEnv = context.nunjucksEnv;
168
+
169
+ // Normalize plugins (handle factory functions)
170
+ const normalizedPlugins = plugins.map(p => {
171
+ if (typeof p === 'function') {
172
+ return p;
173
+ }
174
+ return p;
175
+ });
176
+
177
+ // Validate and sort by dependencies
178
+ const sorted = this._resolveDependencyOrder(normalizedPlugins);
179
+
180
+ // Register each plugin in order (sync)
181
+ for (const plugin of sorted) {
182
+ this._registerPluginSync(plugin, context);
183
+ }
184
+ }
185
+
158
186
  /**
159
187
  * Resolve dependency order using topological sort
160
188
  */
@@ -213,7 +241,7 @@ class PluginManager {
213
241
  }
214
242
 
215
243
  /**
216
- * Register a single plugin
244
+ * Register a single plugin (async)
217
245
  */
218
246
  async _registerPlugin(plugin, context) {
219
247
  // Validate dependencies
@@ -244,6 +272,38 @@ class PluginManager {
244
272
  this._applyHelpersAndFilters();
245
273
  }
246
274
 
275
+ /**
276
+ * Register a single plugin (sync)
277
+ */
278
+ _registerPluginSync(plugin, context) {
279
+ // Validate dependencies
280
+ this._validateDependencies(plugin);
281
+
282
+ // Create plugin context
283
+ const ctx = this._createPluginContext(plugin, context);
284
+
285
+ // Store plugin
286
+ this.plugins.set(plugin.name, plugin);
287
+
288
+ // Store API if provided
289
+ if (plugin.api) {
290
+ // Bind API methods to plugin instance
291
+ const boundAPI = {};
292
+ for (const [key, value] of Object.entries(plugin.api)) {
293
+ boundAPI[key] = typeof value === 'function' ? value.bind(plugin) : value;
294
+ }
295
+ this.pluginAPIs.set(plugin.name, boundAPI);
296
+ }
297
+
298
+ // Call register hook (sync - if plugin has async register, it won't wait)
299
+ if (typeof plugin.register === 'function') {
300
+ plugin.register(ctx);
301
+ }
302
+
303
+ // Apply registered helpers to nunjucks
304
+ this._applyHelpersAndFilters();
305
+ }
306
+
247
307
  /**
248
308
  * Validate plugin dependencies
249
309
  */
package/src/server.js CHANGED
@@ -219,9 +219,9 @@ function createApp(options = {}) {
219
219
  return d.toString();
220
220
  });
221
221
 
222
- // Register plugins (synchronous part)
222
+ // Register plugins (sync)
223
223
  const pluginContext = { app, nunjucksEnv, options };
224
- pluginManager.register(plugins, pluginContext);
224
+ pluginManager.registerSync(plugins, pluginContext);
225
225
 
226
226
  // Request logging middleware
227
227
  if (logging) {