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 +45 -5
- package/bin/commands/dev.js +30 -6
- package/bin/commands/new.js +2 -2
- package/core/orm/index.js +168 -199
- package/core/orm/model.js +1 -1
- package/core/orm/types.js +1 -0
- package/package.json +3 -3
- package/plugins/admin-panel/api.js +17 -17
- package/plugins/admin-panel/index.js +24 -20
- package/src/plugin-manager.js +62 -2
- package/src/server.js +2 -2
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
978
|
-
const postRepo = trx.
|
|
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 });
|
package/bin/commands/dev.js
CHANGED
|
@@ -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(
|
|
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',
|
|
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}
|
|
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
|
|
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' }
|
package/bin/commands/new.js
CHANGED
|
@@ -172,7 +172,7 @@ function registerCommand(program) {
|
|
|
172
172
|
description: 'Webspresso project',
|
|
173
173
|
main: 'server.js',
|
|
174
174
|
scripts: {
|
|
175
|
-
dev: '
|
|
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 = '
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
121
|
-
if (e.message && (e.message.includes('Cannot find module') || e.message.includes('
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
//
|
|
195
|
-
|
|
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
|
-
*
|
|
199
|
-
* @param {
|
|
200
|
-
* @returns {
|
|
122
|
+
* Get a model by name
|
|
123
|
+
* @param {string} name - Model name
|
|
124
|
+
* @returns {import('./types').ModelDefinition}
|
|
201
125
|
*/
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
*
|
|
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
|
|
213
|
-
|
|
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
|
-
*
|
|
218
|
-
* @param {
|
|
219
|
-
* @
|
|
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
|
|
222
|
-
|
|
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
|
-
*
|
|
227
|
-
* @returns {import('
|
|
197
|
+
* Create seeder instance
|
|
198
|
+
* @returns {import('./types').Seeder}
|
|
228
199
|
*/
|
|
229
|
-
function
|
|
230
|
-
return knexInstance;
|
|
200
|
+
function createSeederInstance() {
|
|
201
|
+
return createSeeder(knexInstance, models);
|
|
231
202
|
}
|
|
232
203
|
|
|
233
204
|
/**
|
|
234
|
-
*
|
|
235
|
-
* @
|
|
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
|
-
|
|
238
|
-
|
|
210
|
+
function createRepositoryFromModel(model, scopeContext) {
|
|
211
|
+
const ctx = scopeContext || createScopeContext();
|
|
212
|
+
return createRepository(model, knexInstance, ctx);
|
|
239
213
|
}
|
|
240
214
|
|
|
241
215
|
/**
|
|
242
|
-
*
|
|
243
|
-
* @param {
|
|
244
|
-
* @returns {
|
|
216
|
+
* Run operations in a transaction
|
|
217
|
+
* @param {Function} callback - Callback receiving transaction context
|
|
218
|
+
* @returns {Promise<*>} Result of callback
|
|
245
219
|
*/
|
|
246
|
-
function
|
|
247
|
-
return
|
|
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
|
-
|
|
239
|
+
return {
|
|
251
240
|
knex: knexInstance,
|
|
252
|
-
createRepository: createRepo,
|
|
253
|
-
transaction,
|
|
254
241
|
migrate,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
|
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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
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 ||
|
|
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 ||
|
|
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.
|
|
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 ||
|
|
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.
|
|
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 ||
|
|
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.
|
|
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 ||
|
|
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.
|
|
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 ||
|
|
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.
|
|
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 ||
|
|
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.
|
|
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 ||
|
|
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.
|
|
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
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
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
|
package/src/plugin-manager.js
CHANGED
|
@@ -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 (
|
|
222
|
+
// Register plugins (sync)
|
|
223
223
|
const pluginContext = { app, nunjucksEnv, options };
|
|
224
|
-
pluginManager.
|
|
224
|
+
pluginManager.registerSync(plugins, pluginContext);
|
|
225
225
|
|
|
226
226
|
// Request logging middleware
|
|
227
227
|
if (logging) {
|