diff --git a/aliasLoader.js b/aliasLoader.js new file mode 100644 index 0000000..ea5296b --- /dev/null +++ b/aliasLoader.js @@ -0,0 +1,14 @@ +const path = require('path') +class AliasLoader { + + constructor(aliases = []) { + this._aliases = aliases + } + registerGlobal() { + let aliases = this._aliases + for (let aliasKey in aliases) { + global[aliasKey] = typeof aliases[aliasKey] == 'string' ? require((aliases[aliasKey].startsWith('./')) ? path.resolve(aliases[aliasKey]) : aliases[aliasKey]) : aliases[aliasKey] + } + } +} +module.exports = AliasLoader \ No newline at end of file diff --git a/application.js b/application.js new file mode 100644 index 0000000..691a638 --- /dev/null +++ b/application.js @@ -0,0 +1,427 @@ +const Container = require('@ostro/container') +const LoadEnvironmentVariables = require('./bootstrap/loadConfiguration') +const Filesystem = require('@ostro/filesystem/filesystem') +const ProviderRepository = require('./providerRepository') +const Env = require('@ostro/support/env') +const path = require('path') +const Mix = require('./mix') +const kBasePath = Symbol('basePath') +const kHasBeenBootstrapped = Symbol('hasBeenBootstrapped') +const kBooted = Symbol('booted') +const kServiceProviders = Symbol('serviceProviders') +const kDeferredServices = Symbol('deferredServices') +const kLoadedProviders = Symbol('loadedProviders') +const kAppPath = Symbol('appPath') +const kDatabasePath = Symbol('databasePath') +const kLangPath = Symbol('langPath') +const kStoragePath = Symbol('storagePath') +const kEnvironmentPath = Symbol('environmentPath') +const kEnvironmentFile = Symbol('environmentFile') +class Application extends Container { + + get VERSION() { + return '0.0.0-alpha.0'; + } + + _basePath; + + _hasBeenBootstrapped = false; + + _booted = false; + + _serviceProviders = []; + + _deferredServices = []; + + _loadedProviders = []; + + _appPath; + + _databasePath; + + _langPath; + + _storagePath; + + _environmentPath; + + _environmentFile; + + constructor(basePath = null) { + super() + + if (basePath) { + this.setBasePath(basePath); + } + + this.registerBaseBindings(); + this.registerBaseServiceProviders(); + + global.app = this.app.bind(this) + } + + version() { + return this.VERSION; + } + + registerBaseBindings() { + this.instance('app', this); + this.singleton('mix', function(app) { + return new Mix(app) + }); + + } + + bootstrapWith($bootstrappers) { + this._hasBeenBootstrapped = true; + + for (let $bootstrapper of $bootstrappers) { + this.make($bootstrapper).bootstrap(this); + } + } + + registerBaseServiceProviders() { + this.register('@ostro/logger/logServiceProvider'); + + } + + registerConfiguredProviders() { + let $providers = this.config['app.providers']; + (new ProviderRepository(this, new Filesystem, this.getCachedServicesPath())) + .load($providers); + + } + + getDeferredServices() { + return this._deferredServices; + } + + setDeferredServices($services) { + this._deferredServices = $services; + } + + addDeferredServices($services) { + this._deferredServices = this._deferredServices.concat($services); + } + + isDeferredService($service) { + return this._deferredServices.indexOf($service) > -1 + } + + hasBeenBootstrapped() { + return this._hasBeenBootstrapped; + } + + setBasePath(basePath) { + this._basePath = path.resolve(basePath); + process.chdir(this._basePath || process.cwd()); + this.bindPathsInContainer(); + + return this; + } + + bindPathsInContainer() { + this.instance('path', this.path()); + this.instance('path.base', this.basePath()); + this.instance('path.lang', this.langPath()); + this.instance('path.config', this.configPath()); + this.instance('path.public', this.publicPath()); + this.instance('path.storage', this.storagePath()); + this.instance('path.database', this.databasePath()); + this.instance('path.resources', this.resourcePath()); + this.instance('path.bootstrap', this.bootstrapPath()); + } + + isBooted() { + return this._booted; + } + + boot() { + let app = this.instance('config').get('app') + process.env.TZ = app.timezone + process.env.NODE_ENV = app.env + this._serviceProviders.map(($p) => { + this.bootProvider($p); + }); + this._booted = true; + + } + + register($provider, $force = false) { + if (typeof $provider == 'string') { + $provider = require(path.normalize($provider.startsWith('./') ? path.resolve($provider) : $provider)) + } + let $registered = this.getProvider($provider) + if ($registered && !$force) { + return $registered; + } + + $provider = this.resolveProvider($provider); + + $provider.register(); + + this.markAsRegistered($provider); + + if (this.isBooted()) { + this.bootProvider($provider); + } + + return $provider; + } + + markAsRegistered($provider) { + this._serviceProviders.push($provider); + + } + + getProvider($provider) { + return this.getProviders($provider) + } + + getProviders($provider) { + + return this._serviceProviders.find(function(value) { + return value instanceof $provider + }) + } + + resolveProvider($provider) { + + return new $provider(this); + } + + bootProvider($provider) { + $provider.callBootingCallbacks(); + + if (typeof $provider.boot == 'function') { + $provider.boot(this) + } + + $provider.callBootedCallbacks(); + } + + loadDeferredProviders() { + + for (let $provider of this._deferredServices) { + this.loadDeferredProvider($provider); + } + + this._deferredServices = []; + } + + loadDeferredProvider($provider) { + if (!this.isDeferredService($provider)) { + return; + } + + this.registerDeferredProvider($provider); + + } + + registerDeferredProvider($provider, $service = null) { + + this.register($provider); + + if (!this.isBooted()) { + this.booting(function() { + this.bootProvider($instance); + }); + } + } + + path(dir = '') { + let appPath = this._appPath ? this._appPath : path.normalize(path.join(this._basePath, 'app')); + + return path.normalize(path.join(appPath + dir)); + } + + useAppPath(dir) { + this._appPath = dir; + + this.instance('path', dir); + + return this; + } + + basePath(dir = '') { + return path.normalize(path.join(this._basePath, dir)) + } + + bootstrapPath(dir = '') { + return path.normalize(path.join(this._basePath, 'bootstrap', dir)); + } + + configPath(dir = '') { + return path.normalize(path.join(this._basePath, 'config', dir)); + } + + databasePath(dir = '') { + return path.normalize(path.join((this._databasePath ? this._databasePath : path.join(this._basePath, 'database')), dir)); + } + + useDatabasePath(dir) { + this._databasePath = dir; + + this.instance('path.database', dir); + + return this; + } + + langPath() { + if (this._langPath) { + return this._langPath; + } + let dir = path.normalize(path.join(this.resourcePath(), 'lang')) + if (!!path.extname(dir)) { + return dir; + } + + return path.normalize(path.join(this.basePath(), 'lang')); + } + + useLangPath(dir) { + this._langPath = dir; + + this.instance('path.lang', dir); + + return this; + } + + publicPath() { + return path.normalize(path.join(this._basePath, 'public')); + } + + storagePath() { + return this._storagePath ? this._storagePath : path.normalize(path.join(this._basePath, 'storage')); + } + + useStoragePath(dir) { + this._storagePath = dir; + + this.instance('path.storage', dir); + + return this; + } + + resourcePath(dir = '') { + return path.normalize(path.join(this._basePath, 'resources', dir)); + } + + viewPath(dir = '') { + let basePath = this['config'].get('view.paths', [])[0]; + + return path.normalize(path.join((basePath ? basePath : this._basePath + '/resources/view'), dir)); + } + + environmentPath() { + return this._environmentPath ? this._environmentPath : this._basePath; + } + + useEnvironmentPath(dir) { + this._environmentPath = dir; + + return this; + } + + loadEnvironmentFrom(file) { + this._environmentFile = file; + + return this; + } + + environmentFile() { + return this._environmentFile ? this._environmentFile : '.env'; + } + + environmentFilePath() { + return path.normalize(path.join(this.environmentPath(), this.environmentFile())); + } + + isLocal() { + return this['env'] === 'local'; + } + + isProduction() { + return this['env'] === 'production'; + } + + isDownForMaintenance() { + return file_exists(this.storagePath() + '/framework/down'); + } + + getLocale() { + return this['config'].get('app.locale'); + } + + currentLocale() { + return this.getLocale(); + } + + getFallbackLocale() { + return this['config'].get('app.fallback_locale'); + } + + setLocale(locale) { + this['config'].set('app.locale', locale); + + this['translator'].setLocale(locale); + + this['events'].dispatch(new LocaleUpdated(locale)); + } + + setFallbackLocale(fallbackLocale) { + this['config'].set('app.fallback_locale', fallbackLocale); + + this['translator'].setFallback(fallbackLocale); + } + + isLocale(locale) { + return this.getLocale() == locale; + } + + getCachedServicesPath() { + return this.normalizeCachePath('APP_SERVICES_CACHE', 'cache/services.json'); + } + + getCachedPackagesPath() { + return this.normalizeCachePath('APP_PACKAGES_CACHE', 'cache/packages.json'); + } + + configurationIsCached() { + return is_file(this.getCachedConfigPath()); + } + + getCachedConfigPath() { + return this.normalizeCachePath('APP_CONFIG_CACHE', 'cache/config.json'); + } + + routesAreCached() { + return this['files'].exists(this.getCachedRoutesPath()); + } + + getCachedRoutesPath() { + return this.normalizeCachePath('APP_ROUTES_CACHE', 'cache/routes-v7.json'); + } + + eventsAreCached() { + return this['files'].exists(this.getCachedEventsPath()); + } + + getCachedEventsPath() { + return this.normalizeCachePath('APP_EVENTS_CACHE', 'cache/events.json'); + } + + normalizeCachePath($key, $default) { + let $env = Env.get($key) + if ($env == null) { + return this.bootstrapPath($default); + } + + return $env.startsWith(this.absoluteCachePathPrefixes) ? + $env : + this.basePath($env); + } + +} + +module.exports = Application \ No newline at end of file diff --git a/auth/access/authorizable.js b/auth/access/authorizable.js new file mode 100644 index 0000000..f36312d --- /dev/null +++ b/auth/access/authorizable.js @@ -0,0 +1,4 @@ +class Authorizable { + +} +module.exports = Authorizable \ No newline at end of file diff --git a/auth/guardResolver.js b/auth/guardResolver.js new file mode 100644 index 0000000..b1e8903 --- /dev/null +++ b/auth/guardResolver.js @@ -0,0 +1,3 @@ +class GuardResolver { + +} \ No newline at end of file diff --git a/auth/user.js b/auth/user.js new file mode 100644 index 0000000..d062335 --- /dev/null +++ b/auth/user.js @@ -0,0 +1,14 @@ +const Authenticatable = require('@ostro/auth/authenticatable'); +const Model = require('@ostro/database/eloquent/model') +const Crypt = require('@ostro/support/facades/crypt') +class User extends implement(Model, Authenticatable) { + + async createToken(){ + let token = Crypt.encrypt(this.getAttribute(this.getKeyName())+Date.now()) + this.setApiToken(token) + await this.save() + return {accessToken:token} + } +} + +module.exports = User \ No newline at end of file diff --git a/bootstrap/bootProviders.js b/bootstrap/bootProviders.js new file mode 100644 index 0000000..8923dda --- /dev/null +++ b/bootstrap/bootProviders.js @@ -0,0 +1,8 @@ +class BootProviders { + + bootstrap($app) { + $app.boot(); + } +} + +module.exports = BootProviders \ No newline at end of file diff --git a/bootstrap/handleSystemError.js b/bootstrap/handleSystemError.js new file mode 100644 index 0000000..512c4bf --- /dev/null +++ b/bootstrap/handleSystemError.js @@ -0,0 +1,36 @@ +class HandleSystemError { + + bootstrap($app) { + let logger = $app['logger'] + process + .on('unhandledRejection', (reason, p) => { + if (!logger.getConfig('ignore_exceptions')) { + if (reason instanceof Error) { + reason = reason.stack + } else if (typeof reason == 'object') { + reason = JSON.stringify(reason, undefined, 2) + } + reason = reason.toString() + logger.error('[unhandledRejection] :- ' + reason) + logger.channel('console').error('[unhandledRejection] :- ' + reason) + } + + }) + .on('uncaughtException', (err) => { + if (!logger.getConfig('ignore_exceptions')) { + if (err instanceof Error) { + err = err.stack + } else if (typeof err == 'object') { + err = JSON.stringify(err, undefined, 2) + } + err = err.toString() + logger.error('[uncaughtException] :- ' + err) + logger.channel('console').error('[uncaughtException] :- ' + err) + + } + }); + + } + +} +module.exports = HandleSystemError \ No newline at end of file diff --git a/bootstrap/loadConfiguration.js b/bootstrap/loadConfiguration.js new file mode 100644 index 0000000..cbb8f07 --- /dev/null +++ b/bootstrap/loadConfiguration.js @@ -0,0 +1,54 @@ +const Repository = require('@ostro/config/repository') +const fs = require('fs') +const path = require('path') +class LoadConfiguration { + constructor($app) { + this.$app = $app + } + + bootstrap($app) { + let $items = []; + let config = new Repository($items) + $app.instance('config', config); + this.loadConfigurationFiles($app, config); + this.setTimezone(config['app']['timezone']) + + } + + loadConfigurationFiles($app, $repository) { + let configPath = $app.configPath(); + let files = this.getConfigurationFiles(configPath); + + if (files.indexOf('app') < 0) { + throw new Exception('Unable to load the "app" configuration file.'); + } + + for (let key of files) { + $repository.set(key, require(path.normalize(path.join(configPath, key)))); + } + } + + getConfigurationFiles(configPath) { + let files = []; + + let configFiles = fs.readdirSync(configPath) + for (let dir of configFiles) { + let fileName = dir.substr(0, dir.indexOf('.')) + if (configFiles[fileName]) { + if (typeof configFiles[fileName] == 'object') { + files.push(configFiles[fileName]) + } + } else { + files.push(fileName) + } + } + + return files; + } + + setTimezone(tz) { + process.env.TZ = tz + } + +} +module.exports = LoadConfiguration \ No newline at end of file diff --git a/bootstrap/loadEnvironmentVariables.js b/bootstrap/loadEnvironmentVariables.js new file mode 100644 index 0000000..35acd21 --- /dev/null +++ b/bootstrap/loadEnvironmentVariables.js @@ -0,0 +1,14 @@ +const Env = require('dotenv') +const path = require('path') +class LoadEnvironmentVariables { + + bootstrap($app) { + + Env.config({ + path: path.normalize(path.join($app.environmentPath(), $app.environmentFile())) + }) + + } + +} +module.exports = LoadEnvironmentVariables \ No newline at end of file diff --git a/bootstrap/registerFacades.js b/bootstrap/registerFacades.js new file mode 100644 index 0000000..2bd40d0 --- /dev/null +++ b/bootstrap/registerFacades.js @@ -0,0 +1,11 @@ +const Facade = require('@ostro/support/facades/facade') +const AliasLoader = require('@ostro/foundation/aliasLoader') +class RegisterFacades { + + bootstrap($app) { + Facade.setFacadeApplication($app); + let aliasLoader = new AliasLoader($app.make('config').get('app.aliases', [])) + aliasLoader.registerGlobal() + } +} +module.exports = RegisterFacades \ No newline at end of file diff --git a/bootstrap/registerProviders.js b/bootstrap/registerProviders.js new file mode 100644 index 0000000..ba39107 --- /dev/null +++ b/bootstrap/registerProviders.js @@ -0,0 +1,8 @@ +class RegisterProviders { + + bootstrap($app) { + $app.registerConfiguredProviders(); + } +} + +module.exports = RegisterProviders \ No newline at end of file diff --git a/bootstrap/requestBound.js b/bootstrap/requestBound.js new file mode 100644 index 0000000..bc5096c --- /dev/null +++ b/bootstrap/requestBound.js @@ -0,0 +1,19 @@ +const HttpRequest = require('@ostro/http/request') +const CoreHttpRequest = require('../http/request') +const ServerRequest = require('@ostro/server/request') + +class RequestBound { + + bootstrap($app) { + ServerRequest.prototype['app'] = $app + for (let a of Object.getOwnPropertyNames(HttpRequest.prototype)) { + ServerRequest.prototype[a] = HttpRequest.prototype[a]; + } + for (let a of Object.getOwnPropertyNames(CoreHttpRequest.prototype)) { + ServerRequest.prototype[a] = CoreHttpRequest.prototype[a]; + } + + } +} + +module.exports = RequestBound \ No newline at end of file diff --git a/bootstrap/responseBound.js b/bootstrap/responseBound.js new file mode 100644 index 0000000..82a8387 --- /dev/null +++ b/bootstrap/responseBound.js @@ -0,0 +1,20 @@ +const HttpResponse = require('@ostro/http/response') +const CoreHttpResponse = require('@ostro/foundation/http/response') +const ServerResponse = require('@ostro/server/response') + +class ResponseBound { + + bootstrap($app) { + + ServerResponse.prototype['app'] = $app + + for (var a of Object.getOwnPropertyNames(HttpResponse.prototype)) { + ServerResponse.prototype[a] = HttpResponse.prototype[a]; + } + for (var a of Object.getOwnPropertyNames(CoreHttpResponse.prototype)) { + ServerResponse.prototype[a] = CoreHttpResponse.prototype[a]; + } + } +} + +module.exports = ResponseBound \ No newline at end of file diff --git a/console/consoleMakeCommand.js b/console/consoleMakeCommand.js new file mode 100644 index 0000000..215c28b --- /dev/null +++ b/console/consoleMakeCommand.js @@ -0,0 +1,49 @@ +const GeneratorCommand = require('@ostro/console/generatorCommand') + +class FactoryMakeCommand extends GeneratorCommand { + + get $signature() { + return 'make:command'; + } + + get $description() { + return 'Create a new Assistant command' + }; + + get $options() { + return [ + this.createOption('--command [command] ', 'The terminal command that should be assigned'), + ] + } + + get $arguments() { + return [ + this.createArgument('[name]', 'The name of the command').required() + ] + } + + get $type() { + return 'Console command' + } + + replaceClass($stub, $name) { + $stub = super.replaceClass($stub, $name); + return $stub.replaceAll(['dummy:command', '{{ command }}'], this.option('command')); + } + + getStub() { + return this.resolveStubPath('/stubs/console.stub'); + } + + resolveStubPath($stub) { + let $customPath = this.$app.basePath(trim($stub, '/')) + return this.$file.exists($customPath).then($exists => ($exists ? $customPath : path.join(__dirname, $stub))) + } + + getDefaultNamespace($rootNamespace) { + return path.join($rootNamespace, 'app', 'console', 'commands'); + } + +} + +module.exports = FactoryMakeCommand \ No newline at end of file diff --git a/console/eventGenerateCommand.js b/console/eventGenerateCommand.js new file mode 100644 index 0000000..f1a409d --- /dev/null +++ b/console/eventGenerateCommand.js @@ -0,0 +1,37 @@ +const Command = require('@ostro/console/command') +const EventServiceProvider = require('@ostro/event/eventServiceProvider') +class EventGenerateCommand extends Command { + + get $signature() { + return 'event:generate' + }; + + get $description() { + return 'Generate the missing events and listeners based on registration'; + } + + async handle() { + + this.info('Events and listeners generated successfully!'); + } + + makeEventAndListeners($event, $listeners) { + if (!$event.includes('\\')) { + return; + } + + this.callSilent('make:event', { 'name': $event }); + + this.makeListeners($event, $listeners); + } + + makeListeners($event, $listeners) { + for (let $listener of $listeners) { + $listener = $listener.replace('/@.+$/', ''); + + this.callSilent('make:listener', Object.filter({ 'name': $listener, '--event': $event })); + } + } +} + +module.exports = EventGenerateCommand \ No newline at end of file diff --git a/console/eventMakeCommand.js b/console/eventMakeCommand.js new file mode 100644 index 0000000..18274e9 --- /dev/null +++ b/console/eventMakeCommand.js @@ -0,0 +1,37 @@ +const GeneratorCommand = require('@ostro/console/generatorCommand') + +class EventMakeCommand extends GeneratorCommand { + + get $signature() { + return 'make:event'; + } + + get $description() { + return 'Create a new event class'; + } + + get $type() { + return 'Event'; + } + + alreadyExists($rawName) { + return this.$file.exists(this.getPath(this.qualifyClass($rawName))); + } + + getStub() { + return this.resolveStubPath('/stubs/event.stub'); + } + + async resolveStubPath($stub) { + let $customPath = this.$app.basePath(trim($stub, '/')) + return await this.$file.exists($customPath) ? + $customPath : + __dirname + $stub; + } + + getDefaultNamespace($rootNamespace) { + return path.join($rootNamespace, 'app', 'events'); + } +} + +module.exports = EventMakeCommand \ No newline at end of file diff --git a/console/kernel.js b/console/kernel.js new file mode 100644 index 0000000..8992a33 --- /dev/null +++ b/console/kernel.js @@ -0,0 +1,112 @@ +const fs = require('fs-extra') +const path = require('path') +const KernelContract = require('@ostro/contracts/console/kernel') +const Assistant = require('@ostro/console/application') +const kCommandLoaded = Symbol('commandsLoaded') +class Kernel extends KernelContract { + get $commandsLoaded() { + return this[kCommandLoaded] = this[kCommandLoaded] || false + } + + set $commandsLoaded(value) { + return this[kCommandLoaded] = value + } + + get bootstrappers() { + return [ + '@ostro/foundation/bootstrap/loadEnvironmentVariables', + '@ostro/foundation/bootstrap/loadConfiguration', + '@ostro/foundation/bootstrap/registerFacades', + '@ostro/foundation/bootstrap/registerProviders', + '@ostro/foundation/bootstrap/bootProviders', + ]; + } + + get $commands() { + return {} + } + + constructor() { + super() + + this.bootstrap(); + + } + + bootstrap() { + if (!this.$app.hasBeenBootstrapped()) { + this.$app.bootstrapWith(this.getBootstrappers()); + } + this.$app.loadDeferredProviders(); + if (!this.$commandsLoaded) { + this.commands(); + this.$commandsLoaded = true; + } + } + + getAssistant() { + if (!this.assistant) { + return this.assistant = (new Assistant(this.$app, this.$app.version())) + .resolveCommands(this.$commands); + } + + return this.assistant; + } + + getBootstrappers() { + return this.bootstrappers; + } + + schedule($schedule) { + + } + + async handle($input, $output = null) { + try { + await this.getAssistant().run($input, $output); + } catch ($e) { + this.reportException($e); + + this.renderException($e); + } finally { + process.exit(this.getAssistant().getAutoExit()) + + } + } + + load($paths) { + if (typeof $paths == 'string') { + $paths = path.resolve($paths) + if (fs.existsSync($paths)) { + let state = fs.lstatSync($paths) + if (state.isFile()) { + return this.load(require($paths)) + } else { + return this.load(fs.readdirSync($paths).map(filename => this.load(path.resolve($paths, filename)))); + } + } + } else if ($paths instanceof Array) { + return $paths.map(file => this.load(file)) + } + if (typeof $paths == 'function') { + Assistant.addBootstraper(function($assistant) { + $assistant.resolve($paths) + }) + } + } + + reportException($e) { + this.$app['@ostro/contracts/exception/handler'].report($e); + + } + + renderException($e) { + this.$app['@ostro/contracts/exception/handler'].renderForConsole($e); + } + + commands() { + this.load(__dirname + '/commands'); + } + +} +module.exports = Kernel \ No newline at end of file diff --git a/console/keyGenerateCommand.js b/console/keyGenerateCommand.js new file mode 100644 index 0000000..309fc44 --- /dev/null +++ b/console/keyGenerateCommand.js @@ -0,0 +1,71 @@ +const Command = require('@ostro/console/command') +const Crypto = require('crypto') +class KeyGenerateCommand extends Command { + + get $signature() { + return 'key:generate'; + } + + get $description() { + return 'Set the application key' + }; + + get $options() { + return [ + this.createOption('--show [command] ', 'Display the key instead of modifying files'), + this.createOption('--force [command] ', 'Force the operation to run when in production'), + ] + } + + constructor(file) { + super() + this.$file = file + } + + async handle() { + let $key = this.generateRandomKey(); + + if (this.option('show')) { + return this.line('' + $key + ''); + } + + if (!await this.setKeyInEnvironmentFile($key)) { + return; + } + + this.$app['config']['app.key'] = $key; + + this.info('Application key set successfully.'); + } + + generateRandomKey() { + let $cipher = this.$app['config']['app.cipher'] + let $length = $cipher == 'AES-128-CBC' ? 16 : ($cipher == 'AES-192-CBC' ? 24 : 32) + return 'base64:' + Crypto.randomBytes($length).toString('base64'); + } + + async setKeyInEnvironmentFile($key) { + let $currentKey = this.$app['config']['app.key']; + + if ($currentKey.length !== 0 && (!await this.confirmToProceed())) { + return false; + } + + await this.writeNewEnvironmentFileWith($key); + + return true; + } + + writeNewEnvironmentFileWith($key) { + return this.$file.get(this.$app.environmentFilePath()).then(res => { + return this.$file.put(this.$app.environmentFilePath(), res.replace(this.keyReplacementPattern(), 'APP_KEY=' + $key)) + }) + } + + keyReplacementPattern() { + + return 'APP_KEY=' + this.$app['config']['app.key']; + } +} + +module.exports = KeyGenerateCommand \ No newline at end of file diff --git a/console/modelMakeCommand.js b/console/modelMakeCommand.js new file mode 100644 index 0000000..85626db --- /dev/null +++ b/console/modelMakeCommand.js @@ -0,0 +1,118 @@ +const GeneratorCommand = require('@ostro/console/generatorCommand') + +class ModelMakeCommand extends GeneratorCommand { + + get $signature() { + return 'make:model'; + } + + get $description() { + return 'Create a new Eloquent model class' + }; + + get $options() { + return [ + this.createOption('-a, --all', 'Generate a migration, seeder, factory, and resource controller for the model'), + this.createOption('-c, --controller', 'Create a new controller for the model'), + this.createOption('-f, --factory', 'Create a new factory for the model'), + this.createOption('--force', 'Create the class even if the model already exists'), + this.createOption('-m, --migration', 'Create a new migration file for the model'), + this.createOption('-s, --seed', 'Create a new seeder file for the model'), + this.createOption('-p, --pivot', 'Indicates if the generated model should be a custom intermediate table model'), + this.createOption('-r, --resource', 'Indicates if the generated controller should be a resource controller'), + this.createOption('--api', 'Indicates if the generated controller should be an API controller'), + + ] + } + + get $type() { + return 'Model'; + } + + get $dirname() { + return __dirname + } + + async handle() { + + if (await super.handle() === false && !this.option('force')) { + return false; + } + + if (this.option('all')) { + this.input.setOption('factory', true); + this.input.setOption('seed', true); + this.input.setOption('migration', true); + this.input.setOption('controller', true); + this.input.setOption('resource', true); + } + + if (this.option('factory')) { + await this.createFactory(); + } + + if (this.option('migration')) { + await this.createMigration(); + } + + if (this.option('seed')) { + await this.createSeeder(); + } + + if (this.option('controller') || this.option('resource') || this.option('api')) { + await this.createController(); + } + } + + createFactory() { + let $factory = this.argument('name'); + return this.callCommand('make:factory', { + 'name': `${$factory}Factory`, + '--model': this.qualifyClass(this.getNameInput()), + }); + } + + createMigration() { + let $table = this.getFileName(this.argument('name')).plural().camelCase().snakeCase(); + + return this.callCommand('make:migration', { + 'name': `create_${$table}_table`, + '--create': $table, + '--relativepath': this.getNamespace(this.argument('name')), + }); + } + + createSeeder() { + let $seeder = this.argument('name'); + + return this.callCommand('make:seeder', { + 'name': `${$seeder}Seeder`, + }); + } + + createController() { + let $controller = this.argument('name'); + + let $modelName = this.qualifyClass(this.getNameInput()); + + return this.callCommand('make:controller', Object.filter({ + 'name': `${$controller}Controller`, + '--model': this.option('resource') ? $modelName : null, + })); + } + + getStub() { + return this.resolveStubPath('/stubs/model.stub'); + } + + getDefaultNamespace($rootNamespace) { + return $rootNamespace.includes('.js') ? $rootNamespace : path.resolve($rootNamespace, app_path('models')); + } + + resolveStubPath($stub) { + let $customPath = this.$app.basePath(trim($stub, '/')) + return this.$file.exists($customPath).then($exists => ($exists ? $customPath : path.join(__dirname, $stub))) + } + +} +module.exports = ModelMakeCommand \ No newline at end of file diff --git a/console/resourceMakeCommand.js b/console/resourceMakeCommand.js new file mode 100644 index 0000000..1e6ba4f --- /dev/null +++ b/console/resourceMakeCommand.js @@ -0,0 +1,52 @@ +const GeneratorCommand = require('@ostro/console/generatorCommand') + +class ResourceMakeCommand extends GeneratorCommand { + + get $signature() { + return 'make:resource'; + } + + get $description() { + return 'Create a new resource'; + } + + get $type() { + return 'Resource'; + } + + get $options() { + return [this.createOption('-c, --collection', 'Create a resource collection')] + } + + async handle() { + if (this.collection()) { + this.$type = 'Resource collection'; + } + + await super.handle(); + } + + getStub() { + return this.collection() ? + this.resolveStubPath('/stubs/resource-collection.stub') : + this.resolveStubPath('/stubs/resource.stub'); + } + + collection() { + return this.option('collection') || + String.endsWith(this.argument('name'), 'Collection'); + } + + async resolveStubPath($stub) { + let $customPath = this.$app.basePath(trim($stub, path.sep)) + return await this.$file.exists($customPath) ? + $customPath : + __dirname + $stub; + } + + getDefaultNamespace($rootNamespace) { + return path.join($rootNamespace, 'app', 'http', 'resources'); + } + +} +module.exports = ResourceMakeCommand \ No newline at end of file diff --git a/console/serveCommand.js b/console/serveCommand.js new file mode 100644 index 0000000..b9e75dc --- /dev/null +++ b/console/serveCommand.js @@ -0,0 +1,66 @@ +const Command = require('@ostro/console/command') + +class ServeCommand extends Command { + + get $portOffset() { + return 0 + }; + + get $signature() { + return 'serve'; + } + + get $description() { + return 'Serve the application on the nodejs development server' + }; + + get $options() { + return [ + this.createOption('--host ', 'The host address to serve the application on').default('127.1.0.0', 'localhost ip'), + this.createOption('--port [port] ', 'The port to serve the application on').default(8000, 'Recomanded port'), + this.createOption('--tries [tries] ', 'The max number of ports to attempt to serve from'), + this.createOption('--no-reload [no-reload] ', 'Do not reload the development server on .env file changes'), + ] + } + + constructor(server, file) { + super() + this.$server = server + this.$file = file + } + + async handle() { + this.line(`Starting Ostro development server: http://${this.host()}:${this.port()}`); + + process.env.PORT = this.port() + process.env.HOST = this.host() + + this.requireServerFile(this.serverStarterPath()); + + } + + serverStarterPath() { + return base_path('app.js') + } + + requireServerFile($path) { + return require($path) + } + + host() { + return this.input.getOption('host'); + } + + port() { + let $port = this.input.getOption('port') + + return $port + this.$portOffset; + } + + canTryAnotherPort() { + return is_null(this.input.getOption('port')) && (this.input.getOption('tries') > this.$portOffset); + } + +} + +module.exports = ServeCommand \ No newline at end of file diff --git a/console/storageLinkCommand.js b/console/storageLinkCommand.js new file mode 100644 index 0000000..c63a938 --- /dev/null +++ b/console/storageLinkCommand.js @@ -0,0 +1,59 @@ +const Command = require('@ostro/console/command') +class StorageLinkCommand extends Command { + + get $signature() { + return 'storage:link' + } + + get $description() { + return 'Create the symbolic links configured for the application' + }; + + get $options() { + return [ + this.createOption('--relative', 'The host address to serve the application on'), + this.createOption('--force', 'The port to serve the application on') + ] + } + constructor($file) { + super() + this.$file = $file + } + + async handle() { + let $relative = this.option('relative'); + let links = this.links() + for (let $link in links) { + let $target = links[$link] + if (await this.$file.exists($link) && !await this.isRemovableSymlink($link, this.option('force'))) { + this.error(`The [${$link}] link already exists.`); + continue; + } + + if (await isSymbolicLink($link)) { + await this.$file.delete($link); + } + + if ($relative) { + await this.$file.relativeLink($target, $link); + } else { + await this.$file.link($target, $link); + } + + this.info(`The [${$link}] link has been connected to [${$target}].`); + } + + this.info('The links have been created.'); + } + + links() { + return this.$app['config']['filesystems.links'] || { + [public_path('storage')]: storage_path('app/public') }; + } + + isRemovableSymlink($link, $force) { + return isSymbolicLink($link) && $force; + } +} + +module.exports = StorageLinkCommand \ No newline at end of file diff --git a/console/stubs/console.stub b/console/stubs/console.stub new file mode 100644 index 0000000..a25bcad --- /dev/null +++ b/console/stubs/console.stub @@ -0,0 +1,15 @@ +const Command = require('@ostro/console/command') + +class {{ class }} extends Command +{ + + $signature = '{{ command }}'; + + $description = 'Command description'; + + handle(){ + + } +} + +module.exports = {{ class }} \ No newline at end of file diff --git a/console/stubs/event.stub b/console/stubs/event.stub new file mode 100644 index 0000000..de5ea2e --- /dev/null +++ b/console/stubs/event.stub @@ -0,0 +1,12 @@ + +class {{ class }} +{ + + constructor() + { + // + } + +} + +module.exports = {{ class }} \ No newline at end of file diff --git a/console/stubs/model.pivot.stub b/console/stubs/model.pivot.stub new file mode 100644 index 0000000..4890c21 --- /dev/null +++ b/console/stubs/model.pivot.stub @@ -0,0 +1,7 @@ +const Model = require('@ostro/database/eloquent/model') + +class {{ class }} extends Model{ + +} + +module.exports = {{ class }} diff --git a/console/stubs/model.stub b/console/stubs/model.stub new file mode 100644 index 0000000..4890c21 --- /dev/null +++ b/console/stubs/model.stub @@ -0,0 +1,7 @@ +const Model = require('@ostro/database/eloquent/model') + +class {{ class }} extends Model{ + +} + +module.exports = {{ class }} diff --git a/console/stubs/provider.stub b/console/stubs/provider.stub new file mode 100644 index 0000000..624bd09 --- /dev/null +++ b/console/stubs/provider.stub @@ -0,0 +1,17 @@ +const ServiceProvider = require('@ostro/support/serviceProvider') + +class DummyClass extends ServiceProvider +{ + + public function register() + { + // + } + + public function boot() + { + // + } +} + +module.exports = DummyClass \ No newline at end of file diff --git a/console/stubs/resource-collection.stub b/console/stubs/resource-collection.stub new file mode 100644 index 0000000..5b7882e --- /dev/null +++ b/console/stubs/resource-collection.stub @@ -0,0 +1,10 @@ +const JsonResourceCollection = require("@ostro/http/resources/json/resourceCollection") +class {{ class }} extends JsonResourceCollection { + + static toObject(data) { + return data + } + +} + +module.exports = {{ class }}; \ No newline at end of file diff --git a/console/stubs/resource.stub b/console/stubs/resource.stub new file mode 100644 index 0000000..e277fe9 --- /dev/null +++ b/console/stubs/resource.stub @@ -0,0 +1,10 @@ +const jsonResource = require("@ostro/http/resources/json/jsonResources") +class {{ class }} extends jsonResource { + + static toObject(data) { + return data + } + +} + +module.exports = {{ class }}; \ No newline at end of file diff --git a/exception/handler.js b/exception/handler.js new file mode 100644 index 0000000..2ae82f9 --- /dev/null +++ b/exception/handler.js @@ -0,0 +1,159 @@ +const PageNotFoundException = require('@ostro/contracts/http/pageNotFoundException') +const validationException = require('@ostro/contracts/validation/validationException') +const TokenMismatchException = require('@ostro/contracts/http/tokenMismatchException') +const FileNotFoundException = require('@ostro/contracts/filesystem/fileNotFoundException') +const FileUploadException = require('@ostro/contracts/filesystem/fileUploadException') +const JsonException = require('@ostro/contracts/http/jsonException') +const Redirect = require('@ostro/contracts/http/redirectResponse') +const Session = require('@ostro/contracts/session/session') +const AuthenticationException = require('@ostro/auth/authenticationException') +const path = require('path') +const fs = require('fs') +const kHandle = Symbol('handle') +const kLogger = Symbol('logger') + +class Handler { + $dontReport = [ + validationException, + JsonException, + Redirect, + TokenMismatchException + ]; + + $dontFlash = []; + + constructor(handler) { + this[kHandle] = handler + this[kLogger] = app('logger') + + } + + handle(request, response, $e = {}) { + this.render(request, response, $e) + + } + + render(request, response, $e = {}) { + if ($e instanceof PageNotFoundException) { + return this.pageNotFoundException(request, response, $e) + } else if ($e instanceof validationException) { + return this.convertValidationExceptionToResponse(request, response, $e); + } else if ($e instanceof TokenMismatchException) { + return this.tokenMismatchException(request, response, $e) + } else if ($e instanceof AuthenticationException) { + return this.unauthenticated(request, response, $e); + } else if ($e instanceof Redirect) { + return this.redirect(response, $e) + } else if ($e instanceof FileNotFoundException) { + return response.send({ + name: $e.name, + message: $e.message + }, $e.statusCode) + } else if ($e instanceof FileUploadException) { + return response.send({ + name: $e.name, + message: $e.message + }, $e.statusCode) + } else if ($e instanceof JsonException) { + return this.send({ + name: $e.name, + message: $e.message, + errors: $e.errors + }, $e.statusCode) + } else if (typeof $e.message == 'object') { + $e = $e.message + } + this.report($e) + this[kHandle].render( + request, + response, + $e + ) + + } + + unauthenticated($request, $response, $exception) + { + return $request.expectsJson() + ? $response.json({'message' : $exception.message}, 401) + : $response.redirect().to($exception.redirectTo() || route('login')); + } + + redirect(response, $e) { + response.with($e.getFlash()).withErrors($e.getErrors()).withInput($e.wantedInput()).redirect($e.getUrl()); + } + + jsonException(response, $e) { + + } + + send(request, response, $e = {}) { + this.report($e) + response.send($e) + } + + convertValidationExceptionToResponse(request, response, $e) { + if ($e.response) { + return $e.response; + } + + return request.expectsJson() ? + this.invalidJson(request, response, $e) : + this.invalid(request, response, $e); + } + + invalidJson(request, response, $exception) { + return response.json({ + 'message': $exception.getMessage(), + 'errors': $exception.getErrors(), + }, $exception.status); + } + + invalid(request, response, $exception) { + + if(request.session instanceof Session){ + response.withInput(Object.except(request.input(), this.$dontFlash)) + .withErrors($exception.all()) + } + response.redirect($exception.redirectTo || 'back') + } + + tokenMismatchException(request, response, $e) { + if (request.wantJson() || request.ajax()) { + response.send({ + name: $e.name || 'Token mismatch exception', + errors: $e.errors || {}, + message: $e.message || 'Page Expired' + }, ($e.statusCode || 403)) + } else { + return fs.readFile(path.join(__dirname, 'template/errors/403'), { + encoding: 'utf8' + }, function(error, data) { + response.send(data, $e.statusCode) + }) + } + } + pageNotFoundException(request, response, $e) { + return fs.readFile(path.join(__dirname, 'template/errors/404'), { + encoding: 'utf8' + }, function(error, data) { + response.send(data, $e.statusCode) + }) + } + report($e) { + if (!this.$dontReport.find((clazz) => $e instanceof clazz)) { + if (typeof this[kLogger].getConfig == 'function' && !this[kLogger].getConfig('ignore_exceptions')) { + if (typeof $e == 'object') { + if (typeof $e.stack == 'string') { + $e.capture = $e.stack.split('\n at ') + } + } + this[kLogger].error($e) + } + } + } + renderForConsole($e) { + this[kLogger].channel('console').error($e) + } +} +module.exports = Handler \ No newline at end of file diff --git a/exception/handlerManager.js b/exception/handlerManager.js new file mode 100644 index 0000000..f0d6b5a --- /dev/null +++ b/exception/handlerManager.js @@ -0,0 +1,92 @@ +const { + Macroable +} = require('@ostro/support/macro') +const kApp = Symbol('app') +const kHandlers = Symbol('handlers') +const kCustomHandlers = Symbol('customHandlers') +const kHandlerAdapter = Symbol('handlerAdapter') +const InvalidArgumentException = require('@ostro/support/exceptions/invalidArgumentException') +class ExceptionManager extends Macroable { + constructor(handler) { + super() + Object.defineProperties(this, { + [kHandlerAdapter]: { + value: handler, + writable: true, + }, + [kHandlers]: { + value: Object.create(null), + writable: true, + }, + [kCustomHandlers]: { + value: Object.create(null), + writable: true, + }, + }) + } + + handler(name = 'whoops') { + name = (this.getConfig('app.debug') == false) ? 'production' : name + + return this[kHandlers][name] = this.get(name); + } + + get(name) { + return this[kHandlers][name] || this.resolve(name); + } + + resolve(name) { + let driverMethod = 'create' + (name.ucfirst()) + 'Handler'; + if ((this[kCustomHandlers][name])) { + return this.callCustomHandler(); + } else if (this[driverMethod]) { + return this[driverMethod](); + } else { + throw new Error(`Handler [{${name}}] do not supported.`); + } + } + + callCustomHandler(name) { + var driver = this[kCustomHandlers][name](); + return this.adapt($driver); + } + + createWhoopsHandler($config) { + return this.adapt(new(require('./handlers/whoops'))); + } + + createProductionHandler() { + return this.adapt(new(require('./handlers/production'))); + } + + createJsonHandler() { + return this.adapt(new(require('./handlers/json'))); + } + + adapt(handler) { + if (typeof this[kHandlerAdapter] != 'function') { + throw new InvalidArgumentException('Invalid handler class on exception') + } + return new this[kHandlerAdapter](handler) + } + + getConfig(key) { + return this.$app['config'][key] + } + + setHandlerAdapter(handler) { + this[kHandlerAdapter] = handler + } + + extends($driver, $callback) { + if (!config) { + throw new InvalidArgumentException(`Config not found for [{${$driver}}] driver.`); + } + this[kCustomCreators][$driver] = $callback.call(this, this); + return this; + } + __call(target, method, args) { + return target.handler()[method](...args) + } +} +module.exports = ExceptionManager \ No newline at end of file diff --git a/exception/handlers/json.js b/exception/handlers/json.js new file mode 100644 index 0000000..8ff799e --- /dev/null +++ b/exception/handlers/json.js @@ -0,0 +1,24 @@ +const Ouch = require('ouch'); +const kHandler = Symbol('handler') +class Json { + constructor() { + this[kHandler] = this.register() + } + register() { + return (new Ouch([new Ouch.handlers.JsonResponseHandler(false, true, false)])) + } + render(request, response, $exception = {}) { + this[kHandler].handleException($exception, null, null, function(exception) { + let parsedObj = JSON.parse(exception).error || {} + response.json({ + name: parsedObj.type, + message: parsedObj.message, + errors: ($exception.errors || {}), + file: parsedObj.file, + line: parsedObj.line, + trace: parsedObj.trace, + }, $exception.statusCode || 500) + }); + } +} +module.exports = Json \ No newline at end of file diff --git a/exception/handlers/production.js b/exception/handlers/production.js new file mode 100644 index 0000000..03bd5b4 --- /dev/null +++ b/exception/handlers/production.js @@ -0,0 +1,21 @@ +const fs = require('fs') +class Production { + + register() { + + } + render(request, response, $exception = {}) { + if (request.wantJson() || request.ajax()) { + response.send({ + name: $exception.name || 'HttpException', + errors: $exception.errors || {}, + message: $exception.message || 'Whoops look like somthing wrong' + }, ($exception.statusCode || 500)) + } else { + return fs.readFile(path.join(__dirname, '../template/errors/500'), { encoding: 'utf8' }, function(error, data) { + response.send(data, 500) + }) + } + } +} +module.exports = Production \ No newline at end of file diff --git a/exception/handlers/whoops.js b/exception/handlers/whoops.js new file mode 100644 index 0000000..2356f4c --- /dev/null +++ b/exception/handlers/whoops.js @@ -0,0 +1,17 @@ +const Ouch = require('ouch'); +const kHandler = Symbol('handler') +class Whoops { + constructor() { + this[kHandler] = this.register() + } + register() { + return (new Ouch).pushHandler(new Ouch.handlers.PrettyPageHandler('orange')) + } + render(request, response, $exception = {}) { + if (!response.headersSent) { + response.writeHead(($exception.statusCode || 500), 'Content-Type: text/html'); + } + this[kHandler].handleException($exception, request, response) + } +} +module.exports = Whoops \ No newline at end of file diff --git a/exception/middleware/exceptionHandler.js b/exception/middleware/exceptionHandler.js new file mode 100644 index 0000000..fe8438c --- /dev/null +++ b/exception/middleware/exceptionHandler.js @@ -0,0 +1,14 @@ +class ExceptionHandler { + constructor() { + this.$exceptionHandler = this.$app.make('@ostro/contracts/exception/handler') + } + handle(error, { request, response },next) { + this.$exceptionHandler.handler(((request.ajax() || request.wantJson()) ? 'json' : 'whoops')).handle( + request, + response, + error + ) + } +} + +module.exports = ExceptionHandler \ No newline at end of file diff --git a/exception/template/errors/401 b/exception/template/errors/401 new file mode 100644 index 0000000..212ec92 --- /dev/null +++ b/exception/template/errors/401 @@ -0,0 +1,56 @@ + + + + + + + Unauthorized (401) + + + + + + + + +
+
+ 401 | UNAUTHORIZED
+
+ + diff --git a/exception/template/errors/403 b/exception/template/errors/403 new file mode 100644 index 0000000..08b7f0c --- /dev/null +++ b/exception/template/errors/403 @@ -0,0 +1,56 @@ + + + + + + + FORBIDDEN (403) + + + + + + + + +
+
+ 403 | FORBIDDEN
+
+ + diff --git a/exception/template/errors/404 b/exception/template/errors/404 new file mode 100644 index 0000000..e8f5ffe --- /dev/null +++ b/exception/template/errors/404 @@ -0,0 +1,55 @@ + + + + + + + Page Not Found (404) + + + + + + + + +
+
404 | PAGE NOT FOUND
+
+ + diff --git a/exception/template/errors/419 b/exception/template/errors/419 new file mode 100644 index 0000000..1cca6f4 --- /dev/null +++ b/exception/template/errors/419 @@ -0,0 +1,56 @@ + + + + + + + Page Expired (419) + + + + + + + + +
+
+ 419 | PAGE EXPIRED
+
+ + diff --git a/exception/template/errors/429 b/exception/template/errors/429 new file mode 100644 index 0000000..2e8323b --- /dev/null +++ b/exception/template/errors/429 @@ -0,0 +1,56 @@ + + + + + + + Too Many Requests (429) + + + + + + + + +
+
+ 429 | TOO MANY REQUESTS
+
+ + diff --git a/exception/template/errors/500 b/exception/template/errors/500 new file mode 100644 index 0000000..3332dd7 --- /dev/null +++ b/exception/template/errors/500 @@ -0,0 +1,56 @@ + + + + + + + Internal Server Error (500) + + + + + + + + +
+ +
500 | INTERNAL SERVER ERROR
+
+ + diff --git a/exception/template/errors/503 b/exception/template/errors/503 new file mode 100644 index 0000000..3394cc6 --- /dev/null +++ b/exception/template/errors/503 @@ -0,0 +1,56 @@ + + + + + + + Internal Server Error (500) + + + + + + + + +
+
+ 503 | SERVICE UNAVAILABLE
+
+ + diff --git a/http/httpContext.js b/http/httpContext.js new file mode 100644 index 0000000..861acef --- /dev/null +++ b/http/httpContext.js @@ -0,0 +1,33 @@ +const HttpContext = require('@ostro/http/httpContext') +class FoundationHttpContext extends HttpContext { + constructor(request, response, next) { + super(request, response, next) + } + + get auth() { + return this.request.auth + } + + get view() { + return this.response.view + } + + get session() { + return this.request.session + } + + get params() { + return this.request.params + } + + csrf_token() { + return this.session.token() || '' + } + + csrfToken() { + return this.csrf_token() + } + +} + +module.exports = FoundationHttpContext \ No newline at end of file diff --git a/http/kernel.js b/http/kernel.js new file mode 100644 index 0000000..4e20625 --- /dev/null +++ b/http/kernel.js @@ -0,0 +1,126 @@ +const KernelContract = require('@ostro/contracts/http/kernel') +const HttpContext = require('./httpContext') +const kCachedMiddleware = Symbol('cachedMiddleware') +class Kernel extends KernelContract { + + get $bootstrappers() { + return [ + '@ostro/foundation/bootstrap/loadEnvironmentVariables', + '@ostro/foundation/bootstrap/loadConfiguration', + '@ostro/foundation/bootstrap/registerFacades', + '@ostro/foundation/bootstrap/registerProviders', + '@ostro/foundation/bootstrap/bootProviders', + '@ostro/foundation/bootstrap/handleSystemError', + ]; + } + + get $defaultMiddlewares() { + return [] + } + + get $middlewareGroups() { + return {} + } + + get $namedMiddlewares() { + return {} + } + + get $middlewarePriority() { + return [ + require('@ostro/foundation/exception/middleware/exceptionHandler'), + require('@ostro/foundation/view/middleware/BindViewOnResponse'), + + ]; + } + + constructor() { + super() + + this.bootstrap() + this.$router = this.$app['router'] + this.syncMiddlewareToRouter(); + this.PrepareRouter() + } + + handle() { + return this.$router.handle() + } + + syncMiddlewareToRouter() { + this.$router.defaultMiddlewares(this.$middlewarePriority.concat(this.$defaultMiddlewares)) + let allNamedMiddlewares = { + ...this.resolveNamedMiddleware(), + ...this.resolveMiddlewareGroups() + } + for (let key in allNamedMiddlewares) { + let middlewares = Array.isArray(allNamedMiddlewares[key]) ? allNamedMiddlewares[key] : [allNamedMiddlewares[key]] + this.$router.namedMiddleware(key, middlewares); + } + + } + resolveNamedMiddleware() { + let middlewares = {} + for (let key in this.$namedMiddlewares) { + middlewares[key] = this.resolveMiddleware(this.$namedMiddlewares[key]) + } + return middlewares + } + resolveMiddlewareGroups() { + let middlewares = {} + for (let key in this.$middlewareGroups) { + middlewares[key] = this.$middlewareGroups[key].map((middleware) => { + if (typeof middleware == 'string') { + if (!this.$namedMiddlewares[middleware]) { + throw new Error(`${middleware} was not available on namedMiddlewares`) + } + middleware = this.$namedMiddlewares[middleware] + } + return this.resolveMiddleware(middleware) + }) + } + return middlewares + } + resolveMiddleware(middleware) { + return middleware + } + + PrepareRouter() { + HttpContext.prototype.$app = this.$app + this.$router.httpContextHandler(HttpContext) + } + + bootstrap() { + if (!this.$app.hasBeenBootstrapped()) { + this.$app.bootstrapWith(this.getBootstrappers()); + } + } + + getBootstrappers() { + return this.$bootstrappers; + } + + getMiddlewarePriority() { + return this.$middlewarePriority; + } + + getMiddlewareGroups() { + return this.$middlewareGroups; + } + + getRouteMiddleware() { + return this.$routeMiddleware; + } + + getApplication() { + return this.$app; + } + + setApplication($app) { + this.$app = $app; + + return this; + } +} + +module.exports = Kernel \ No newline at end of file diff --git a/http/middleware/serveStatic.js b/http/middleware/serveStatic.js new file mode 100644 index 0000000..7a76d74 --- /dev/null +++ b/http/middleware/serveStatic.js @@ -0,0 +1,21 @@ +const serveStatic = require('serve-static'); +class ServeStatic { + constructor() { + this.$publicPath = $app['path.public'] + } + get $defaultOptions() { + return { + 'maxAge': '180d' + } + } + + get $options(){ + return {} + } + + handle({ request, response }, next) { + serveStatic(this.$publicPath, { ...this.$defaultOptions, ...this.$options })(request, response, next) + } +} + +module.exports = ServeStatic \ No newline at end of file diff --git a/http/middleware/verifyCsrfToken.js b/http/middleware/verifyCsrfToken.js new file mode 100644 index 0000000..a6588f9 --- /dev/null +++ b/http/middleware/verifyCsrfToken.js @@ -0,0 +1,70 @@ +const tokens = require('csrf')() +const TokenMismatchException = require('@ostro/http/exception/tokenMismatchException') +class VerifyCsrfToken { + + $cookieName = 'XSRF-TOKEN'; + + $addHttpCookie = true; + + $except = [ + // + ]; + + handle({ request, response }, next) { + if ( + this.isReading(request) || + this.inExceptArray(request) || + this.tokensMatch(request) + ) { + if (this.shouldAddXsrfTokenCookie()) { + this.addCookieToResponse(request, response); + } + next() + } else { + next(new TokenMismatchException('CSRF token mismatch.')) + } + } + + addCookieToResponse(request, response) { + request.cookie.set(this.$cookieName, request.session.token(), { httpOnly: false }) + + } + + inExceptArray($request) { + for (let $except of this.$except) { + if ($request.fullUrlIs($except)) { + return true; + } + } + + return false; + } + + tokensMatch($request) { + let $token = this.getTokenFromRequest($request); + return (typeof $request.session.token() == 'string') && + (typeof $token == 'string') && + tokens.verify(this.$app.config['app']['key'], $token); + } + + getTokenFromRequest($request) { + let $token = $request.input('_token') ? $request.input('_token') : $request.header('X-CSRF-TOKEN'); + let $header = $request.header('X-XSRF-TOKEN') + if (!$token && $header) { + $token = $header + } + + return $token; + } + + shouldAddXsrfTokenCookie() { + return this.$addHttpCookie; + } + + isReading($request) { + return ['HEAD', 'GET', 'OPTIONS'].includes($request.method); + } + +} + +module.exports = VerifyCsrfToken \ No newline at end of file diff --git a/http/request.js b/http/request.js new file mode 100644 index 0000000..0f53126 --- /dev/null +++ b/http/request.js @@ -0,0 +1,23 @@ +const kErrors = Symbol('errors') +const ErrorBag = require('@ostro/support/errorBag') +const ObjectGet = require('lodash.get') +const Request = require('@ostro/http/request') +class HttpRequest extends Request{ + old(key, defaultValue = null) { + return ObjectGet(this.session.get('__inputs'), key, defaultValue); + } + + error(key, defaultValue = null) { + this[kErrors] = this[kErrors] || new ErrorBag((this.session ? this.session.get('__errors') : {})) + if (key) { + return this[kErrors].first('key') + } + return this[kErrors] + } + + csrfToken() { + return this.session.token() || '' + } +} + +module.exports = HttpRequest \ No newline at end of file diff --git a/http/response.js b/http/response.js new file mode 100644 index 0000000..7034856 --- /dev/null +++ b/http/response.js @@ -0,0 +1,35 @@ +const Response = require('@ostro/http/response') +const Model = require('@ostro/contracts/database/eloquent/model') +const Collection = require('@ostro/contracts/collection/collect') +const GenericUser = require('@ostro/contracts/auth/genericUser') +class HttpResponse extends Response { + send(body, status = 200) { + if (body instanceof Model) { + body = body.serialize() + } else if (body instanceof Collection) { + body = body.toArray() + } else if (body instanceof Collection) { + body = body.toJSON() + } + super.send(body, status) + } + + with(obj = {}) { + this.req.session.flash(obj) + return this; + } + + withInput(data = true) { + if (data) { + this.req.session.flash('__inputs', this.req.except('_token')) + } + return this; + } + + withErrors(errors = {}) { + this.req.session.flash('__errors', errors) + return this + } + +} +module.exports = HttpResponse \ No newline at end of file diff --git a/mix.js b/mix.js new file mode 100644 index 0000000..78123fe --- /dev/null +++ b/mix.js @@ -0,0 +1,57 @@ +const fs = require('fs') +const MixExceptions = require('@ostro/support/exceptions/mixException') +const kManifest = Symbol('manifest') + +class Mix { + + $manifests = {}; + + constructor(app) { + this.$app = app + + } + + path($path, $manifestDirectory = '') { + + if (!String.startsWith($path, '/')) { + $path = `/${$path}`; + } + + if ($manifestDirectory && !String.startsWith($manifestDirectory, '/')) { + $manifestDirectory = `/${$manifestDirectory}`; + } + + let $manifestPath = public_path($manifestDirectory + '/mix-manifest.json'); + + if (!isset(this.$manifests[$manifestPath])) { + try { + let stat = fs.lstatSync($manifestPath) + if (!stat.isFile()) { + throw new MixExceptions('The Mix manifest does not exist.'); + } + + this.$manifests[$manifestPath] = JSON.parse(fs.readFileSync($manifestPath), true); + } catch (e) { + throw new MixExceptions('The Mix manifest does not exist.'); + } + } + + let $manifest = this.$manifests[$manifestPath]; + if (!isset($manifest[$path])) { + let $exception = new MixExceptions(`Unable to locate Mix file: ${$path}.`); + if (!this.$app.instance('config').get('app.debug')) { + this.$app['logger'].report($exception); + return $path; + } else { + throw $exception; + } + } + let mixurl = this.$app['config'].get('app.mix_url') + mixurl = mixurl || '' + return mixurl + $manifestDirectory + $manifest[$path] + + } + +} + +module.exports = Mix \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..664d512 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ostro/foundation", + "version": "0.0.0-alpha.0", + "description": "MVC framework for NodeJS", + "engines": { + "node": ">= 12.0" + }, + "main": "application.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@ostro/auth": "^0.0.0-alpha.0", + "@ostro/config": "^0.0.0-alpha.0", + "@ostro/container": "^0.0.0-alpha.0", + "@ostro/contracts": "^0.0.0-alpha.0", + "@ostro/filesystem": "^0.0.0-alpha.0", + "@ostro/http": "^0.0.0-alpha.0", + "@ostro/logger": "^0.0.0-alpha.0", + "@ostro/support": "^0.0.0-alpha.0", + "dotenv": "10.0.0", + "csrf": "3.1.0", + "fs-extra": "^10.0.0", + "serve-static": "1.14.1", + "lodash.get": "^4.4.2", + "ouch": "^2.0.0" + }, + "keywords": [ + "ostro", + "framework", + "nodejs framework", + "mvc", + "foundation" + ], + "author": "amar", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ostrojs/foundation.git" + }, + "bugs": { + "url": "https://github.com/ostrojs/foundation/issues" + }, + "homepage": "https://github.com/ostrojs/foundation#readme" +} diff --git a/providerRepository.js b/providerRepository.js new file mode 100644 index 0000000..f5017b9 --- /dev/null +++ b/providerRepository.js @@ -0,0 +1,59 @@ +class ProviderRepository { + + constructor($app, $files) { + this.$app = $app; + this.$files = $files; + } + + load($providers) { + + let $manifest = this.compileManifest($providers); + + for (let $provider of $manifest['eager']) { + this.$app.register($provider); + } + + this.$app.addDeferredServices($manifest['deferred']); + } + + loadManifest() { + + } + + compileManifest($providers) { + + let $manifest = this.freshManifest($providers); + + for (let $provider of $providers) { + + let $instance = this.createProvider($provider); + + if ($instance.isDeferred()) { + $manifest['deferred'].push($provider) + } else { + $manifest['eager'].push($provider); + } + } + + return this.writeManifest($manifest); + } + + freshManifest($providers) { + return { + 'providers': $providers, + 'eager': [], + 'deferred': [] + }; + } + + writeManifest($manifest) { + + return $manifest + } + + createProvider($provider) { + return new $provider(this.$app); + } +} + +module.exports = ProviderRepository \ No newline at end of file diff --git a/providers/assistantServiceProvider.js b/providers/assistantServiceProvider.js new file mode 100644 index 0000000..c5a670b --- /dev/null +++ b/providers/assistantServiceProvider.js @@ -0,0 +1,146 @@ +const ServiceProvider = require('@ostro/support/serviceProvider'); + +class AssistantServiceProvider extends ServiceProvider { + $commands = { + 'DbQuery': 'command.db.query', + 'KeyGenerate': 'command.key.generate', + 'Seed': 'command.seed', + 'StorageLink': 'command.storage.link', + 'CacheClear': 'command.cache.clear', + 'CacheForget': 'command.cache.forget', + 'DbWipe': 'command.db.wipe', + }; + + $devCommands = { + 'CacheTable': 'command.cache.table', + 'ConsoleMake': 'command.console.make', + 'ControllerMake': 'command.controller.make', + 'FactoryMake': 'command.factory.make', + 'MiddlewareMake': 'command.middleware.make', + 'ModelMake': 'command.model.make', + 'ResourceMake': 'command.resource.make', + 'SeederMake': 'command.seeder.make', + 'SessionTable': 'command.session.table', + }; + + register() { + this.registerCommands(Object.assign( + this.$commands, this.$devCommands + )); + } + + registerCommands($commands = {}) { + for (let $command of Object.keys($commands)) { + if (typeof this[`register${$command}Command`] != 'function') { + throw new Error(`register${$command}Command not available.`) + } + this[`register${$command}Command`](); + } + + this.commands(Object.values($commands)); + } + + registerDbQueryCommand() { + this.$app.singleton('command.db.query', function($app) { + let $table = $app['config']['database.table']; + return new(require('@ostro/database/console/databaseQuery'))($app['db'], $table); + }); + } + + registerSeederMakeCommand() { + this.$app.singleton('command.seeder.make', function($app) { + return new(require('@ostro/database/console/seeds/seederMakeCommand'))($app['files']); + }); + } + + registerSeedCommand() { + this.$app.singleton('command.seed', function($app) { + return new(require('@ostro/database/console/seeds/seedCommand'))($app['db']); + }); + } + + registerCacheClearCommand() { + this.$app.singleton('command.cache.clear', function($app) { + return new(require('@ostro/cache/console/clearCommand'))($app['cache'], $app['files']); + }); + } + + registerCacheForgetCommand() { + this.$app.singleton('command.cache.forget', function($app) { + return new(require('@ostro/cache/console/forgetCommand'))($app['cache']); + }); + } + + registerCacheTableCommand() { + this.$app.singleton('command.cache.table', function($app) { + return new(require('@ostro/cache/console/cacheTableCommand'))($app['files']); + }); + } + + registerConsoleMakeCommand() { + this.$app.singleton('command.console.make', function($app) { + return new(require('@ostro/foundation/console/consoleMakeCommand'))($app['files']); + }); + } + + registerControllerMakeCommand() { + this.$app.singleton('command.controller.make', function($app) { + return new(require('@ostro/router/console/controllerMakeCommand'))($app['files']); + }); + } + + registerFactoryMakeCommand() { + this.$app.singleton('command.factory.make', function($app) { + return new(require('@ostro/database/console/factories/factoryMakeCommand'))($app['files']); + }); + } + + registerKeyGenerateCommand() { + this.$app.singleton('command.key.generate', function($app) { + return new(require('@ostro/foundation/console/keyGenerateCommand'))($app['files']); + }); + } + + registerMiddlewareMakeCommand() { + this.$app.singleton('command.middleware.make', function($app) { + return new(require('@ostro/router/console/middlewareMakeCommand'))($app['files']); + }); + } + + registerModelMakeCommand() { + this.$app.singleton('command.model.make', function($app) { + return new(require('@ostro/foundation/console/modelMakeCommand'))($app['files']); + }); + } + + registerResourceMakeCommand() { + this.$app.singleton('command.resource.make', function($app) { + return new(require('@ostro/foundation/console/resourceMakeCommand'))($app['files']); + }); + } + + registerSessionTableCommand() { + this.$app.singleton('command.session.table', function($app) { + return new(require('@ostro/session/console/sessionTableCommand'))($app['files']); + }); + } + + registerStorageLinkCommand() { + this.$app.singleton('command.storage.link', function($app) { + return new(require('@ostro/foundation/console/storageLinkCommand'))($app['files']); + }); + } + + registerDbWipeCommand() { + this.$app.singleton('command.db.wipe', function() { + return new(require('@ostro/database/console/wipeCommand')); + }); + } + + provides() { + return Object.values(this.$commands); + } + +} + +module.exports = AssistantServiceProvider \ No newline at end of file diff --git a/providers/foundationServiceProvider.js b/providers/foundationServiceProvider.js new file mode 100644 index 0000000..e92b8af --- /dev/null +++ b/providers/foundationServiceProvider.js @@ -0,0 +1,23 @@ +const AggregateServiceProvider = require('@ostro/support/aggregateServiceProvider') +const Http = require('@ostro/http/httpContext') +const Request = require('@ostro/http/request') +class FoundationServiceProvider extends AggregateServiceProvider { + + register() { + super.register(); + + this.registerRequestValidation(); + } + + registerRequestValidation() { + let $app = this.$app + + + Request.macro('validate', function($rules = {}, $message = {}) { + return $app['validation'].validate(this.all(), $rules, $message) + }) + } + +} + +module.exports = FoundationServiceProvider \ No newline at end of file diff --git a/providers/routeServiceProvider.js b/providers/routeServiceProvider.js new file mode 100644 index 0000000..04e0a0f --- /dev/null +++ b/providers/routeServiceProvider.js @@ -0,0 +1,36 @@ +const ServiceProvider = require('@ostro/router/routeServiceProvider') +class RouteServiceProvider extends ServiceProvider { + + $namespace = ''; + + register() { + + } + + boot() { + // + } + + routes() { + + } + + setRootControllerNamespace() { + + } + + routesAreCached() { + + } + + loadCachedRoutes() { + + } + + loadRoutes() { + + } + +} + +module.exports = ServiceProvider \ No newline at end of file diff --git a/support/providers/consoleSupportServiceProvider.js b/support/providers/consoleSupportServiceProvider.js new file mode 100644 index 0000000..4f97221 --- /dev/null +++ b/support/providers/consoleSupportServiceProvider.js @@ -0,0 +1,10 @@ +const AggregateServiceProvider = require('@ostro/support/aggregateServiceProvider') +const DeferrableProvider = require('@ostro/support/deferrableProvider') +class ConsoleSupportServiceProvider extends implement(AggregateServiceProvider, DeferrableProvider) { + + $providers = [ + '@ostro/foundation/providers/assistantServiceProvider', + '@ostro/database/migrationServiceProvider' + ]; +} +module.exports = ConsoleSupportServiceProvider \ No newline at end of file diff --git a/validation/validatesRequests.js b/validation/validatesRequests.js new file mode 100644 index 0000000..da171ef --- /dev/null +++ b/validation/validatesRequests.js @@ -0,0 +1,23 @@ +class ValidatesRequests { + + validate($request, $rules, $messages = [], $customAttributes = []) { + return this.getValidationFactory().validate( + $request.all(), $rules, $messages, $customAttributes + ) + } + + validateWithBag($errorBag, $request, $rules, + $messages = [], $customAttributes = []) { + return this.validate($request, $rules, $messages, $customAttributes).catch($e => { + $e.errorBag = $errorBag; + Promise.reject($e) + }) + + } + + getValidationFactory() { + return app('validation'); + } +} + +module.exports = ValidatesRequests \ No newline at end of file diff --git a/view/helpers.js b/view/helpers.js new file mode 100644 index 0000000..d880506 --- /dev/null +++ b/view/helpers.js @@ -0,0 +1,69 @@ +const kHttp = Symbol('http') +const kApp = Symbol('app') + +class Helpers { + constructor(app, http) { + Object.defineProperty(this, kHttp, { + value: http, + enumerable: false, + configurable: false, + writable: false + }) + Object.defineProperty(this, kApp, { + value: app, + enumerable: false, + configurable: false, + writable: false + }) + + } + + secure_asset($name) { + let domain = this[kApp].config.get('app.asset_url') || ('https://' + this[kHttp].request.get('host')) + return new URL(path.join(...arguments), domain).href + } + + asset() { + let domain = this[kApp].config.get('app.asset_url') || (this[kHttp].request.protocol() + '://' + this[kHttp].request.get('host')) + return new URL(path.join(...arguments), domain).href + } + + session(key) { + return this[kHttp].request.session.get(key) + } + + get auth() { + return this[kHttp].request.auth + } + + old(key, defaultValue) { + return this[kHttp].request.old(key, defaultValue) + } + + csrfToken() { + return this[kHttp].csrfToken() + } + + csrf_token() { + return this.csrfToken() + } + + get error() { + return this[kHttp].request.error() + } + + route() { + return this[kApp]['router'].route(...arguments) + } + + mix(path) { + return this[kApp].mix.path(path) + } + + absolutePath() { + return this[kApp].config.get('app.url', '') + this[kHttp].request.path() + } + +} + +module.exports = Helpers \ No newline at end of file diff --git a/view/middleware/BindViewOnResponse.js b/view/middleware/BindViewOnResponse.js new file mode 100644 index 0000000..a378516 --- /dev/null +++ b/view/middleware/BindViewOnResponse.js @@ -0,0 +1,10 @@ +const View = require('../viewEngine') +class BindView { + + handle(http, next) { + http.response.view = new View(this.$app, http, next) + next() + + } +} +module.exports = BindView \ No newline at end of file diff --git a/view/viewEngine.js b/view/viewEngine.js new file mode 100644 index 0000000..99da699 --- /dev/null +++ b/view/viewEngine.js @@ -0,0 +1,48 @@ +const ViewException = require('./viewException') + +const ViewHelpers = require('./helpers') +const { ProxyHandler } = require('@ostro/support/macro') +class View { + constructor(app, http, next) { + let context = {}; + Object.assign(context, (app['locals'] || {})); + Object.assign(context, global) + context.request = http.request; + context.session = http.session; + context.helpers = new ViewHelpers(app, http); + const fn = function(file, data = {}, status = 200) { + + fn.render(...arguments) + } + fn.engine = function(engine) { + Object.defineProperty(http.response, '__viewEngine', { + value: engine, + configurable: false, + writable: false, + enumerable: false + }) + return this + + } + + fn.render = function(file, data = {}, status = 200) { + Object.assign(data, context) + + app.view.engine(http.response.__viewEngine).renderFile(file, data, async (data) => { + if (typeof data == 'object' && data instanceof Promise == false) { + return next(new ViewException(await data)) + } + try { + http.response.send(await data, status) + } catch (e) { + next(e) + } + + }) + + } + return fn + } +} + +module.exports = View \ No newline at end of file diff --git a/view/viewException.js b/view/viewException.js new file mode 100644 index 0000000..a451715 --- /dev/null +++ b/view/viewException.js @@ -0,0 +1,12 @@ +const ViewExceptionContract = require('@ostro/contracts/view/viewException') +class ViewException extends ViewExceptionContract { + constructor(errors) { + super(); + this.name = this.constructor.name; + this.message = errors; + this.statusCode = 500; + Error.captureStackTrace(this, this.constructor); + + } +} +module.exports = ViewException \ No newline at end of file