webspresso 0.0.100 ā 0.0.101
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 +47 -4
- package/.agents/AGENTS.md +2 -0
- package/.agents/AUTH.md +63 -0
- package/.agents/CLI.md +81 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +59 -0
- package/.agents/ROUTING.md +64 -1
- package/AGENTS.md +1 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +17 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +10 -0
- package/core/auth/middleware.js +15 -1
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +6 -0
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +4 -0
- package/index.d.ts +266 -0
- package/index.js +40 -0
- package/package.json +5 -1
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +2 -2
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/transport.js +30 -11
- package/plugins/redirect/index.js +1 -1
- package/src/api/api-loader.js +1 -1
- package/src/app-context.js +48 -6
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/helpers.js +143 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +14 -10
- package/src/server.js +180 -26
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DB MakeMigrations Command (Django-inspired)
|
|
3
|
+
* Auto-detect model schema changes against live database and generate migrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
8
|
+
const { makeMigrations } = require('../../core/orm/migrations/diff');
|
|
9
|
+
|
|
10
|
+
function registerCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('db:makemigrations')
|
|
13
|
+
.alias('makemigrations')
|
|
14
|
+
.description('Auto-detect model changes against live database and generate migration files')
|
|
15
|
+
.option('-c, --config <path>', 'Path to database config file (webspresso.db.js or knexfile.js)')
|
|
16
|
+
.option('-m, --models <path>', 'Models directory (overrides config.models)')
|
|
17
|
+
.option('--migrations <path>', 'Migrations directory (overrides config.migrations.directory)')
|
|
18
|
+
.option('-n, --name <name>', 'Custom migration name override')
|
|
19
|
+
.option('--include-drops', 'Generate dropColumn operations for columns removed from models')
|
|
20
|
+
.option('--dry-run', 'Preview migrations that would be created without writing files')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
let knex = null;
|
|
23
|
+
try {
|
|
24
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
25
|
+
console.log(`\nš¦ Using config: ${configPath}\n`);
|
|
26
|
+
|
|
27
|
+
knex = await createDbInstance(config);
|
|
28
|
+
|
|
29
|
+
const modelsDir = options.models || config.models || './models';
|
|
30
|
+
const migrationDir = path.resolve(
|
|
31
|
+
process.cwd(),
|
|
32
|
+
options.migrations || config.migrations?.directory || './migrations'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
console.log(`š Inspecting database and comparing with models in ${modelsDir}...`);
|
|
36
|
+
|
|
37
|
+
const result = await makeMigrations({
|
|
38
|
+
knex,
|
|
39
|
+
modelsDir,
|
|
40
|
+
migrationDir,
|
|
41
|
+
dryRun: Boolean(options.dryRun),
|
|
42
|
+
includeDrops: Boolean(options.includeDrops),
|
|
43
|
+
name: options.name || null,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!result.hasChanges) {
|
|
47
|
+
console.log('\n⨠No changes detected in models. Database schema is up to date.\n');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const verb = options.dryRun ? 'Would generate' : 'Generated';
|
|
52
|
+
console.log(`\nā
${verb} ${result.created.length} migration(s):\n`);
|
|
53
|
+
|
|
54
|
+
for (const change of result.created) {
|
|
55
|
+
console.log(` š ${change.file}`);
|
|
56
|
+
console.log(` Model: ${change.model} (table: ${change.table})`);
|
|
57
|
+
console.log(` Type: ${change.type}`);
|
|
58
|
+
if (change.addedColumns?.length > 0) {
|
|
59
|
+
console.log(` + Added column(s): ${change.addedColumns.join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (change.removedColumns?.length > 0) {
|
|
62
|
+
console.log(` - Dropped column(s): ${change.removedColumns.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
console.log();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.dryRun) {
|
|
68
|
+
console.log('š” Dry-run mode: No files were written to disk.\n');
|
|
69
|
+
} else {
|
|
70
|
+
console.log('š Next step: Run "webspresso db:migrate" to apply the changes.\n');
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error('\nā Migration generation failed:', err.message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
} finally {
|
|
76
|
+
if (knex) {
|
|
77
|
+
await knex.destroy().catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { registerCommand };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - list Command
|
|
5
|
+
* Lists all built-in and custom project commands with descriptions
|
|
6
|
+
* @module bin/commands/list
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { createCommandRegistry } = require('../../core/commands');
|
|
11
|
+
|
|
12
|
+
function registerCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('list')
|
|
15
|
+
.alias('commands')
|
|
16
|
+
.description('List all available Webspresso and custom project management commands')
|
|
17
|
+
.option('-d, --dir <path>', 'Custom commands directory (default: ./commands)')
|
|
18
|
+
.action((options) => {
|
|
19
|
+
const rootDir = process.cwd();
|
|
20
|
+
const commandsDir = options.dir
|
|
21
|
+
? path.resolve(rootDir, options.dir)
|
|
22
|
+
: path.resolve(rootDir, 'commands');
|
|
23
|
+
|
|
24
|
+
const customRegistry = createCommandRegistry({ commandsDir });
|
|
25
|
+
const customCommands = customRegistry.list();
|
|
26
|
+
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log('ā Webspresso CLI - Command Palette');
|
|
29
|
+
console.log('===================================');
|
|
30
|
+
|
|
31
|
+
const sections = [
|
|
32
|
+
{
|
|
33
|
+
title: 'š Scaffolding & Code Generation',
|
|
34
|
+
commands: [
|
|
35
|
+
['make:model <name>', 'Scaffold a new ORM model definition in models/'],
|
|
36
|
+
['make:job <name>', 'Scaffold a background queue job worker in jobs/'],
|
|
37
|
+
['make:policy <name>', 'Scaffold an authorization policy in policies/'],
|
|
38
|
+
['make:factory <name>', 'Scaffold a test/seeder model factory in factories/'],
|
|
39
|
+
['make:event <name>', 'Scaffold an event listener in listeners/'],
|
|
40
|
+
['make:mail <name>', 'Scaffold a declarative Mailable class in emails/'],
|
|
41
|
+
['make:command <name>', 'Scaffold a custom management command in commands/'],
|
|
42
|
+
['page <path>', 'Scaffold a new file-based SSR page'],
|
|
43
|
+
['api <path>', 'Scaffold a new file-based API route'],
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: 'š¾ Database & Migrations',
|
|
48
|
+
commands: [
|
|
49
|
+
['db:migrate', 'Run all pending Knex migrations'],
|
|
50
|
+
['db:makemigrations', 'Auto-diff model schemas against database and generate migration'],
|
|
51
|
+
['db:rollback', 'Rollback the last batch of migrations'],
|
|
52
|
+
['db:status', 'Show database connection and migration status'],
|
|
53
|
+
['db:scaffold', 'Generate initial create table migrations from models/'],
|
|
54
|
+
['db:make <name>', 'Create an empty Knex migration file'],
|
|
55
|
+
['seed', 'Run database seeders'],
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: 'š ļø Server & Developer Experience',
|
|
60
|
+
commands: [
|
|
61
|
+
['dev', 'Start local development server with hot-reload'],
|
|
62
|
+
['start', 'Start production HTTP server'],
|
|
63
|
+
['build', 'Build client assets and precompile templates'],
|
|
64
|
+
['routes', 'Inspect all discovered SSR page and API routes'],
|
|
65
|
+
['doctor', 'Diagnose project configuration, dependencies, and health'],
|
|
66
|
+
['service [name]', 'Run and test services interactively without HTTP server'],
|
|
67
|
+
['run <command>', 'Execute a custom management command with full DI context'],
|
|
68
|
+
['list', 'Display this command index'],
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const section of sections) {
|
|
74
|
+
console.log(`\n${section.title}:`);
|
|
75
|
+
for (const [cmd, desc] of section.commands) {
|
|
76
|
+
console.log(` ${cmd.padEnd(28)} ${desc}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (customCommands.length > 0) {
|
|
81
|
+
console.log(`\nā” Custom Project Commands (${path.relative(rootDir, commandsDir) || 'commands/'}):`);
|
|
82
|
+
for (const cmd of customCommands) {
|
|
83
|
+
console.log(` ${('run ' + cmd.name).padEnd(28)} ${cmd.description || 'Custom management command'}`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
console.log('\nā” Custom Project Commands:');
|
|
87
|
+
console.log(' (None discovered. Create with: npx webspresso make:command <name>)');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('\nUse "webspresso <command> --help" for more information about any command.\n');
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
registerCommand,
|
|
96
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:command Command
|
|
5
|
+
* Scaffolds a custom management command in commands/<name>.js
|
|
6
|
+
* @module bin/commands/make-command
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateCommandContent(commandName, description = '') {
|
|
13
|
+
return `'use strict';
|
|
14
|
+
|
|
15
|
+
const { defineCommand } = require('webspresso');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom Management Command: ${commandName}
|
|
19
|
+
* Run via: npx webspresso run ${commandName}
|
|
20
|
+
*/
|
|
21
|
+
module.exports = defineCommand({
|
|
22
|
+
name: '${commandName}',
|
|
23
|
+
description: '${description || `Execute ${commandName} task`}',
|
|
24
|
+
options: [
|
|
25
|
+
{ flags: '-d, --dry-run', description: 'Run without making persistent changes', defaultValue: false },
|
|
26
|
+
],
|
|
27
|
+
handle: async ({ db, queue, services, mailer, events, logger, args, flags }) => {
|
|
28
|
+
logger.log('Executing "${commandName}"...');
|
|
29
|
+
if (flags.dryRun) {
|
|
30
|
+
logger.log('[Dry Run] No actual changes will be committed.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Business logic goes here:
|
|
34
|
+
// e.g. const users = await db.getRepository('User').find({ active: false });
|
|
35
|
+
|
|
36
|
+
return { success: true };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function registerCommand(program) {
|
|
43
|
+
program
|
|
44
|
+
.command('make:command <name>')
|
|
45
|
+
.alias('command:make')
|
|
46
|
+
.description('Scaffold a custom management command in commands/<name>.js')
|
|
47
|
+
.option('-d, --desc <description>', 'Command description', '')
|
|
48
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
49
|
+
.action((nameArg, options) => {
|
|
50
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
51
|
+
const commandsDir = path.join(rootDir, 'commands');
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(commandsDir)) {
|
|
54
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const commandName = String(nameArg || '').trim();
|
|
58
|
+
if (!commandName) {
|
|
59
|
+
console.error('ā Command name is required.');
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert colons or slashes to subdirectories or file path
|
|
65
|
+
// e.g. "users:prune" -> commands/users/prune.js or commands/users-prune.js
|
|
66
|
+
const relPath = commandName.includes(':')
|
|
67
|
+
? commandName.replace(/:/g, '/') + '.js'
|
|
68
|
+
: `${commandName}.js`;
|
|
69
|
+
|
|
70
|
+
const targetFilePath = path.join(commandsDir, relPath);
|
|
71
|
+
const targetDir = path.dirname(targetFilePath);
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(targetDir)) {
|
|
74
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (fs.existsSync(targetFilePath)) {
|
|
78
|
+
console.error(`ā Command file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const content = generateCommandContent(commandName, options.desc);
|
|
84
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
85
|
+
console.log(`\nā
Created custom management command: ${path.relative(rootDir, targetFilePath)}`);
|
|
86
|
+
console.log(` Execute it with: npx webspresso run ${commandName}\n`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
registerCommand,
|
|
92
|
+
generateCommandContent,
|
|
93
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:event Command
|
|
5
|
+
* Scaffolds an event / signal listener in listeners/<name>.js
|
|
6
|
+
* @module bin/commands/make-event
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateEventContent(eventName) {
|
|
13
|
+
return `'use strict';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Event Listener: ${eventName}
|
|
17
|
+
* Triggered by: app.events.emit('${eventName}', payload)
|
|
18
|
+
*/
|
|
19
|
+
module.exports = {
|
|
20
|
+
event: '${eventName}',
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle incoming event
|
|
24
|
+
* @param {Object} payload - Event payload
|
|
25
|
+
* @param {Object} ctx - Injected context { events, db, queue, mailer, logger }
|
|
26
|
+
*/
|
|
27
|
+
async handle(payload, ctx = {}) {
|
|
28
|
+
const { logger, mailer, queue } = ctx;
|
|
29
|
+
if (logger) {
|
|
30
|
+
logger.log('[Event:${eventName}] Received event payload:', payload);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Example: Dispatch background job or send notification email
|
|
34
|
+
// if (queue) await queue.dispatch('notify', payload);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registerCommand(program) {
|
|
41
|
+
program
|
|
42
|
+
.command('make:event <name>')
|
|
43
|
+
.alias('event:make')
|
|
44
|
+
.description('Scaffold an event listener in listeners/<name>.js')
|
|
45
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
46
|
+
.action((nameArg, options) => {
|
|
47
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
48
|
+
const listenersDir = path.join(rootDir, 'listeners');
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(listenersDir)) {
|
|
51
|
+
fs.mkdirSync(listenersDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rawName = String(nameArg || '').trim();
|
|
55
|
+
if (!rawName) {
|
|
56
|
+
console.error('ā Event name is required.');
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const eventName = rawName.replace(/\.js$/, '');
|
|
62
|
+
const fileName = eventName.replace(/:/g, '-');
|
|
63
|
+
const targetFilePath = path.join(listenersDir, `${fileName}.js`);
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(targetFilePath)) {
|
|
66
|
+
console.error(`ā Listener file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const content = generateEventContent(eventName);
|
|
72
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
73
|
+
console.log(`\nā
Created event listener: ${path.relative(rootDir, targetFilePath)} (event: "${eventName}")\n`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
registerCommand,
|
|
79
|
+
generateEventContent,
|
|
80
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:factory Command
|
|
5
|
+
* Scaffolds an ORM model factory in factories/<name>.factory.js
|
|
6
|
+
* @module bin/commands/make-factory
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateFactoryContent(modelName) {
|
|
13
|
+
const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
14
|
+
|
|
15
|
+
return `'use strict';
|
|
16
|
+
|
|
17
|
+
const { defineFactory } = require('webspresso');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Model Factory: ${cleanName}
|
|
21
|
+
* Used in tests and seeders:
|
|
22
|
+
* const ${cleanName.toLowerCase()} = await ${cleanName}Factory.create({ status: 'active' });
|
|
23
|
+
*/
|
|
24
|
+
module.exports = defineFactory('${cleanName}', ({ faker, seq }) => ({
|
|
25
|
+
title: faker ? faker.lorem.words(3) : \`${cleanName} \${seq}\`,
|
|
26
|
+
status: 'active',
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
updatedAt: new Date(),
|
|
29
|
+
}));
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function registerCommand(program) {
|
|
34
|
+
program
|
|
35
|
+
.command('make:factory <name>')
|
|
36
|
+
.alias('factory:make')
|
|
37
|
+
.description('Scaffold an ORM model factory in factories/<name>.factory.js')
|
|
38
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
39
|
+
.action((nameArg, options) => {
|
|
40
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
41
|
+
const factoriesDir = path.join(rootDir, 'factories');
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(factoriesDir)) {
|
|
44
|
+
fs.mkdirSync(factoriesDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rawName = String(nameArg || '').trim();
|
|
48
|
+
if (!rawName) {
|
|
49
|
+
console.error('ā Model name is required for factory.');
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cleanName = rawName.replace(/\.factory(\.js)?$/, '').replace(/\.js$/, '');
|
|
55
|
+
const targetFilePath = path.join(factoriesDir, `${cleanName.toLowerCase()}.factory.js`);
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(targetFilePath)) {
|
|
58
|
+
console.error(`ā Factory file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const content = generateFactoryContent(cleanName);
|
|
64
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
65
|
+
console.log(`\nā
Created model factory: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
registerCommand,
|
|
71
|
+
generateFactoryContent,
|
|
72
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:mail Command
|
|
5
|
+
* Scaffolds a declarative Mailable class in emails/<name>.js
|
|
6
|
+
* @module bin/commands/make-mail
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateMailContent(rawName) {
|
|
13
|
+
const baseName = rawName.replace(/email$/i, '').replace(/mail$/i, '').trim();
|
|
14
|
+
const cleanBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
15
|
+
const className = `${cleanBase}Email`;
|
|
16
|
+
const tplName = cleanBase.toLowerCase();
|
|
17
|
+
|
|
18
|
+
return `'use strict';
|
|
19
|
+
|
|
20
|
+
const { Mailable } = require('webspresso');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mailable: ${className}
|
|
24
|
+
* Send via: req.mailer.send(new ${className}(user))
|
|
25
|
+
*/
|
|
26
|
+
class ${className} extends Mailable {
|
|
27
|
+
constructor(recipient, data = {}) {
|
|
28
|
+
super();
|
|
29
|
+
this.recipient = recipient;
|
|
30
|
+
this.data = data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build email definition
|
|
35
|
+
*/
|
|
36
|
+
build() {
|
|
37
|
+
const toAddress = typeof this.recipient === 'string' ? this.recipient : this.recipient?.email;
|
|
38
|
+
const toName = typeof this.recipient === 'object' ? this.recipient?.name : undefined;
|
|
39
|
+
|
|
40
|
+
this.to(toAddress, toName)
|
|
41
|
+
.subject('Hello from Webspresso')
|
|
42
|
+
.view('${tplName}', this.data);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
${className},
|
|
48
|
+
};
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function registerCommand(program) {
|
|
53
|
+
program
|
|
54
|
+
.command('make:mail <name>')
|
|
55
|
+
.alias('mail:make')
|
|
56
|
+
.alias('make:mailable')
|
|
57
|
+
.description('Scaffold a declarative Mailable class in emails/<name>.js')
|
|
58
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
59
|
+
.action((nameArg, options) => {
|
|
60
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
61
|
+
const emailsDir = path.join(rootDir, 'emails');
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(emailsDir)) {
|
|
64
|
+
fs.mkdirSync(emailsDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rawName = String(nameArg || '').trim();
|
|
68
|
+
if (!rawName) {
|
|
69
|
+
console.error('ā Mailable name is required.');
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cleanName = rawName.replace(/\.js$/, '');
|
|
75
|
+
const fileName = cleanName.endsWith('Email') ? cleanName : `${cleanName}Email`;
|
|
76
|
+
const targetFilePath = path.join(emailsDir, `${fileName}.js`);
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(targetFilePath)) {
|
|
79
|
+
console.error(`ā Mailable file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const content = generateMailContent(cleanName);
|
|
85
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
86
|
+
console.log(`\nā
Created Mailable class: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
registerCommand,
|
|
92
|
+
generateMailContent,
|
|
93
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:policy Command
|
|
5
|
+
* Scaffolds an authorization policy in policies/<name>.policy.js
|
|
6
|
+
* @module bin/commands/make-policy
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generatePolicyContent(policyName) {
|
|
13
|
+
const modelName = policyName.replace(/policy$/i, '').trim();
|
|
14
|
+
const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
15
|
+
|
|
16
|
+
return `'use strict';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Authorization Policy: ${cleanName}
|
|
20
|
+
* Invoked via req.bouncer.authorize('${cleanName.toLowerCase()}.update', record)
|
|
21
|
+
* or {% if fsy.can('${cleanName.toLowerCase()}.update', record) %}
|
|
22
|
+
*/
|
|
23
|
+
module.exports = {
|
|
24
|
+
name: '${cleanName.toLowerCase()}',
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Determine whether the user can view any records
|
|
28
|
+
*/
|
|
29
|
+
async viewAny(user) {
|
|
30
|
+
return true;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Determine whether the user can view the specific record
|
|
35
|
+
*/
|
|
36
|
+
async view(user, record) {
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Determine whether the user can create records
|
|
42
|
+
*/
|
|
43
|
+
async create(user) {
|
|
44
|
+
return Boolean(user);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Determine whether the user can update the record
|
|
49
|
+
*/
|
|
50
|
+
async update(user, record) {
|
|
51
|
+
if (!user) return false;
|
|
52
|
+
if (user.isAdmin || user.role === 'admin') return true;
|
|
53
|
+
return record && (record.userId === user.id || record.user_id === user.id);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Determine whether the user can delete the record
|
|
58
|
+
*/
|
|
59
|
+
async delete(user, record) {
|
|
60
|
+
if (!user) return false;
|
|
61
|
+
return Boolean(user.isAdmin || user.role === 'admin');
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function registerCommand(program) {
|
|
68
|
+
program
|
|
69
|
+
.command('make:policy <name>')
|
|
70
|
+
.alias('policy:make')
|
|
71
|
+
.description('Scaffold an authorization policy in policies/<name>.policy.js')
|
|
72
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
73
|
+
.action((nameArg, options) => {
|
|
74
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
75
|
+
const policiesDir = path.join(rootDir, 'policies');
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(policiesDir)) {
|
|
78
|
+
fs.mkdirSync(policiesDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rawName = String(nameArg || '').trim();
|
|
82
|
+
if (!rawName) {
|
|
83
|
+
console.error('ā Policy name is required.');
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const policyBase = rawName.replace(/\.policy(\.js)?$/, '').replace(/\.js$/, '');
|
|
89
|
+
const targetFilePath = path.join(policiesDir, `${policyBase}.policy.js`);
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(targetFilePath)) {
|
|
92
|
+
console.error(`ā Policy file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const content = generatePolicyContent(policyBase);
|
|
98
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
99
|
+
console.log(`\nā
Created authorization policy: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
registerCommand,
|
|
105
|
+
generatePolicyContent,
|
|
106
|
+
};
|