webspresso 0.0.96 → 0.0.98
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/.agents/ADMIN.md +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
- package/.agents/skills/webspresso-framework/SKILL.md +56 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
- package/.agents/skills/webspresso-services/SKILL.md +101 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +51 -11
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/page.js +6 -3
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/seed.js +1 -1
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +31 -17
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +32 -12
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +124 -31
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +34 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/basic-auth/index.js +3 -1
- package/plugins/content/client/inline-edit.js +12 -12
- package/plugins/cors.js +6 -7
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/core.js +1 -1
- package/plugins/file-manager/security.js +33 -23
- package/plugins/index.js +49 -87
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +30 -8
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +213 -63
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
- package/src/services/discovery.js +6 -1
- package/src/services/index.js +6 -13
|
@@ -10,14 +10,26 @@ const { execSync } = require('child_process');
|
|
|
10
10
|
function registerCommand(program) {
|
|
11
11
|
program
|
|
12
12
|
.command('add tailwind')
|
|
13
|
+
.alias('add:tailwind')
|
|
13
14
|
.description('Add Tailwind CSS to the project with build process')
|
|
14
|
-
.
|
|
15
|
+
.option('--v4', 'Use Tailwind CSS v4 (Oxide engine, LightningCSS, zero postcss config)')
|
|
16
|
+
.option('--v3', 'Use Tailwind CSS v3 (legacy PostCSS engine)')
|
|
17
|
+
.option('-v, --tailwind-version <version>', 'Tailwind version (3 or 4)', '3')
|
|
18
|
+
.option('--upgrade', 'Upgrade existing Tailwind v3 project to Tailwind v4')
|
|
19
|
+
.action(async (...actionArgs) => {
|
|
20
|
+
const cmd = actionArgs[actionArgs.length - 1];
|
|
21
|
+
const options = (cmd && typeof cmd.opts === 'function')
|
|
22
|
+
? cmd.opts()
|
|
23
|
+
: (actionArgs.find(a => a && typeof a === 'object') || {});
|
|
24
|
+
|
|
15
25
|
if (!fs.existsSync('package.json')) {
|
|
16
26
|
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
17
27
|
process.exit(1);
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
const isV4 = Boolean(options.v4 || options.upgrade || options.tailwindVersion === '4');
|
|
31
|
+
|
|
32
|
+
console.log(`\n🎨 Adding Tailwind CSS ${isV4 ? 'v4 (Oxide engine, zero-config)' : 'v3'} to your project...\n`);
|
|
21
33
|
|
|
22
34
|
// Read package.json
|
|
23
35
|
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
@@ -27,17 +39,26 @@ function registerCommand(program) {
|
|
|
27
39
|
packageJson.devDependencies = {};
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
packageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
31
|
-
packageJson.devDependencies['postcss'] = '^8.4.35';
|
|
32
|
-
packageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
33
|
-
|
|
34
|
-
// Add build scripts
|
|
35
42
|
if (!packageJson.scripts) {
|
|
36
43
|
packageJson.scripts = {};
|
|
37
44
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
|
|
46
|
+
if (isV4) {
|
|
47
|
+
packageJson.devDependencies['@tailwindcss/cli'] = '^4.0.0';
|
|
48
|
+
packageJson.devDependencies['tailwindcss'] = '^4.0.0';
|
|
49
|
+
delete packageJson.devDependencies['postcss'];
|
|
50
|
+
delete packageJson.devDependencies['autoprefixer'];
|
|
51
|
+
|
|
52
|
+
packageJson.scripts['build:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --minify';
|
|
53
|
+
packageJson.scripts['watch:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --watch';
|
|
54
|
+
} else {
|
|
55
|
+
packageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
56
|
+
packageJson.devDependencies['postcss'] = '^8.4.35';
|
|
57
|
+
packageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
58
|
+
|
|
59
|
+
packageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
60
|
+
packageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
61
|
+
}
|
|
41
62
|
|
|
42
63
|
// Update dev script to include CSS watch
|
|
43
64
|
if (packageJson.scripts.dev) {
|
|
@@ -50,24 +71,48 @@ function registerCommand(program) {
|
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
|
53
|
-
console.log(
|
|
74
|
+
console.log(`✅ Updated package.json (${isV4 ? 'Tailwind v4' : 'Tailwind v3'})`);
|
|
54
75
|
|
|
55
76
|
// Create src directory if it doesn't exist
|
|
56
77
|
if (!fs.existsSync('src')) {
|
|
57
78
|
fs.mkdirSync('src', { recursive: true });
|
|
58
79
|
}
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
if (isV4) {
|
|
82
|
+
// Create v4 input.css
|
|
83
|
+
const inputCss = `@import "tailwindcss";
|
|
84
|
+
|
|
85
|
+
@source "../pages/**/*.{njk,js}";
|
|
86
|
+
@source "../views/**/*.njk";
|
|
87
|
+
@source "../src/**/*.js";
|
|
88
|
+
`;
|
|
89
|
+
fs.writeFileSync('src/input.css', inputCss);
|
|
90
|
+
console.log('✅ Created src/input.css (@import "tailwindcss"; + @source directives)');
|
|
91
|
+
|
|
92
|
+
// If upgrading from v3, clean up obsolete configs
|
|
93
|
+
if (fs.existsSync('postcss.config.js')) {
|
|
94
|
+
try {
|
|
95
|
+
fs.unlinkSync('postcss.config.js');
|
|
96
|
+
console.log('🧹 Removed obsolete postcss.config.js (not needed in v4)');
|
|
97
|
+
} catch (_) {}
|
|
98
|
+
}
|
|
99
|
+
if (fs.existsSync('tailwind.config.js')) {
|
|
100
|
+
try {
|
|
101
|
+
fs.renameSync('tailwind.config.js', 'tailwind.config.js.backup');
|
|
102
|
+
console.log('ℹ️ Renamed tailwind.config.js to tailwind.config.js.backup (v4 is CSS-first)');
|
|
103
|
+
} catch (_) {}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// Create v3 input.css
|
|
107
|
+
const inputCss = `@tailwind base;
|
|
62
108
|
@tailwind components;
|
|
63
109
|
@tailwind utilities;
|
|
64
110
|
`;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
111
|
+
fs.writeFileSync('src/input.css', inputCss);
|
|
112
|
+
console.log('✅ Created src/input.css');
|
|
113
|
+
|
|
114
|
+
// Create tailwind.config.js
|
|
115
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
71
116
|
module.exports = {
|
|
72
117
|
content: [
|
|
73
118
|
'./pages/**/*.{njk,js}',
|
|
@@ -80,21 +125,20 @@ module.exports = {
|
|
|
80
125
|
plugins: [],
|
|
81
126
|
}
|
|
82
127
|
`;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const postcssConfig = `module.exports = {
|
|
128
|
+
fs.writeFileSync('tailwind.config.js', tailwindConfig);
|
|
129
|
+
console.log('✅ Created tailwind.config.js');
|
|
130
|
+
|
|
131
|
+
// Create postcss.config.js
|
|
132
|
+
const postcssConfig = `module.exports = {
|
|
89
133
|
plugins: {
|
|
90
134
|
tailwindcss: {},
|
|
91
135
|
autoprefixer: {},
|
|
92
136
|
},
|
|
93
137
|
}
|
|
94
138
|
`;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
139
|
+
fs.writeFileSync('postcss.config.js', postcssConfig);
|
|
140
|
+
console.log('✅ Created postcss.config.js');
|
|
141
|
+
}
|
|
98
142
|
|
|
99
143
|
// Check if layout.njk exists and update it (before creating CSS)
|
|
100
144
|
const layoutPath = 'views/layout.njk';
|
|
@@ -128,9 +172,10 @@ module.exports = {
|
|
|
128
172
|
fs.writeFileSync('public/css/style.css', '/* Run npm run build:css */\n');
|
|
129
173
|
console.log('✅ Created public/css/style.css');
|
|
130
174
|
|
|
131
|
-
// Try to build CSS if tailwindcss is already installed
|
|
175
|
+
// Try to build CSS if tailwindcss / @tailwindcss/cli is already installed
|
|
132
176
|
const tailwindBin = path.join(process.cwd(), 'node_modules', '.bin', 'tailwindcss');
|
|
133
|
-
|
|
177
|
+
const tailwindCliBin = path.join(process.cwd(), 'node_modules', '.bin', '@tailwindcss/cli');
|
|
178
|
+
if (fs.existsSync(tailwindBin) || fs.existsSync(tailwindCliBin)) {
|
|
134
179
|
try {
|
|
135
180
|
console.log('\n🎨 Building Tailwind CSS from your templates...');
|
|
136
181
|
execSync('npm run build:css', { stdio: 'inherit', cwd: process.cwd() });
|
|
@@ -139,7 +184,7 @@ module.exports = {
|
|
|
139
184
|
console.log('\n⚠️ CSS build failed. Run "npm run build:css" manually.\n');
|
|
140
185
|
}
|
|
141
186
|
} else {
|
|
142
|
-
console.log(
|
|
187
|
+
console.log(`\n✅ Tailwind CSS ${isV4 ? 'v4' : 'v3'} added successfully!\n`);
|
|
143
188
|
console.log('Next steps:');
|
|
144
189
|
console.log(' npm install');
|
|
145
190
|
console.log(' npm run build:css');
|
|
@@ -92,7 +92,8 @@ function registerCommand(program) {
|
|
|
92
92
|
|
|
93
93
|
// Hash the password (same rounds as admin panel setup)
|
|
94
94
|
const { hash } = require('../../core/auth/hash');
|
|
95
|
-
const
|
|
95
|
+
const rounds = process.env.NODE_ENV === 'test' ? 4 : 10;
|
|
96
|
+
const hashedPassword = await hash(password, rounds);
|
|
96
97
|
|
|
97
98
|
// Update the password
|
|
98
99
|
await db('admin_users')
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { loadDbConfig } = require('../utils/db');
|
|
9
|
-
const { generateAdminUsersMigration } = require('../../plugins/admin-panel/migration-template');
|
|
10
9
|
|
|
11
10
|
function registerCommand(program) {
|
|
12
11
|
program
|
|
@@ -53,6 +52,7 @@ function registerCommand(program) {
|
|
|
53
52
|
const filepath = path.join(migrationDir, filename);
|
|
54
53
|
|
|
55
54
|
// Generate migration content
|
|
55
|
+
const { generateAdminUsersMigration } = require('../../plugins/admin-panel/migration-template');
|
|
56
56
|
const content = generateAdminUsersMigration();
|
|
57
57
|
|
|
58
58
|
// Write migration file
|
|
@@ -39,6 +39,40 @@ function copyFileSafe(src, dest, force = false) {
|
|
|
39
39
|
return destExists ? 'overwritten' : 'created';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Recursively copies a directory safely.
|
|
44
|
+
* @param {string} srcDir
|
|
45
|
+
* @param {string} destDir
|
|
46
|
+
* @param {boolean} force
|
|
47
|
+
* @param {string[]} installed
|
|
48
|
+
* @param {string[]} skipped
|
|
49
|
+
* @param {string} baseRel
|
|
50
|
+
*/
|
|
51
|
+
function copyDirSafe(srcDir, destDir, force = false, installed = [], skipped = [], baseRel = '') {
|
|
52
|
+
if (!fs.existsSync(srcDir)) return;
|
|
53
|
+
if (!fs.existsSync(destDir)) {
|
|
54
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
60
|
+
const destPath = path.join(destDir, entry.name);
|
|
61
|
+
const relPath = path.join(baseRel, entry.name);
|
|
62
|
+
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
copyDirSafe(srcPath, destPath, force, installed, skipped, relPath);
|
|
65
|
+
} else {
|
|
66
|
+
const status = copyFileSafe(srcPath, destPath, force);
|
|
67
|
+
if (status === 'created' || status === 'overwritten') {
|
|
68
|
+
installed.push(relPath);
|
|
69
|
+
} else {
|
|
70
|
+
skipped.push(relPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
42
76
|
/**
|
|
43
77
|
* Scaffolds AI Agent rules and guidebook files into target project directory.
|
|
44
78
|
* @param {string} targetDir
|
|
@@ -98,23 +132,29 @@ function scaffoldAgentsFiles(targetDir, options = {}) {
|
|
|
98
132
|
}
|
|
99
133
|
}
|
|
100
134
|
|
|
101
|
-
// 4. .agents/*.md Guidebook directory
|
|
135
|
+
// 4. .agents/*.md Guidebook and skills/ directory
|
|
102
136
|
if (includeGuides && fs.existsSync(SOURCE_AGENTS_DIR)) {
|
|
103
137
|
const destAgentsDir = path.join(targetDir, '.agents');
|
|
104
138
|
if (!fs.existsSync(destAgentsDir)) {
|
|
105
139
|
fs.mkdirSync(destAgentsDir, { recursive: true });
|
|
106
140
|
}
|
|
107
141
|
|
|
108
|
-
const
|
|
109
|
-
for (const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
const entries = fs.readdirSync(SOURCE_AGENTS_DIR, { withFileTypes: true });
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.name === 'internal') continue; // exclude internal reviews from user projects
|
|
145
|
+
const srcPath = path.join(SOURCE_AGENTS_DIR, entry.name);
|
|
146
|
+
const destPath = path.join(destAgentsDir, entry.name);
|
|
147
|
+
const relPath = path.join('.agents', entry.name);
|
|
148
|
+
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
copyDirSafe(srcPath, destPath, force, result.installed, result.skipped, relPath);
|
|
151
|
+
} else if (entry.name.endsWith('.md')) {
|
|
152
|
+
const status = copyFileSafe(srcPath, destPath, force);
|
|
153
|
+
if (status === 'created' || status === 'overwritten') {
|
|
154
|
+
result.installed.push(relPath);
|
|
155
|
+
} else {
|
|
156
|
+
result.skipped.push(relPath);
|
|
157
|
+
}
|
|
118
158
|
}
|
|
119
159
|
}
|
|
120
160
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prune old site analytics records (CLI)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
6
|
+
|
|
7
|
+
function registerCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('analytics:prune')
|
|
10
|
+
.description('Delete analytics page views and client errors older than the given retention window')
|
|
11
|
+
.option('--days <n>', 'Delete page views older than this many days', '90')
|
|
12
|
+
.option('--error-days <n>', 'Delete client errors older than this many days', '30')
|
|
13
|
+
.option('--table <name>', 'Page views table name', 'analytics_page_views')
|
|
14
|
+
.option('--errors-table <name>', 'Client errors table name', 'analytics_client_errors')
|
|
15
|
+
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
16
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const days = parseInt(options.days, 10);
|
|
19
|
+
if (Number.isNaN(days) || days < 1) {
|
|
20
|
+
console.error('❌ --days must be a positive integer');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const errorDays = parseInt(options.errorDays, 10);
|
|
25
|
+
if (Number.isNaN(errorDays) || errorDays < 1) {
|
|
26
|
+
console.error('❌ --error-days must be a positive integer');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
31
|
+
console.log(`\n📦 Using config: ${configPath}`);
|
|
32
|
+
console.log(` Environment: ${options.env}\n`);
|
|
33
|
+
|
|
34
|
+
const knex = await createDbInstance(config, options.env);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { purgeAnalyticsData } = require('../../plugins/site-analytics/purge');
|
|
38
|
+
const result = await purgeAnalyticsData(knex, {
|
|
39
|
+
days,
|
|
40
|
+
errorDays,
|
|
41
|
+
tableName: options.table,
|
|
42
|
+
errorsTableName: options.errorsTable,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(`✅ Analytics purge completed:`);
|
|
46
|
+
console.log(` - Page views deleted: ${result.viewsDeleted} (cutoff: ${result.viewsCutoff})`);
|
|
47
|
+
console.log(` - Client errors deleted: ${result.errorsDeleted} (cutoff: ${result.errorsCutoff})\n`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('❌ analytics:prune failed:', err.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
} finally {
|
|
52
|
+
await knex.destroy();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { registerCommand };
|
package/bin/commands/api.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* API Command
|
|
5
|
-
* Add a new API endpoint with modern Webspresso schema & handler contract
|
|
5
|
+
* Add a new API endpoint or full REST CRUD suite with modern Webspresso schema & handler contract
|
|
6
6
|
* @module bin/commands/api
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -18,6 +18,31 @@ const path = require('path');
|
|
|
18
18
|
|
|
19
19
|
const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
20
20
|
|
|
21
|
+
function singularize(word) {
|
|
22
|
+
if (!word) return '';
|
|
23
|
+
const lower = word.toLowerCase();
|
|
24
|
+
if (lower.endsWith('ies')) {
|
|
25
|
+
return lower.slice(0, -3) + 'y';
|
|
26
|
+
}
|
|
27
|
+
if (lower.endsWith('ses')) {
|
|
28
|
+
return lower.slice(0, -2);
|
|
29
|
+
}
|
|
30
|
+
if (lower.endsWith('s') && !lower.endsWith('ss')) {
|
|
31
|
+
return lower.slice(0, -1);
|
|
32
|
+
}
|
|
33
|
+
return lower;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toPascalCase(str) {
|
|
37
|
+
if (!str) return '';
|
|
38
|
+
const clean = str.replace(/[^a-zA-Z0-9]/g, ' ');
|
|
39
|
+
return clean
|
|
40
|
+
.split(' ')
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
43
|
+
.join('');
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
function parseRouteAndMethod(rawInput, optionMethod) {
|
|
22
47
|
let route = String(rawInput || '').trim();
|
|
23
48
|
let method = optionMethod ? String(optionMethod).toUpperCase() : null;
|
|
@@ -45,18 +70,160 @@ function parseRouteAndMethod(rawInput, optionMethod) {
|
|
|
45
70
|
};
|
|
46
71
|
}
|
|
47
72
|
|
|
73
|
+
function generateCrudFiles(apiDir, route, modelName, middlewareStr) {
|
|
74
|
+
const cleanRoute = route.replace(/^\/+|\/+$/g, '');
|
|
75
|
+
const baseDir = path.join(apiDir, cleanRoute);
|
|
76
|
+
const detailDir = path.join(baseDir, '[id]');
|
|
77
|
+
fs.mkdirSync(detailDir, { recursive: true });
|
|
78
|
+
|
|
79
|
+
const files = [
|
|
80
|
+
{
|
|
81
|
+
path: path.join(apiDir, `${cleanRoute}.get.js`),
|
|
82
|
+
label: `GET /api/${cleanRoute} (List)`,
|
|
83
|
+
content: `'use strict';
|
|
84
|
+
|
|
85
|
+
const { defineApi } = require('webspresso');
|
|
86
|
+
|
|
87
|
+
module.exports = defineApi({
|
|
88
|
+
schema: ({ z }) => ({
|
|
89
|
+
query: z.object({
|
|
90
|
+
page: z.coerce.number().optional().default(1),
|
|
91
|
+
limit: z.coerce.number().optional().default(20),
|
|
92
|
+
}),
|
|
93
|
+
}),${middlewareStr}
|
|
94
|
+
handler: async (req, res) => {
|
|
95
|
+
const repo = req.db ? req.db.getRepository('${modelName}') : null;
|
|
96
|
+
const { page, limit } = req.input.query;
|
|
97
|
+
const items = repo ? await repo.query().paginate({ page, limit }) : [];
|
|
98
|
+
return res.json(items);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
`,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
path: path.join(apiDir, `${cleanRoute}.post.js`),
|
|
105
|
+
label: `POST /api/${cleanRoute} (Create)`,
|
|
106
|
+
content: `'use strict';
|
|
107
|
+
|
|
108
|
+
const { defineApi } = require('webspresso');
|
|
109
|
+
|
|
110
|
+
module.exports = defineApi({
|
|
111
|
+
schema: ({ z }) => ({
|
|
112
|
+
body: z.object({
|
|
113
|
+
title: z.string().min(1).optional(),
|
|
114
|
+
}).passthrough(),
|
|
115
|
+
}),${middlewareStr}
|
|
116
|
+
handler: async (req, res) => {
|
|
117
|
+
const repo = req.db ? req.db.getRepository('${modelName}') : null;
|
|
118
|
+
const created = repo ? await repo.create(req.input.body) : req.input.body;
|
|
119
|
+
return res.status(201).json(created);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
`,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
path: path.join(detailDir, '[id].get.js'),
|
|
126
|
+
label: `GET /api/${cleanRoute}/:id (Detail)`,
|
|
127
|
+
content: `'use strict';
|
|
128
|
+
|
|
129
|
+
const { defineApi } = require('webspresso');
|
|
130
|
+
|
|
131
|
+
module.exports = defineApi({
|
|
132
|
+
schema: ({ z }) => ({
|
|
133
|
+
params: z.object({
|
|
134
|
+
id: z.coerce.number().or(z.string().min(1)),
|
|
135
|
+
}),
|
|
136
|
+
}),${middlewareStr}
|
|
137
|
+
handler: async (req, res) => {
|
|
138
|
+
const repo = req.db ? req.db.getRepository('${modelName}') : null;
|
|
139
|
+
const item = repo ? await repo.findById(req.params.id) : null;
|
|
140
|
+
if (!item) {
|
|
141
|
+
return res.status(404).json({ error: '${modelName} not found' });
|
|
142
|
+
}
|
|
143
|
+
return res.json(item);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
`,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
path: path.join(detailDir, '[id].patch.js'),
|
|
150
|
+
label: `PATCH /api/${cleanRoute}/:id (Update)`,
|
|
151
|
+
content: `'use strict';
|
|
152
|
+
|
|
153
|
+
const { defineApi } = require('webspresso');
|
|
154
|
+
|
|
155
|
+
module.exports = defineApi({
|
|
156
|
+
schema: ({ z }) => ({
|
|
157
|
+
params: z.object({
|
|
158
|
+
id: z.coerce.number().or(z.string().min(1)),
|
|
159
|
+
}),
|
|
160
|
+
body: z.record(z.unknown()).optional(),
|
|
161
|
+
}),${middlewareStr}
|
|
162
|
+
handler: async (req, res) => {
|
|
163
|
+
const repo = req.db ? req.db.getRepository('${modelName}') : null;
|
|
164
|
+
const updated = repo ? await repo.update(req.params.id, req.input.body || {}) : null;
|
|
165
|
+
if (!updated) {
|
|
166
|
+
return res.status(404).json({ error: '${modelName} not found' });
|
|
167
|
+
}
|
|
168
|
+
return res.json(updated);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
`,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
path: path.join(detailDir, '[id].delete.js'),
|
|
175
|
+
label: `DELETE /api/${cleanRoute}/:id (Delete)`,
|
|
176
|
+
content: `'use strict';
|
|
177
|
+
|
|
178
|
+
const { defineApi } = require('webspresso');
|
|
179
|
+
|
|
180
|
+
module.exports = defineApi({
|
|
181
|
+
schema: ({ z }) => ({
|
|
182
|
+
params: z.object({
|
|
183
|
+
id: z.coerce.number().or(z.string().min(1)),
|
|
184
|
+
}),
|
|
185
|
+
}),${middlewareStr}
|
|
186
|
+
handler: async (req, res) => {
|
|
187
|
+
const repo = req.db ? req.db.getRepository('${modelName}') : null;
|
|
188
|
+
if (repo) {
|
|
189
|
+
await repo.delete(req.params.id);
|
|
190
|
+
}
|
|
191
|
+
return res.status(204).end();
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
`,
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
for (const f of files) {
|
|
199
|
+
fs.writeFileSync(f.path, f.content, 'utf8');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return files;
|
|
203
|
+
}
|
|
204
|
+
|
|
48
205
|
function registerCommand(program) {
|
|
49
206
|
program
|
|
50
207
|
.command('api [route]')
|
|
208
|
+
.alias('make:api')
|
|
209
|
+
.alias('api:make')
|
|
51
210
|
.description('Add a new API endpoint to pages/api/ with schema validation and handler')
|
|
52
211
|
.option('-m, --method <method>', 'HTTP method (GET, POST, PUT, PATCH, DELETE)', 'GET')
|
|
212
|
+
.option('--crud', 'Generate full REST CRUD endpoints (list, create, get, update, delete)')
|
|
213
|
+
.option('--model <model>', 'Associated ORM model name for repository calls')
|
|
53
214
|
.option('--auth', 'Protect with session auth middleware')
|
|
54
215
|
.option('--jwt', 'Protect with JWT bearer middleware')
|
|
216
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
55
217
|
.action(async (routeArg, options) => {
|
|
56
|
-
const
|
|
218
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
219
|
+
const pagesDir = path.join(rootDir, 'pages');
|
|
57
220
|
if (!fs.existsSync(pagesDir)) {
|
|
58
|
-
|
|
59
|
-
|
|
221
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const apiDir = path.join(pagesDir, 'api');
|
|
225
|
+
if (!fs.existsSync(apiDir)) {
|
|
226
|
+
fs.mkdirSync(apiDir, { recursive: true });
|
|
60
227
|
}
|
|
61
228
|
|
|
62
229
|
let route;
|
|
@@ -83,17 +250,39 @@ function registerCommand(program) {
|
|
|
83
250
|
message: 'HTTP method:',
|
|
84
251
|
choices: VALID_METHODS,
|
|
85
252
|
default: options.method ? options.method.toUpperCase() : 'GET',
|
|
253
|
+
when: () => !options.crud,
|
|
86
254
|
},
|
|
87
255
|
]);
|
|
88
256
|
|
|
89
|
-
const parsed = parseRouteAndMethod(answers.route, answers.method);
|
|
257
|
+
const parsed = parseRouteAndMethod(answers.route, answers.method || options.method);
|
|
90
258
|
route = parsed.route;
|
|
91
259
|
method = parsed.method;
|
|
92
260
|
}
|
|
93
261
|
|
|
262
|
+
const middlewares = [];
|
|
263
|
+
if (options.auth) middlewares.push("'auth'");
|
|
264
|
+
if (options.jwt) middlewares.push("'jwt'");
|
|
265
|
+
const middlewareStr = middlewares.length > 0 ? `\n middleware: [${middlewares.join(', ')}],` : '';
|
|
266
|
+
|
|
267
|
+
// CRUD Generation Mode
|
|
268
|
+
if (options.crud) {
|
|
269
|
+
const lastSegment = route.split('/').filter(Boolean).pop() || route;
|
|
270
|
+
const singular = singularize(lastSegment);
|
|
271
|
+
const modelName = options.model ? String(options.model).trim() : toPascalCase(singular);
|
|
272
|
+
|
|
273
|
+
const createdFiles = generateCrudFiles(apiDir, route, modelName, middlewareStr);
|
|
274
|
+
console.log(`\n✅ Generated REST CRUD API endpoints for "${modelName}" in pages/api/${route}:`);
|
|
275
|
+
for (const cf of createdFiles) {
|
|
276
|
+
console.log(` - ${cf.label} → ${path.relative(process.cwd(), cf.path)}`);
|
|
277
|
+
}
|
|
278
|
+
console.log('');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Single Route Generation Mode
|
|
94
283
|
const methodUpper = String(method).toUpperCase();
|
|
95
284
|
const methodLower = methodUpper.toLowerCase();
|
|
96
|
-
const routePath = path.join(
|
|
285
|
+
const routePath = path.join(apiDir, route);
|
|
97
286
|
const dirPath = path.dirname(routePath);
|
|
98
287
|
const fileName = path.basename(routePath);
|
|
99
288
|
|
|
@@ -101,11 +290,6 @@ function registerCommand(program) {
|
|
|
101
290
|
|
|
102
291
|
const apiFile = path.join(dirPath, `${fileName}.${methodLower}.js`);
|
|
103
292
|
|
|
104
|
-
const middlewares = [];
|
|
105
|
-
if (options.auth) middlewares.push("'auth'");
|
|
106
|
-
if (options.jwt) middlewares.push("'jwt'");
|
|
107
|
-
const middlewareStr = middlewares.length > 0 ? `\n middleware: [${middlewares.join(', ')}],` : '';
|
|
108
|
-
|
|
109
293
|
const hasParams = route.includes('[');
|
|
110
294
|
const hasBody = ['POST', 'PUT', 'PATCH'].includes(methodUpper);
|
|
111
295
|
|
|
@@ -114,10 +298,12 @@ function registerCommand(program) {
|
|
|
114
298
|
/**
|
|
115
299
|
* ${methodUpper} /api/${route}
|
|
116
300
|
*/
|
|
117
|
-
|
|
301
|
+
const { defineApi } = require('webspresso');
|
|
302
|
+
|
|
303
|
+
module.exports = defineApi({
|
|
118
304
|
// Input validation schema (Zod)
|
|
119
305
|
schema: ({ z }) => ({
|
|
120
|
-
${hasParams ? 'params: z.object({ id: z.string().min(1) }),\n ' : ''}${hasBody ? 'body: z.object({\n // Define body schema here\n }),\n ' : ''}query: z.object({\n // Define query params here\n }),
|
|
306
|
+
${hasParams ? 'params: z.object({ id: z.coerce.number().or(z.string().min(1)) }),\n ' : ''}${hasBody ? 'body: z.object({\n // Define body schema here\n }).passthrough(),\n ' : ''}query: z.object({\n // Define query params here\n }),
|
|
121
307
|
}),${middlewareStr}
|
|
122
308
|
|
|
123
309
|
// Request Handler
|
|
@@ -131,7 +317,7 @@ module.exports = {
|
|
|
131
317
|
timestamp: new Date().toISOString(),
|
|
132
318
|
});
|
|
133
319
|
},
|
|
134
|
-
};
|
|
320
|
+
});
|
|
135
321
|
`;
|
|
136
322
|
|
|
137
323
|
fs.writeFileSync(apiFile, apiContent, 'utf8');
|
|
@@ -139,4 +325,10 @@ module.exports = {
|
|
|
139
325
|
});
|
|
140
326
|
}
|
|
141
327
|
|
|
142
|
-
module.exports = {
|
|
328
|
+
module.exports = {
|
|
329
|
+
registerCommand,
|
|
330
|
+
parseRouteAndMethod,
|
|
331
|
+
singularize,
|
|
332
|
+
toPascalCase,
|
|
333
|
+
generateCrudFiles,
|
|
334
|
+
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
6
|
-
const { purgeAuditLogs } = require('../../plugins/audit-log/purge');
|
|
7
6
|
|
|
8
7
|
function registerCommand(program) {
|
|
9
8
|
program
|
|
@@ -28,6 +27,7 @@ function registerCommand(program) {
|
|
|
28
27
|
const olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
29
28
|
|
|
30
29
|
try {
|
|
30
|
+
const { purgeAuditLogs } = require('../../plugins/audit-log/purge');
|
|
31
31
|
const deleted = await purgeAuditLogs(knex, {
|
|
32
32
|
tableName: options.table,
|
|
33
33
|
olderThan,
|