├── tests ├── app-folders │ ├── hooks1 │ │ ├── public.js │ │ ├── io_exit.js │ │ ├── io_init.js │ │ ├── item.js │ │ ├── _out.js │ │ ├── ignored.js │ │ ├── io_proto.js │ │ ├── ctrl_proto.js │ │ └── ctrl_hooks.js │ ├── hooks2 │ │ ├── public.js │ │ ├── _in.js │ │ ├── _out.js │ │ ├── views │ │ │ └── .gitkeep │ │ ├── A │ │ │ ├── _post_sub.js │ │ │ ├── _pre_sub.js │ │ │ ├── _post_method.js │ │ │ └── _pre_method.js │ │ └── index.js │ ├── Method │ │ ├── A │ │ │ ├── B │ │ │ │ ├── a-method │ │ │ │ │ ├── _METHOD │ │ │ │ │ └── index.js │ │ │ │ ├── c1.js │ │ │ │ ├── _post-method.js │ │ │ │ └── _pre-method.js │ │ │ ├── b1.js │ │ │ ├── _pre-method.js │ │ │ └── _post-method.js │ │ ├── a1.js │ │ ├── _post_method.js │ │ └── _pre_method.js │ ├── creation-errors │ │ ├── ignore-not-string │ │ │ ├── www │ │ │ │ └── .gitkeep │ │ │ └── www_hooks │ │ │ │ └── ignore.js │ │ ├── no-params-io-init │ │ │ ├── www │ │ │ │ └── .gitkeep │ │ │ └── www_hooks │ │ │ │ └── io_init.js │ │ ├── app-hook-folder-no-index │ │ │ ├── www │ │ │ │ └── .gitkeep │ │ │ └── www_hooks │ │ │ │ └── empty-folder │ │ │ │ └── .gitkeep │ │ ├── ignore-item-not-string │ │ │ ├── www │ │ │ │ └── .gitkeep │ │ │ └── www_hooks │ │ │ │ └── ignore.js │ │ ├── io-exit-must-be-function │ │ │ ├── www │ │ │ │ └── .gitkeep │ │ │ └── www_hooks │ │ │ │ └── io_exit.js │ │ ├── io-init-must-be-function │ │ │ ├── www │ │ │ │ └── .gitkeep │ │ │ └── www_hooks │ │ │ │ └── io_init.js │ │ ├── shared-ctrl-not-folder │ │ │ ├── www │ │ │ │ └── .gitkeep │ │ │ └── www_hooks │ │ │ │ └── shared_ctrls.js │ │ ├── method-no-params │ │ │ └── www │ │ │ │ └── no-params.js │ │ └── file-folder-same-name │ │ │ └── www │ │ │ ├── same-name.js │ │ │ └── same-name │ │ │ └── index.js │ ├── hooks1_hooks │ │ ├── item.js │ │ ├── ignore.js │ │ ├── ctrl_proto.js │ │ ├── io_exit.js │ │ ├── io_proto.js │ │ ├── ctrl_hooks.js │ │ ├── io_init.js │ │ └── shared_methods.js │ ├── hooks2_hooks │ │ ├── ignore │ │ │ └── index.js │ │ ├── stuff │ │ │ └── index.js │ │ ├── ctrl_proto │ │ │ ├── qwe.js │ │ │ └── asd │ │ │ │ └── index.js │ │ ├── io_proto │ │ │ ├── qwe.js │ │ │ └── asd │ │ │ │ └── index.js │ │ ├── ctrl_hooks │ │ │ ├── public.js │ │ │ └── views │ │ │ │ └── index.js │ │ ├── shared_methods │ │ │ ├── m1.js │ │ │ └── m2 │ │ │ │ └── index.js │ │ ├── shared_ctrls │ │ │ └── x │ │ │ │ ├── _in.js │ │ │ │ ├── _out.js │ │ │ │ └── index.js │ │ ├── io_exit │ │ │ └── index.js │ │ └── io_init │ │ │ └── index.js │ ├── www │ │ ├── _get.js │ │ ├── _in.js │ │ ├── _out.js │ │ ├── A │ │ │ ├── B │ │ │ │ ├── _get.js │ │ │ │ ├── _in.js │ │ │ │ ├── _out.js │ │ │ │ ├── zxc.js │ │ │ │ ├── index.js │ │ │ │ ├── _after_verb.js │ │ │ │ ├── _post_method.js │ │ │ │ └── _pre_method.js │ │ │ ├── _get.js │ │ │ ├── _in.js │ │ │ ├── _out.js │ │ │ ├── index.js │ │ │ ├── _after_verb.js │ │ │ ├── _no_verb.js │ │ │ ├── _post_sub.js │ │ │ ├── _pre_sub.js │ │ │ ├── _post_method.js │ │ │ ├── _pre_method.js │ │ │ └── asd.js │ │ ├── qwe.js │ │ ├── _after_verb.js │ │ ├── _post_method.js │ │ ├── _post_sub.js │ │ ├── _pre_method.js │ │ ├── _pre_sub.js │ │ └── index.js │ ├── Parent │ │ ├── A │ │ │ ├── B │ │ │ │ ├── c1.js │ │ │ │ ├── C │ │ │ │ │ └── d1.js │ │ │ │ ├── _pre-sub.js │ │ │ │ └── _post-sub.js │ │ │ ├── b1.js │ │ │ ├── _post-sub.js │ │ │ └── _pre-sub.js │ │ ├── _pre_sub.js │ │ └── _post_sub.js │ ├── Target │ │ ├── _get.js │ │ ├── A │ │ │ ├── B │ │ │ │ ├── _put.js │ │ │ │ ├── C │ │ │ │ │ ├── D │ │ │ │ │ │ ├── E │ │ │ │ │ │ │ ├── _put.js │ │ │ │ │ │ │ ├── _after-verb.js │ │ │ │ │ │ │ └── _verbs │ │ │ │ │ │ │ │ ├── post.js │ │ │ │ │ │ │ │ ├── get │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ │ └── put.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── _after-verb.js │ │ │ │ │ │ └── _verbs.js │ │ │ │ │ ├── _delete.js │ │ │ │ │ ├── _no-verb.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── _after-verb.js │ │ │ │ ├── _no_verb.js │ │ │ │ ├── _after_verb.js │ │ │ │ └── _before_verb.js │ │ │ ├── _post.js │ │ │ ├── _after-verb.js │ │ │ ├── _before-verb.js │ │ │ └── _no-verb.js │ │ ├── _after-verb.js │ │ └── _before-verb.js │ ├── Wrappers │ │ ├── _in.js │ │ ├── _out.js │ │ └── A │ │ │ ├── B │ │ │ ├── _in.js │ │ │ └── _out.js │ │ │ ├── _in.js │ │ │ └── _out.js │ └── Params │ │ ├── _out.js │ │ ├── A │ │ ├── _in.js │ │ ├── _out.js │ │ └── B │ │ │ ├── _in.js │ │ │ └── _out.js │ │ └── _in.js ├── bootstruct.spec.js ├── specs │ ├── io-params.spec.js │ ├── wrappers.spec.js │ ├── parent-chain.spec.js │ ├── method-chain.spec.js │ ├── hooks-2.spec.js │ ├── hooks-1.spec.js │ ├── full-use-case.spec.js │ ├── creation-errors.spec.js │ └── target-chain.spec.js └── make-request.js ├── .gitignore ├── .travis.yml ├── .vscode ├── settings.json └── launch.json ├── src ├── utils │ ├── is-array.js │ ├── is-string.js │ ├── is-function.js │ ├── is-empty.js │ ├── has-own-prop.js │ ├── for-in.js │ ├── get-duplicated-keys.js │ ├── remove-extension.js │ ├── normalize-entry-name.js │ ├── index.js │ ├── log.js │ ├── should-be-ignored.js │ ├── f2j.js │ └── try-require.js ├── constants.js ├── ctrl │ ├── private-methods │ │ ├── get-lowered-first-param.js │ │ ├── register-state.js │ │ ├── init-sub-ctrls.js │ │ ├── delegate-no-verb.js │ │ ├── remove-self-name.js │ │ ├── get-next-fn.js │ │ ├── get-chain-type.js │ │ ├── has.js │ │ ├── set-id.js │ │ ├── setup-chains.js │ │ ├── parse-folder-map.js │ │ └── run.js │ ├── index.js │ └── hooks.js ├── io.js ├── app │ ├── index.js │ └── hooks.js ├── debug.js └── errors.js ├── playground ├── www │ ├── a │ │ ├── index.js │ │ └── _private.js │ └── index.js └── play.js ├── .npmignore ├── Docs ├── controller-flowchart.png ├── Controller Hooks │ ├── after_verb.md │ ├── pre & post method.md │ ├── no_verb.md │ ├── index.md │ ├── pre & post sub.md │ ├── verbs.md │ ├── in & out.md │ ├── use-case.md │ └── get post put delete.md ├── App Hooks │ ├── shared_ctrls.md │ ├── io_exit.md │ ├── ignore.md │ ├── shared_methods.md │ ├── ctrl_proto.md │ ├── io_proto.md │ ├── io_init.md │ └── ctrl_hooks.md ├── Controller Hooks.md ├── Get Started.md ├── Extending Bootstruct.md └── README.md ├── index.js ├── .editorconfig ├── package.json ├── LICENSE ├── README.md └── .eslintrc.js /tests/app-folders/hooks1/public.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/public.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/B/a-method/_METHOD: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/ignore-not-string/www/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/no-params-io-init/www/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/item.js: -------------------------------------------------------------------------------- 1 | module.exports = 'item'; 2 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/app-hook-folder-no-index/www/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/ignore-item-not-string/www/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/io-exit-must-be-function/www/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/io-init-must-be-function/www/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/shared-ctrl-not-folder/www/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/ignore.js: -------------------------------------------------------------------------------- 1 | module.exports = 'ignored'; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.panel.defaultLocation": "right" 3 | } -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/ignore/index.js: -------------------------------------------------------------------------------- 1 | module.exports = ['ignored']; 2 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/app-hook-folder-no-index/www_hooks/empty-folder/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/ignore-not-string/www_hooks/ignore.js: -------------------------------------------------------------------------------- 1 | module.exports = 1; 2 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/stuff/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | item: 'item', 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/is-array.js: -------------------------------------------------------------------------------- 1 | module.exports = function isArray (ary) { 2 | return Array.isArray(ary); 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/ignore-item-not-string/www_hooks/ignore.js: -------------------------------------------------------------------------------- 1 | module.exports = ['a', 3]; 2 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/ctrl_proto.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | qwe: () => 'ctrl_proto', 3 | }; 4 | -------------------------------------------------------------------------------- /playground/www/a/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.end('/a'); 3 | io.next() 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/is-string.js: -------------------------------------------------------------------------------- 1 | module.exports = function isString (str) { 2 | return typeof str === 'string'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/ctrl_proto/qwe.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return 'c_pro1'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/io_proto/qwe.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return 'io_pro1'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/www/_get.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('g'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('f'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('l'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /playground/ 3 | /Docs/ 4 | /tests/ 5 | npm-debug.log 6 | .travis.yml 7 | .vscode 8 | -------------------------------------------------------------------------------- /Docs/controller-flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taitulism/Bootstruct/HEAD/Docs/controller-flowchart.png -------------------------------------------------------------------------------- /src/utils/is-function.js: -------------------------------------------------------------------------------- 1 | module.exports = function isFunction (fn) { 2 | return typeof fn === 'function'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/B/c1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('c'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/b1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('b'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/a1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('a'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/A/B/c1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('c'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/A/b1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('b'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/_get.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('get'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Wrappers/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('f'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Wrappers/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('l'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/io-exit-must-be-function/www_hooks/io_exit.js: -------------------------------------------------------------------------------- 1 | module.exports = 'not-a-function'; 2 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/io-init-must-be-function/www_hooks/io_init.js: -------------------------------------------------------------------------------- 1 | module.exports = 'not-a-function'; 2 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/ctrl_proto/asd/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return 'c_pro2'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/io_proto/asd/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return 'io_pro2'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/_get.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('g2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('f2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('l2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/zxc.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('zxc'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_get.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('g1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('f1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('l1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('i1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/qwe.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('qwe'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/A/B/C/d1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('d'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/_pre_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pre'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/_put.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('put'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/_post.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Wrappers/A/B/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('f2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Wrappers/A/B/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('l2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Wrappers/A/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('f1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Wrappers/A/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('l1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/io_exit.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.exited = 'io_exit'; 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/io_exit.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | this.res.end(this.exited || ''); 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/io_proto.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | qwe () { 3 | return 'io_proto'; 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('['); 3 | 4 | io.next(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write(']'); 3 | 4 | io.next(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/views/.gitkeep: -------------------------------------------------------------------------------- 1 | git ignores empty folders so in order to track "views" folder it must have content. -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/ctrl_hooks/public.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | this.public = 'pub'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('i2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_after_verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('a1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_no_verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('nv'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_post_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pts1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_pre_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('prs1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/_after_verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('a'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/_post_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ptm'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/_post_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pts'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/_pre_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('prm'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/_pre_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('prs'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/_pre-method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pre'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/_post_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/_pre_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pre'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/A/B/_pre-sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pre2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/A/_post-sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/A/_pre-sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pre1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/_post_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/E/_put.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('put'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('b44'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/_delete.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('del'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/_no-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('nvc'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('b43'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/_no_verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('nvb'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/_after-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ftr1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/_before-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('b41'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/_no-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('nva'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/_after-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ftr'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/_before-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('b4'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/io_init.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write(io.initiated); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/A/_post_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('}'); 3 | 4 | io.next(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/A/_pre_sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('{'); 3 | 4 | io.next(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/ctrl_hooks/views/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | this.views = 'view'; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/_after_verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('a2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/_post_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ptm2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/B/_pre_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('prm2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_post_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ptm1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/_pre_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('prm1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /playground/www/a/_private.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | console.log('no no no!'); 3 | io.res.end('NO NO NO'); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/B/_post-method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/B/_pre-method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('pre'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/_post-method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Parent/A/B/_post-sub.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/_after-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ftr3'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/_after_verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ftr2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/_before_verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('b42'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/item.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write(this.app.item); 3 | 4 | io.next(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/ctrl_hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | public () { 3 | this.public = 'public'; 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/A/_post_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('}'); 3 | 4 | io.next(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/A/_pre_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('{'); 3 | 4 | io.next(); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/E/_after-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ftr5'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/E/_verbs/post.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('post'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/_after-verb.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('ftr4'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/method-no-params/www/no-params.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = function () {}; 4 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/E/_verbs/get/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('get'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/E/_verbs/put.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('overriden'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/file-folder-same-name/www/same-name.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = function (io) {}; 4 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/no-params-io-init/www_hooks/io_init.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = function () {}; 4 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/io_init.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app) { 2 | this.initiated = 'io_init'; 3 | app.checkIn(this); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/shared_methods/m1.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('shared_m1'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /src/utils/is-empty.js: -------------------------------------------------------------------------------- 1 | module.exports = function isEmpty (obj) { 2 | const keys = Object.keys(obj); 3 | 4 | return keys.length === 0; 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/file-folder-same-name/www/same-name/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = function (io) {}; 4 | -------------------------------------------------------------------------------- /tests/app-folders/creation-errors/shared-ctrl-not-folder/www_hooks/shared_ctrls.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = function () {}; 4 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/shared_ctrls/x/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | 3 | io.res.write('X1'); 4 | 5 | io.next(); 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/shared_ctrls/x/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | 3 | io.res.write('X3'); 4 | 5 | io.next(); 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/shared_methods/m2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('shared_m2'); 3 | io.next(); 4 | }; 5 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/shared_ctrls/x/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | 3 | io.res.write('X2'); 4 | 5 | io.next(); 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/Params/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | const params = io.params.join(','); 3 | 4 | io.res.write(params); 5 | io.next(); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/app-folders/Params/A/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | const params = io.params.join(','); 3 | 4 | io.res.write(`${params}|`); 5 | io.next(); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/app-folders/Params/A/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | const params = io.params.join(','); 3 | 4 | io.res.write(`${params}|`); 5 | io.next(); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/app-folders/Params/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | const params = io.params.join(','); 3 | 4 | io.res.write(`${params}|`); 5 | io.next(); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | if (io.ignore_failed) { 3 | io.res.write('IGNORE_FAIL!'); 4 | } 5 | 6 | io.next(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/ignored.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | module.exports = function (io) { 4 | io.ignore_failed = true; 5 | 6 | io.next(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/Params/A/B/_in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | const params = io.params.join(','); 3 | 4 | io.res.write(`${params}|`); 5 | io.next(); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/app-folders/Params/A/B/_out.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | const params = io.params.join(','); 3 | 4 | io.res.write(`${params}|`); 5 | io.next(); 6 | }; 7 | -------------------------------------------------------------------------------- /tests/app-folders/www/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('i'); 3 | 4 | if (io.params[0] === 'bla') io.res.write('bla'); 5 | 6 | io.next(); 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/has-own-prop.js: -------------------------------------------------------------------------------- 1 | const {hasOwnProperty} = Object.prototype; 2 | 3 | module.exports = function hasOwnProp (obj, prop) { 4 | return hasOwnProperty.call(obj, prop); 5 | }; 6 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/io_proto.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | if (typeof io.qwe === 'function') { 3 | io.res.write(io.qwe()); 4 | } 5 | 6 | io.res.end(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/Method/A/B/a-method/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | if (this.methods['a-method']) { 3 | io.res.write('method'); 4 | } 5 | 6 | io.next(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/ctrl_proto.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | if (typeof this.qwe === 'function') { 3 | io.res.write(this.qwe()); 4 | } 5 | 6 | io.res.end(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/io_exit/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | if (this.req.url === '/') { 3 | this.res.write('exit'); 4 | } 5 | 6 | this.res.end(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1/ctrl_hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | if (typeof this.public === 'string') { 3 | io.res.write(this.public); 4 | } 5 | 6 | io.res.end(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks1_hooks/shared_methods.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | module.exports = { 4 | a_shared_method (io) { 5 | io.res.end('shared_methods'); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2_hooks/io_init/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app) { 2 | this.initiated = 'io_init'; 3 | 4 | setTimeout(() => { 5 | app.checkIn(this); 6 | }, 0); 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | FILE: 1, 3 | FOLDER: 0, 4 | 5 | ROOT_CTRL_NAME: '$ROOT_CTRL', 6 | 7 | PARENT_CHAIN_INDEX: 0, 8 | TARGET_CHAIN_INDEX: 1, 9 | METHOD_CHAIN_INDEX: 2, 10 | }; 11 | -------------------------------------------------------------------------------- /tests/app-folders/Target/A/B/C/D/_verbs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | GET (io) { 3 | io.res.write('get'); 4 | io.next(); 5 | }, 6 | 7 | post (io) { 8 | io.res.write('post'); 9 | io.next(); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/get-lowered-first-param.js: -------------------------------------------------------------------------------- 1 | module.exports = function getLoweredFirstParam (io) { 2 | const [firstParam] = io.params; 3 | 4 | if (!firstParam) return null; 5 | 6 | return firstParam.toLowerCase(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/app-folders/www/A/asd.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | io.res.write('asd'); 3 | 4 | if (io.params[0] === 'bla' && io.params[1] === 'blu') { 5 | io.res.write('blablu'); 6 | } 7 | 8 | io.next(); 9 | }; 10 | -------------------------------------------------------------------------------- /playground/www/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | module.exports = function (io) { 3 | // io.res.setHeader('qwe', '666'); 4 | // io.res.writeHead(201); 5 | // io.res.write('written'); 6 | // io.res.end('hello world'); 7 | io.next(); 8 | }; 9 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/register-state.js: -------------------------------------------------------------------------------- 1 | const getChainType = require('./get-chain-type'); 2 | 3 | module.exports = function registerState (ctrl, io) { 4 | io.states[ctrl.id] = { 5 | index: 0, 6 | chainType: getChainType(ctrl, io), 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/for-in.js: -------------------------------------------------------------------------------- 1 | const hasOwnProp = require('./has-own-prop'); 2 | 3 | module.exports = function forIn (obj, fn) { 4 | for (const key in obj) { 5 | if (hasOwnProp(obj, key)) { 6 | fn.call(obj, key, obj[key]); 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /playground/play.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const http = require('http'); 4 | 5 | const bts = require('../'); 6 | const app = bts(__dirname + '/www'); 7 | 8 | http.createServer(app).listen(8080, () => { 9 | console.log('Listening on port 8080'); 10 | }); 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const App = require('./src/app'); 2 | const log = require('./src/utils/log'); 3 | 4 | module.exports = function bootstrcut (webRoot, debug) { 5 | const app = new App(webRoot, debug); 6 | 7 | if (debug) { 8 | log('Bootstruct - debugging mode started'); 9 | } 10 | 11 | return app.requestHandler; 12 | }; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | indent_style = space 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /src/utils/get-duplicated-keys.js: -------------------------------------------------------------------------------- 1 | module.exports = function getDuplicateKeys (objA, objB) { 2 | const AKeys = Object.keys(objA); 3 | const BKeys = new Set(Object.keys(objB)); 4 | const dups = []; 5 | 6 | AKeys.forEach((key) => { 7 | if (BKeys.has(key)) { 8 | dups.push(key); 9 | } 10 | }); 11 | 12 | return dups; 13 | }; 14 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/init-sub-ctrls.js: -------------------------------------------------------------------------------- 1 | const forIn = require('../../utils/for-in'); 2 | 3 | module.exports = function initSubCtrl (ctrl) { 4 | const {subCtrls} = ctrl; 5 | 6 | forIn(subCtrls, (name, subCtrlMap) => { 7 | // this ctrl is passed as a parent 8 | subCtrls[name] = new ctrl.constructor(name, subCtrlMap, ctrl); 9 | }); 10 | 11 | return subCtrls; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/remove-extension.js: -------------------------------------------------------------------------------- 1 | /* 2 | ┌───────────────────────── 3 | │ removes file extension 4 | */ 5 | module.exports = function removeExtension (name) { 6 | const lastDot = name.lastIndexOf('.'); 7 | 8 | // at least 1 char before dot 9 | // nor a folder or a .dotFile 10 | if (lastDot >= 1) { 11 | name = name.substr(0, lastDot); 12 | } 13 | 14 | return name; 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/normalize-entry-name.js: -------------------------------------------------------------------------------- 1 | const removeExt = require('./remove-extension'); 2 | 3 | /* 4 | ┌──────────────────────────────────────────────── 5 | │ returns lowerCased extension-less entryname. 6 | */ 7 | module.exports = function normalizeEntryName (entryName, isFile = true) { 8 | const name = entryName.toLowerCase(); 9 | 10 | return isFile 11 | ? removeExt(name) 12 | : name; 13 | }; 14 | -------------------------------------------------------------------------------- /tests/bootstruct.spec.js: -------------------------------------------------------------------------------- 1 | describe('Bootstruct', () => { 2 | require('./specs/creation-errors.spec'); 3 | require('./specs/wrappers.spec'); 4 | require('./specs/method-chain.spec'); 5 | require('./specs/parent-chain.spec'); 6 | require('./specs/target-chain.spec'); 7 | require('./specs/io-params.spec'); 8 | require('./specs/full-use-case.spec'); 9 | require('./specs/hooks-1.spec'); 10 | require('./specs/hooks-2.spec'); 11 | }); 12 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/delegate-no-verb.js: -------------------------------------------------------------------------------- 1 | /* 2 | ┌────────────────────────────────────────────────────────────── 3 | │ "delegate" means that when "noVerb" found for a ctrl, all of 4 | │ its sub-ctrls will get it too (inherit behavior) 5 | │ unless they have a "noVerb" of their own. 6 | */ 7 | module.exports = function delegateNoVerb (ctrl) { 8 | if (!ctrl.noVerb && ctrl.parent) { 9 | ctrl.noVerb = ctrl.parent.noVerb; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | f2j: require('./f2j'), 3 | forIn: require('./for-in'), 4 | isEmpty: require('./is-empty'), 5 | isArray: require('./is-array'), 6 | isString: require('./is-string'), 7 | isFunction: require('./is-function'), 8 | removeExtension: require('./remove-extension'), 9 | shouldBeIgnored: require('./should-be-ignored'), 10 | getDuplicatedKeys: require('./get-duplicated-keys'), 11 | normalizeEntryName: require('./normalize-entry-name'), 12 | }; 13 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/remove-self-name.js: -------------------------------------------------------------------------------- 1 | const getLoweredFirstParam = require('./get-lowered-first-param'); 2 | 3 | /* 4 | ┌────────────────────────────────────── 5 | │ example: 6 | │ url: /A/B/C 7 | │ on "RC" check-in: 8 | │ io.params = ['A','B','C'] 9 | │ 10 | │ on "A" ctrl check-in: 11 | │ io.params = ['B','C'] 12 | */ 13 | module.exports = function removeSelfName (ctrl, io) { 14 | const first = getLoweredFirstParam(io); 15 | 16 | if (first && first === ctrl.name) { 17 | // remove first item 18 | io.params.shift(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/after_verb.md: -------------------------------------------------------------------------------- 1 | _after_verb 2 | ----------- 3 | **Chain**: Target. 4 | **Alias**: `_after-verb` 5 | 6 | `_after_verb`, like `_before_verb` (`index` alias), will run for **any** request type in the **target**-controller but **after** the \ method. 7 | ``` 8 | ├── api 9 | │ ├── _before_verb.js 10 | │ ├── _get.js 11 | │ ├── _post.js 12 | │ └── _after_verb.js 13 | ``` 14 | ``` 15 | request: GET `/` 16 | logs: 17 | api/_before_verb 18 | api/_get 19 | api/_after_verb 20 | 21 | request: POST `/` 22 | logs: 23 | api/_before_verb 24 | api/_post 25 | api/_after_verb 26 | ``` 27 | -------------------------------------------------------------------------------- /tests/specs/io-params.spec.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {expect} = require('chai'); 3 | 4 | const bts = require('../../'); 5 | const makeRequest = require('../make-request'); 6 | 7 | describe('io.params test', function () { 8 | const app = bts('./tests/app-folders/Params'); 9 | const server = http.createServer(app); 10 | 11 | 12 | beforeEach(function () { 13 | server.listen(8181, '127.0.0.1'); 14 | }); 15 | afterEach(function () { 16 | server.close(); 17 | }); 18 | 19 | it('should pass', function (done) { 20 | makeRequest('GET', '/a/b/c/d/e', (body) => { 21 | expect(body).to.equal('a,b,c,d,e|b,c,d,e|c,d,e|c,d,e|c,d,e|c,d,e'); 22 | done(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/make-request.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable 2 | no-console, 3 | max-params, 4 | newline-before-return, 5 | */ 6 | const http = require('http'); 7 | 8 | module.exports = makeRequest; 9 | 10 | const options = { 11 | hostname: '127.0.0.1', 12 | port: 8181, 13 | }; 14 | 15 | function makeRequest (verb, path, callback) { 16 | options.path = path; 17 | options.method = verb.toUpperCase(); 18 | 19 | const req = http.request(options, (response) => { 20 | let body = ''; 21 | 22 | response 23 | .setEncoding('utf8') 24 | .on('data', (chunk) => { 25 | body += chunk; 26 | }) 27 | .on('end', () => { 28 | callback(body); 29 | }); 30 | }); 31 | 32 | req.on('error', (err) => { 33 | console.log('makeRequest ERROR:\n', err); 34 | }); 35 | 36 | req.end(); 37 | } 38 | -------------------------------------------------------------------------------- /tests/app-folders/hooks2/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable multiline-ternary */ 2 | module.exports = function (io) { 3 | let body = ''; 4 | 5 | // io_init 6 | body += (io.initiated) ? `${io.initiated}-` : 'IO_INIT_FAIL-'; 7 | 8 | // ctrl_proto 9 | body += (this.qwe) ? `${this.qwe()}-` : 'CTRL_PROTO_FAIL_1-'; 10 | body += (this.asd) ? `${this.asd()}-` : 'CTRL_PROTO_FAIL_2-'; 11 | 12 | // io_proto 13 | body += (io.qwe) ? `${io.qwe()}-` : 'IO_PROTO_FAIL_1-'; 14 | body += (io.asd) ? `${io.asd()}-` : 'IO_PROTO_FAIL_2-'; 15 | 16 | // ctrl_hooks 17 | body += (this.public) ? `${this.public}-` : 'ENTRY_HANDLER_FAIL_1'; 18 | body += (this.views) ? `${this.views}-` : 'ENTRY_HANDLER_FAIL_2'; 19 | 20 | body += (this.app.stuff) ? this.app.stuff.item : 'DEFAULT_HANDLER_FAIL'; 21 | 22 | io.res.write(body); 23 | 24 | io.next(); 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/log.js: -------------------------------------------------------------------------------- 1 | const {isString, isArray} = require('./'); 2 | 3 | module.exports = log; 4 | 5 | // eslint-disable-next-line no-console 6 | const clog = console.log; 7 | 8 | function log (title, content) { 9 | if (!title) { 10 | // log empty line 11 | clog(); 12 | 13 | return; 14 | } 15 | 16 | const titleIsString = isString(title); 17 | 18 | if (titleIsString && !content) { 19 | // log a simple line 20 | clog(title); 21 | // maybe instead, use "log()" for recuresion arrays 22 | 23 | return; 24 | } 25 | 26 | if (isString(content)) { 27 | // log a title and an indented line 28 | clog(title); 29 | clog(` ${content}`); 30 | 31 | return; 32 | } 33 | 34 | if (titleIsString && isArray(content)) { 35 | clog(title); 36 | content.forEach((line) => { 37 | if (isString(line)) { 38 | clog(` ${line}`); 39 | } 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/get-next-fn.js: -------------------------------------------------------------------------------- 1 | /* 2 | ┌────────────────────────────────────────────────────────────────────────── 3 | │ ctrls has 3 chains of methods and uses only one of them per request. 4 | │ 5 | │ io holds ctrl profiles with the index which represents the progress in 6 | │ each chain. 7 | │ 8 | │ by calling io.next() you increment that index. 9 | | 0 = PARENT (sub-ctrl) 10 | | 1 = TARGET 11 | | 2 = METHOD 12 | */ 13 | module.exports = function getNextFn (ctrl, io, incrementIndex = true) { 14 | // get ctrl profile 15 | const ctrlProfile = io.states[ctrl.id]; 16 | 17 | // don't increment chain index when debugging 18 | const i = incrementIndex 19 | ? ctrlProfile.index++ 20 | : ctrlProfile.index; 21 | 22 | const chainIndex = ctrlProfile.chainType; 23 | 24 | // get corresponding chain 25 | const chain = ctrl.chains[chainIndex]; 26 | 27 | return chain[i]; 28 | }; 29 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/get-chain-type.js: -------------------------------------------------------------------------------- 1 | const getLoweredFirstParam = require('./get-lowered-first-param'); 2 | 3 | const { 4 | PARENT_CHAIN_INDEX, 5 | TARGET_CHAIN_INDEX, 6 | METHOD_CHAIN_INDEX, 7 | } = require('../../constants'); 8 | 9 | 10 | /* 11 | ┌──────────────────────────────────────────────────────────────────────────── 12 | | test on checkIn, if this ctrl is the target ctrl for the current request. 13 | | checks if the next param is an existing sub 14 | | 15 | | returns 16 | | 0 (parent) 17 | | 1 (target) 18 | | 2 (method) 19 | */ 20 | module.exports = function getChainType (ctrl, io) { 21 | const next = getLoweredFirstParam(io); 22 | 23 | if (next) { 24 | if (ctrl.methods[next]) { 25 | return METHOD_CHAIN_INDEX; 26 | } 27 | else if (ctrl.subCtrls[next]) { 28 | return PARENT_CHAIN_INDEX; 29 | } 30 | 31 | return TARGET_CHAIN_INDEX; 32 | } 33 | 34 | return TARGET_CHAIN_INDEX; 35 | }; 36 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/has.js: -------------------------------------------------------------------------------- 1 | const isEmpty = require('../../utils/is-empty'); 2 | 3 | /* 4 | ┌────────────────────────────────────────────────────────────── 5 | │ "delegate" means that when "noVerb" found for a ctrl, all of 6 | │ its sub-ctrls will get it too (inherit behavior) 7 | │ unless they have a "noVerb" of their own. 8 | */ 9 | module.exports = { 10 | verbs (ctrl) { 11 | return !isEmpty(ctrl.verbs); 12 | }, 13 | 14 | methods (ctrl) { 15 | let sharedMethods; 16 | 17 | const ownMethods = !isEmpty(ctrl.methods); 18 | 19 | if (ctrl.app.shared_methods) { 20 | sharedMethods = !isEmpty(ctrl.app.shared_methods); 21 | } 22 | 23 | return ownMethods || sharedMethods; 24 | }, 25 | 26 | subCtrls (ctrl) { 27 | let sharedCtrls; 28 | 29 | const hasOwnCtrls = !isEmpty(ctrl.subCtrls); 30 | 31 | if (ctrl.app.shared_ctrls) { 32 | sharedCtrls = !isEmpty(ctrl.app.shared_ctrls); 33 | } 34 | 35 | return hasOwnCtrls || sharedCtrls; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /tests/specs/wrappers.spec.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {expect} = require('chai'); 3 | 4 | const bts = require('../../'); 5 | const makeRequest = require('../make-request'); 6 | 7 | describe('Wrappers test', function () { 8 | const app = bts('./tests/app-folders/Wrappers'); 9 | const server = http.createServer(app); 10 | 11 | 12 | beforeEach(function () { 13 | server.listen(8181, '127.0.0.1'); 14 | }); 15 | afterEach(function () { 16 | server.close(); 17 | }); 18 | 19 | 20 | it('should pass', function (done) { 21 | makeRequest('GET', '/', (body) => { 22 | expect(body).to.equal('fl'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should pass', function (done) { 28 | makeRequest('GET', '/a', (body) => { 29 | expect(body).to.equal('ff1l1l'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should pass', function (done) { 35 | makeRequest('GET', '/a/b', (body) => { 36 | expect(body).to.equal('ff1f2l2l1l'); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstruct", 3 | "version": "1.7.0", 4 | "description": "Routing by structure. A name-convention web framework for Node.js.", 5 | "main": "index.js", 6 | "author": "Taitu Lizenbaum taitu.dev@gmail.com", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/taitulism/Bootstruct" 10 | }, 11 | "scripts": { 12 | "dev": "npm test -- --watch --reporter nyan", 13 | "test": "mocha --inspect ./tests/bootstruct.spec.js", 14 | "lint": "eslint ./index.js ./src ./tests", 15 | "check": "npm run lint && npm test", 16 | "play": "node ./playground/play.js", 17 | "preversion": "npm run check", 18 | "postversion": "git push && git push --tags" 19 | }, 20 | "keywords": [ 21 | "bootstruct", 22 | "route", 23 | "routing", 24 | "structure", 25 | "folder", 26 | "app", 27 | "restful", 28 | "framework" 29 | ], 30 | "license": "MIT", 31 | "devDependencies": { 32 | "chai": "^4.2.0", 33 | "eslint": "^5.16.0", 34 | "mocha": "^5.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/set-id.js: -------------------------------------------------------------------------------- 1 | /* 2 | ┌──────────────────────────────────────────────── 3 | │ ctrl.id is composed using it's parent.id: 4 | │ ctrl.id = "parent.id + / + own name" 5 | │ id is unique, name isn't. 6 | │ examples: 7 | │ id: '/A' │ '/A/B' │ '/A/B/A' 8 | │ name: 'A' │ 'B' │ 'A' 9 | */ 10 | module.exports = function setId (ctrl) { 11 | if (ctrl.isRootCtrl) return '/'; 12 | 13 | const {parent} = ctrl; 14 | 15 | let tempId = `/${ctrl.name}`; 16 | 17 | /* 18 | │ RC's ID is slash: '/'. 19 | │ to avoid double starting slashes for RC's direct sub-ctrls like: 20 | │ ctrl.id = '//subCtrl' 21 | │ the id will only start with the parent.id if current ctrl is 22 | │ not RC nor RC's direct sub-ctrl 23 | */ 24 | const isNotRC = !ctrl.isRootCtrl; 25 | const hasParent = Boolean(parent); 26 | const parentIsNotRC = hasParent && !parent.isRootCtrl; 27 | 28 | if (isNotRC && parentIsNotRC) { 29 | tempId = parent.id + tempId; 30 | } 31 | 32 | return tempId; 33 | }; 34 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/pre & post method.md: -------------------------------------------------------------------------------- 1 | _pre_method & _post_method 2 | -------------------------- 3 | **Chain**: Method. 4 | **Aliass**: `_pre_method`, `_post_method` (respectively). 5 | 6 | These reserved methods will run before and after the target-method (pre=before, post=after). 7 | ``` 8 | ├── api 9 | │ ├── _in.js 10 | │ ├── index.js 11 | │ ├── _pre_method.js <── runs before methods 12 | │ ├── A.js <── user file = method 13 | │ ├── B.js <── user file = method 14 | │ ├── _post_method.js <── runs after methods 15 | │ └── _out.js 16 | ``` 17 | 18 | Assume all files log their names and calling `io.next()`: 19 | ``` 20 | request: / 21 | logs: 22 | api/_in 23 | api/index 24 | api/_out 25 | (no method was called) 26 | 27 | request: /a 28 | logs: 29 | api/_in 30 | api/_pre_method <── 31 | api/A.js 32 | api/_post_method <── 33 | api/_out 34 | 35 | request: /b 36 | logs: 37 | api/_in 38 | api/_pre_method <── 39 | api/B.js 40 | api/_post_method <── 41 | api/_out 42 | ``` 43 | -------------------------------------------------------------------------------- /tests/specs/parent-chain.spec.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {expect} = require('chai'); 3 | 4 | const bts = require('../../'); 5 | const makeRequest = require('../make-request'); 6 | 7 | describe('Parent-chain test', function () { 8 | const app = bts('./tests/app-folders/Parent'); 9 | const server = http.createServer(app); 10 | 11 | 12 | beforeEach(function () { 13 | server.listen(8181, '127.0.0.1'); 14 | }); 15 | afterEach(function () { 16 | server.close(); 17 | }); 18 | 19 | 20 | it('should pass', function (done) { 21 | makeRequest('GET', '/a/b1', (body) => { 22 | expect(body).to.equal('prebpost'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should pass', function (done) { 28 | makeRequest('GET', '/a/b/c1', (body) => { 29 | expect(body).to.equal('prepre1cpost1post'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should pass', function (done) { 35 | makeRequest('GET', '/a/b/c/d1', (body) => { 36 | expect(body).to.equal('prepre1pre2dpost2post1post'); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/utils/should-be-ignored.js: -------------------------------------------------------------------------------- 1 | function isNotJs (name) { 2 | const lastDot = name.lastIndexOf('.'); 3 | const ext = name.substr(lastDot + 1); 4 | 5 | return ext.toLowerCase() !== 'js'; 6 | } 7 | 8 | 9 | function matchStartWith (app, name) { 10 | const starterList = app.ignoreStartWith; 11 | const starterListLen = starterList.length; 12 | 13 | for (let i = 0; i < starterListLen; i += 1) { 14 | const starter = starterList[i]; 15 | const nameStart = name.substr(0, starter.length); 16 | 17 | if (nameStart === starter) { 18 | return true; 19 | } 20 | } 21 | 22 | return false; 23 | } 24 | 25 | 26 | function isInIgnoreList (app, name) { 27 | if (app.ignoreList.length) { 28 | return app.ignoreList.includes(name); 29 | } 30 | 31 | return false; 32 | } 33 | 34 | module.exports = function shouldBeIgnored (app, name, normalized, isFile) { 35 | if (isFile && isNotJs(name)) return true; 36 | 37 | if (matchStartWith(app, normalized)) return true; 38 | 39 | if (isInIgnoreList(app, normalized)) return true; 40 | 41 | return false; 42 | }; 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Tests", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--timeout", 14 | "999999", 15 | "--colors", 16 | "${workspaceFolder}/tests" 17 | ], 18 | "internalConsoleOptions": "openOnSessionStart" 19 | }, 20 | { 21 | "name": "Debug Playground", 22 | "type": "node", 23 | "request": "launch", 24 | "program": "${workspaceFolder}/playground/play.js", 25 | "cwd": "${workspaceFolder}/playground", 26 | "windows": { 27 | "program": "${workspaceFolder}\\playground\\play.js", 28 | "cwd": "${workspaceFolder}\\playground" 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /Docs/App Hooks/shared_ctrls.md: -------------------------------------------------------------------------------- 1 | Hook: "shared_ctrls" 2 | ====================== 3 | **Entry Type**: a folder 4 | 5 | When Bootstruct is initialized it parses the web root folder. User custom named folders become controllers, and files become methods. 6 | 7 | A shared controller is a "standalone" controller, a child-controller that can be "adopted" by any controller in your web root. 8 | 9 | Let's say you want to run the same test over all of your controllers when addressing them with an additional `/test` in the URL (e.g. `/user/test`, `/user/profile/test`, `user/friends/test`). 10 | 11 | You can create a shared `test` controller. 12 | 13 | To create a shared controller, create in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) a `shared_ctrls` folder. 14 | 15 | ``` 16 | ├── myProject 17 | │ ├── node_modules 18 | │ ├── server-index.js 19 | │ ├── api 20 | │ └── api_hooks 21 | │ └── shared_ctrls <── 22 | │ └── test 23 | ``` 24 | 25 | Folders inside `shared_ctrls` (like `test`) will be parsed as controllers as if they were in your web root folder. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Itai Tenenbaum (https://github.com/taitulism) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Docs/Controller Hooks/no_verb.md: -------------------------------------------------------------------------------- 1 | _no_verb 2 | -------- 3 | **Chain**: Target. 4 | **Alias**: `_no-verb`. 5 | 6 | The `_no_verb` method will get called for request type that the target-controller doesn't have a for, like a POST request to a controller without a `_post.js` file. 7 | 8 | ``` 9 | ├── api 10 | │ ├── index.js 11 | │ ├── _get.js 12 | │ └── _no_verb.js 13 | ``` 14 | 15 | If someone makes a non-supported HTTP verb request and you want to respond to this case, `_no_verb` is your method. 16 | ``` 17 | request: GET `/` 18 | logs: 19 | api/index 20 | api/_get 21 | 22 | request: POST `/` 23 | logs: 24 | api/index 25 | api/_no_verb 26 | ``` 27 | 28 | The `_no_verb` method is kinda special because it's a delegated method: the controller who has it, delgates this method to all of its sub-controllers. 29 | ``` 30 | ├── api (RC) 31 | │ ├── index.js 32 | │ ├── _get.js 33 | │ ├── _post.js 34 | │ ├── _no_verb.js 35 | │ └── A 36 | │ └── _get.js 37 | ``` 38 | On a POST request to `/A` (which doesn't have a `_post` method) you'll get the `api/_no_verb.js` called. Delegated from the "RC". 39 | 40 | **IMPORTANT NOTE:** The `_no_verb` method gets called only if at least 1 verb file exists (`_get`, `_post`, `_put`, `_delete`). 41 | -------------------------------------------------------------------------------- /tests/specs/method-chain.spec.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {expect} = require('chai'); 3 | 4 | const bts = require('../../'); 5 | const makeRequest = require('../make-request'); 6 | 7 | describe('Method-chain test', function () { 8 | const app = bts('./tests/app-folders/Method'); 9 | const server = http.createServer(app); 10 | 11 | 12 | beforeEach(function () { 13 | server.listen(8181, '127.0.0.1'); 14 | }); 15 | afterEach(function () { 16 | server.close(); 17 | }); 18 | 19 | 20 | it('should pass', function (done) { 21 | makeRequest('GET', '/a1', (body) => { 22 | expect(body).to.equal('preapost'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should pass', function (done) { 28 | makeRequest('GET', '/a/b1', (body) => { 29 | expect(body).to.equal('prebpost'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should pass', function (done) { 35 | makeRequest('GET', '/a/b/c1', (body) => { 36 | expect(body).to.equal('precpost'); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('_METHOD turns a folder into a method (instead of a controller)', function (done) { 42 | makeRequest('GET', '/a/b/a-method', (body) => { 43 | expect(body).to.equal('premethodpost'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/index.md: -------------------------------------------------------------------------------- 1 | index 2 | ----- 3 | **Chain**: Target. 4 | **Aliass**: `_before_verb`, `_before-verb`. 5 | 6 | >**NOTE**: `index` is the only reserved name that doesn't start with a `_` sign. 7 | 8 | `index` is a reserved entry name (and so are its aliases). Its exported function gets mounted on the target-chain of the controller it's in. The `index` method gets called when its controller is the request target-controller, for **all** HTTP verbs. 9 | 10 | Example structure: 11 | ``` 12 | ├── api (RC) 13 | │ ├── index.js ──> handles requests to / 14 | │ └── A 15 | │ └── index.js ──> handles requests to /A 16 | ``` 17 | This works very similar to other platforms where you ask for a folder and its default file is served/executed (index.html, index.php, index.asp etc). 18 | 19 | If a controller has no sub-controllers and has only an `index` method, like "A" in our case, you can cut the overhead of a folder (a controller) and turn it into a file (a method). 20 | 21 | Before: 22 | ``` 23 | ├── api 24 | │ ├── index.js 25 | │ └── A ──> folder 26 | │ └── index.js 27 | ``` 28 | After: 29 | ``` 30 | ├── api 31 | │ ├── index.js 32 | │ └── A.js ──> file 33 | ``` 34 | Now "A" is a method instead of a controller with a single method. 35 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/pre & post sub.md: -------------------------------------------------------------------------------- 1 | _pre_sub & _post_sub 2 | -------------------- 3 | **Chain**: Parent. 4 | **Aliass**: `_pre-sub`, `_post-sub` (respectively). 5 | 6 | These reserved methods will run before and after a child-controller (sub-ctrl) (pre=before, post=after). 7 | 8 | `index` and the verbs get called only by the target-controller. `_in` & `_out` get called anyway, whether the controller is the target-controller or not. `pre_sub` and `post_sub` get called only by a parent-controller, before and after the sub-controller, respectively. 9 | ``` 10 | ├── api 11 | │ ├── _in.js 12 | │ ├── index.js 13 | │ ├── _pre_sub.js <── 14 | │ ├── _post_sub.js <── 15 | │ ├── _out.js 16 | │ └── A 17 | │ ├── _in.js 18 | │ ├── index.js 19 | │ └── _out.js 20 | ``` 21 | 22 | Assume all files log their names and calling `io.next()` as before: 23 | ``` 24 | request: / 25 | logs: 26 | api/_in 27 | api/index 28 | api/_out 29 | (no sub-controller was called) 30 | 31 | request: /A 32 | logs: 33 | api/_in 34 | api/_pre_sub <── 35 | api/A/_in 36 | api/A/index 37 | api/A/_out 38 | api/_post_sub <── 39 | api/_out 40 | ``` 41 | 42 | The "A" controller doesn't have any sub-controllers so `_pre_sub` and `_post_sub` would be redundent if existed. They would never get called. 43 | -------------------------------------------------------------------------------- /src/utils/f2j.js: -------------------------------------------------------------------------------- 1 | const {statSync, readdirSync: readDirSync} = require('fs'); 2 | const {resolve} = require('path'); 3 | 4 | const {FILE, FOLDER} = require('../constants'); 5 | 6 | 7 | /* 8 | ┌───────────────────────────────────────────────────────────────────────────── 9 | │ args: 10 | │ folderPath [string] - a resolved path to an existing folder. 11 | │ 12 | │ returns: 13 | │ mappedObj [obj] - folder mappings json obj. 14 | │ 15 | │ mappedObj = { 16 | │ path: 'absolute/path/to/entry', 17 | │ type: 0|1, // folder|file 18 | │ entries: { 19 | │ entryName1: {mappedObj}, 20 | │ entryName2: {mappedObj}, 21 | │ entryName3: {mappedObj} 22 | │ } 23 | │ }; 24 | │ 25 | */ 26 | function f2j (path) { 27 | const folderObj = { 28 | path, 29 | type: FOLDER, 30 | entries: {}, 31 | }; 32 | 33 | const entries = readDirSync(resolve(path)); 34 | 35 | entries.forEach((entryName) => { 36 | const resolved = resolve(path, entryName); 37 | const entryStat = statSync(resolved); 38 | const isFile = entryStat.isFile(); 39 | 40 | let newObj; 41 | 42 | if (isFile) { 43 | newObj = { 44 | path: resolved, 45 | type: FILE, 46 | }; 47 | } 48 | else { 49 | newObj = f2j(resolved); 50 | } 51 | 52 | folderObj.entries[entryName] = newObj; 53 | }); 54 | 55 | return folderObj; 56 | } 57 | 58 | 59 | // ------------------ 60 | module.exports = f2j; 61 | -------------------------------------------------------------------------------- /tests/specs/hooks-2.spec.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {expect} = require('chai'); 3 | 4 | const bts = require('../../'); 5 | const makeRequest = require('../make-request'); 6 | 7 | describe('hooks 2 test', function () { 8 | const app = bts('./tests/app-folders/hooks2'); 9 | const server = http.createServer(app); 10 | 11 | 12 | beforeEach(function () { 13 | server.listen(8181, '127.0.0.1'); 14 | }); 15 | afterEach(function () { 16 | server.close(); 17 | }); 18 | 19 | 20 | it('should pass all tests', function (done) { 21 | makeRequest('GET', '/', (body) => { 22 | expect(body).to.equal('[io_init-c_pro1-c_pro2-io_pro1-io_pro2-pub-view-item]exit'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should pass sub_methods 1 test', function (done) { 28 | makeRequest('GET', '/m1', (body) => { 29 | expect(body).to.equal('[shared_m1]'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should pass sub_methods 2 test', function (done) { 35 | makeRequest('GET', '/a/m2', (body) => { 36 | expect(body).to.equal('[{shared_m2}]'); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should pass sub_ctrls 1 test', function (done) { 42 | makeRequest('GET', '/x', (body) => { 43 | expect(body).to.equal('[X1X2X3]'); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should pass sub_ctrls 2 test', function (done) { 49 | makeRequest('GET', '/a/x', (body) => { 50 | expect(body).to.equal('[{X1X2X3}]'); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/io.js: -------------------------------------------------------------------------------- 1 | const {STATUS_CODES} = require('http'); 2 | 3 | const NOT_FOUND = 404; 4 | 5 | module.exports = function getIoClass () { 6 | class IO { 7 | constructor (req, res) { 8 | this.ctrl = null; 9 | this.req = req; 10 | this.res = res; 11 | this.states = Object.create(null); 12 | this.params = splitPath(req.url); 13 | } 14 | 15 | next () { 16 | this.ctrl.next(this); 17 | } 18 | 19 | /* 20 | | This function gets overriden by the "io.exit" hook. 21 | */ 22 | exit () { 23 | const {res} = this; 24 | 25 | // .finished is depracated since v13.4.0. Use writableFinished/writableEnded. 26 | if (res.finished) return; 27 | if (res.headersSent) return res.end(); 28 | 29 | res.writeHead(NOT_FOUND, STATUS_CODES[NOT_FOUND]); 30 | res.end(); 31 | } 32 | } 33 | 34 | return IO; 35 | }; 36 | 37 | 38 | /* 39 | ┌────────────────────────────────────────── 40 | │ accepts a full url and returns an array 41 | │ 42 | │ ignores qryStr 43 | │ merges repeating slashes 44 | │ trim preceding & trailing slashes 45 | │ split by / 46 | │ 47 | */ 48 | function splitPath (url) { 49 | // split by ? and take the first part 50 | let [path] = url.split('?'); 51 | 52 | // replace multi slashes with one 53 | path = path.replace(/\/{2,}/gu, '/'); 54 | 55 | // remove preceding slash 56 | path = path.substr(1); 57 | 58 | const len = path.length; 59 | 60 | // remove trailing slash 61 | if (path.charAt(len - 1) === '/') { 62 | path = path.substring(0, len - 1); 63 | } 64 | 65 | return path.split('/'); 66 | } 67 | -------------------------------------------------------------------------------- /Docs/App Hooks/io_exit.md: -------------------------------------------------------------------------------- 1 | Hook: "io_exit" 2 | =============== 3 | **Entry Type**: both a file or a folder 4 | **Exports**: a function 5 | 6 | The `IO` class is one of the main components in Bootstruct. `io` instances are created on every request and used to carry request related data while they "travel" between your controllers and methods. 7 | 8 | You can write a function that will get called for any request on the `io`'s way out, after the `RC`'s `last` method. It's the very last code to run for any request. This can be used to set a default ending response like a "404 - not found". 9 | 10 | To do that, create in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) an `io_exit.js` file or an `io_exit` folder. 11 | 12 | **File** 13 | ``` 14 | ├── myProject 15 | │ ├── node_modules 16 | │ ├── server-index.js 17 | │ ├── api 18 | │ └── api_hooks 19 | │ └── io_exit.js <── 20 | ``` 21 | When using a file it should export an object: 22 | ```js 23 | // io_exit.js 24 | module.exports = function (app) {...} 25 | ``` 26 | 27 | If your `io_exit` gets too big for a single file, turn it into a folder and export the function from an `index.js` file. 28 | 29 | **Folder** 30 | ``` 31 | ├── myProject 32 | │ ├── node_modules 33 | │ ├── server-index.js 34 | │ ├── api 35 | │ └── api_hooks 36 | │ └── io_exit <── 37 | │ └── index.js 38 | ``` 39 | 40 | The context of the "this" keyword within the `io_exit` function refers to the `io` instance. 41 | 42 | The only argument the `io_exit` function is called with is the `app` object. 43 | -------------------------------------------------------------------------------- /Docs/App Hooks/ignore.md: -------------------------------------------------------------------------------- 1 | Hook: "ignore" 2 | ============== 3 | **Entry Type**: file 4 | **Exports**: a string or an array of strings 5 | 6 | When Bootstruct is initialized it parses the web root folder. User folders become controllers, user files become methods and reserved name entries become methods in the controllers' chains. When you need an entry (a file or a folder) to be ignored you can give it a name that starts with an underscore or a dot (e.g. `.ignoredEntry` or `_ignoredEntry`). 7 | 8 | An example would be: 9 | ``` 10 | ├── api 11 | │ ├── _myModules <── ignored by parser 12 | │ │ ├── helper1.js 13 | │ │ ├── helper2.js 14 | │ │ └── helper3.js 15 | │ └── index.js 16 | ``` 17 | In this case `_myModules` is not parsed as a controller and you cannot reach it by requesting `/_myModules`. This request would be handled by the `index` method and `_myModules` would be a parameter in `io.params` array. 18 | 19 | An underscore in your entry names is generally not a pretty sight. 20 | 21 | You can add "myModules" (without the underscore) to the parser's ignore list. You do that by creating a file named `ignore.js` in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) and exporting your ignored names: 22 | ``` 23 | ├── myProject 24 | │ ├── node_modules 25 | │ ├── server-index.js 26 | │ ├── api 27 | │ └── api_hooks 28 | │ └── ignore.js <── 29 | ``` 30 | 31 | From within `ignore.js` you export a string: 32 | ```js 33 | module.exports = 'myModules'; 34 | ``` 35 | or an array of strings (ignore list) 36 | ```js 37 | module.exports = ['myModules', 'myHelpers']; 38 | ``` 39 | -------------------------------------------------------------------------------- /Docs/App Hooks/shared_methods.md: -------------------------------------------------------------------------------- 1 | Hook: "shared_methods" 2 | ====================== 3 | **Entry Type**: both a file or a folder 4 | **Exports**: an object of functions (if file) or function files (if folder) 5 | 6 | When Bootstruct is initialized it parses the web root folder. User custom named folders become controllers, and files become methods. 7 | 8 | When you need a certain method to be called by more than one controller, you don't want to duplicate the file in each folder you need it. 9 | 10 | Let's say you are building a very friendly server: by requesting any of its paths with an additional `/help` in the URL (e.g. `/user/help`, `/user/profile/help`, `user/friends/help`) you get a response with some help text. 11 | 12 | You can create a shared `help` method. 13 | 14 | To create a shared method, create in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) a `shared_methods.js` file or a `shared_methods` folder. 15 | 16 | **File** 17 | ``` 18 | ├── myProject 19 | │ ├── node_modules 20 | │ ├── server-index.js 21 | │ ├── api 22 | │ └── api_hooks 23 | │ └── shared_methods.js <── 24 | ``` 25 | When using a file it should export an object of named function: 26 | ```js 27 | // shared_methods.js 28 | module.exports = { 29 | help: function (io){...} 30 | }; 31 | ``` 32 | 33 | **Folder** 34 | ``` 35 | ├── myProject 36 | │ ├── node_modules 37 | │ ├── server-index.js 38 | │ ├── api 39 | │ └── api_hooks 40 | │ └── shared_methods <── 41 | │ └── help.js 42 | ``` 43 | When using a folder, its entry names become the keys and whatever you export are the values. If `help` gets too big for a single file, turn it into a folder and export the function from an `index.js` file. 44 | -------------------------------------------------------------------------------- /Docs/App Hooks/ctrl_proto.md: -------------------------------------------------------------------------------- 1 | Hook: "ctrl_proto" 2 | ================== 3 | **Entry Type**: both a file or a folder 4 | **Exports**: anything 5 | 6 | The `Ctrl` class is one of the main components in Bootstruct. `ctrl` instances (controllers) are nested inside each other and the root controller is the building that `io`s visit its different "departments" and "sub-departments". 7 | 8 | The `Ctrl` prototype is extendable. You can create your own methods and use them when you handle requests. 9 | 10 | Let's say you want to be able to log some request data to a file. Create on the `Ctrl` prototype a `log2file` method that accepts an `io` as an argument and call it when you need it with `this.log2file(io)`. 11 | 12 | To extend the `Ctrl` prototype, create in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) an `ctrl_proto.js` file or an `ctrl_proto` folder. 13 | 14 | **File** 15 | ``` 16 | ├── myProject 17 | │ ├── node_modules 18 | │ ├── server-index.js 19 | │ ├── api 20 | │ └── api_hooks 21 | │ └── ctrl_proto.js <── 22 | ``` 23 | When using a file it should export an object: 24 | ```js 25 | // ctrl_proto.js 26 | module.exports = { 27 | log2file: function (io){...} 28 | }; 29 | ``` 30 | 31 | **Folder** 32 | ``` 33 | ├── myProject 34 | │ ├── node_modules 35 | │ ├── server-index.js 36 | │ ├── api 37 | │ └── api_hooks 38 | │ └── ctrl_proto <── 39 | │ └── log2file.js 40 | ``` 41 | When using a folder, its entry names become the keys and whatever you export are the values. If `log2file` gets too big for a single file, turn it into a folder and export the function from an `index.js` file. 42 | 43 | As expected, the "this" keyword within `Ctrl` prototype methods refers to the `ctrl` instance. 44 | -------------------------------------------------------------------------------- /Docs/App Hooks/io_proto.md: -------------------------------------------------------------------------------- 1 | Hook: "io_proto" 2 | ================ 3 | **Entry Type**: both a file or a folder 4 | **Exports**: anything 5 | 6 | The `IO` class is one of the main components in Bootstruct. `io` instances are created on every request and used to carry request related data while they "travel" between your controllers and methods. 7 | 8 | The `IO` prototype is extendable. You can create your own methods and use them when you handle requests. 9 | 10 | Let's say you want to do something with the query-string (domain.com/?key=value) in some of your methods. You would probably prefer to work with an object rather than a string. Create a `parseQryStr` method on the `IO` prototype and call it with `io.parseQryStr()` from where you need it. 11 | 12 | To extend the `IO` prototype, create in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) an `io_proto.js` file or an `io_proto` folder. 13 | 14 | **File** 15 | ``` 16 | ├── myProject 17 | │ ├── node_modules 18 | │ ├── server-index.js 19 | │ ├── api 20 | │ └── api_hooks 21 | │ └── io_proto.js <── 22 | ``` 23 | When using a file it should export an object: 24 | ```js 25 | // io_proto.js 26 | module.exports = { 27 | parseQryStr: function (){...} 28 | }; 29 | ``` 30 | 31 | **Folder** 32 | ``` 33 | ├── myProject 34 | │ ├── node_modules 35 | │ ├── server-index.js 36 | │ ├── api 37 | │ └── api_hooks 38 | │ └── io_proto <── 39 | │ └── parseQryStr.js 40 | ``` 41 | When using a folder, its entry names become the keys and whatever you export are the values. If `parseQryStr` gets too big for a single file, turn it into a folder and export the function from an `index.js` file. 42 | 43 | As expected, the "this" keyword within `IO` prototype methods refers to the `io` instance. 44 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/verbs.md: -------------------------------------------------------------------------------- 1 | _verbs 2 | ------ 3 | 4 | >**NOTE**: If you're using the `_verbs` folder, any duplicate \ outside it will override the ones inside. 5 | 6 | `_verbs` is a reserved entry name but it doesn't stand for a method like the others. `_verbs` acts only as a namespace entry to hold the different verb files. 7 | 8 | Using all verb types and having multiple sub-controllers/methods can hurt your eyes: 9 | ``` 10 | ├── api 11 | │ ├── [blog] <── controller 12 | │ ├── [messages] <── controller 13 | │ ├── [profile] <── controller 14 | │ ├── about.js <── method 15 | │ ├── index.js <── verbs ("index" alias) 16 | │ ├── contact.js <── method 17 | │ ├── _delete.js <── verb 18 | │ ├── _get.js <── verb 19 | │ ├── _post.js <── verb 20 | │ └── _put.js <── verb 21 | ``` 22 | For the sake of your eyes, you can use a `_verbs` folder, just as a namespace to contain the verbs entries: 23 | ``` 24 | ├── api 25 | │ ├── [blog] 26 | │ ├── [messages] 27 | │ ├── [profile] 28 | │ ├── about.js 29 | │ ├── contact.js 30 | │ └── _verbs <── 31 | │ ├── index.js 32 | │ ├── get.js 33 | │ ├── post.js 34 | │ ├── put.js 35 | │ └── delete.js 36 | ``` 37 | 38 | >**NOTE**: Under `_verbs` namespace (file or folder), you won't be needing the `_` sign for your verbs (e.g. _get, _post etc.) 39 | 40 | You can also use a `_verbs.js` file to export your verbs methods from an object: 41 | ``` 42 | ├── api 43 | │ ├── [blog] 44 | │ ├── [messages] 45 | │ ├── [profile] 46 | │ ├── about.js 47 | │ ├── contact.js 48 | │ └── _verbs.js <── 49 | ``` 50 | 51 | ```js 52 | // _verbs.js 53 | module.exports = { 54 | get: function (io) { 55 | 56 | }, 57 | post: function (io) { 58 | 59 | }, 60 | put: function (io) { 61 | 62 | }, 63 | delete: function (io) { 64 | 65 | }, 66 | no_verb: function (io) { 67 | 68 | } 69 | }; 70 | ``` 71 | -------------------------------------------------------------------------------- /tests/specs/hooks-1.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const http = require('http'); 4 | const {expect} = require('chai'); 5 | 6 | const bts = require('../../'); 7 | const makeRequest = require('../make-request'); 8 | 9 | describe('hooks 1 test', function() { 10 | const app = bts('./tests/app-folders/hooks1'); 11 | const server = http.createServer(app); 12 | 13 | 14 | beforeEach(function() { 15 | server.listen(8181, '127.0.0.1'); 16 | }); 17 | afterEach(function() { 18 | server.close(); 19 | }); 20 | 21 | 22 | it('should pass ignore test', (done) => { 23 | makeRequest('GET', '/ignored', (body) => { 24 | expect(body).to.equal(''); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should pass io_init test', (done) => { 30 | makeRequest('GET', '/io_init', (body) => { 31 | expect(body).to.equal('io_init'); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should pass io_exit test', (done) => { 37 | makeRequest('GET', '/io_exit', (body) => { 38 | expect(body).to.equal('io_exit'); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('should pass ctrl_proto test', (done) => { 44 | makeRequest('GET', '/ctrl_proto', (body) => { 45 | expect(body).to.equal('ctrl_proto'); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should pass io_proto test', (done) => { 51 | makeRequest('GET', '/io_proto', (body) => { 52 | expect(body).to.equal('io_proto'); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('should pass ctrl_hooks test', (done) => { 58 | makeRequest('GET', '/ctrl_hooks', (body) => { 59 | expect(body).to.equal('public'); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should pass shared_methods test', (done) => { 65 | makeRequest('GET', '/a_shared_method', (body) => { 66 | expect(body).to.equal('shared_methods'); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should pass item test', (done) => { 72 | makeRequest('GET', '/item', (body) => { 73 | expect(body).to.equal('item'); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/in & out.md: -------------------------------------------------------------------------------- 1 | _in & _out 2 | ---------- 3 | **Chain**: All. 4 | 5 | `_in` is the first thing controllers run when an `io` checks-in and `_out` is the very last thing to run. 6 | They get called for **any** request to the controller they are in. 7 | 8 | Assume: 9 | ``` 10 | ├── api 11 | │ ├── _in.js 12 | │ ├── index.js 13 | │ ├── _out.js 14 | │ └── A 15 | │ ├── _in.js 16 | │ ├── index.js 17 | │ └── _out.js 18 | ``` 19 | Assume all files log their names and calling `io.next()`: 20 | ```js 21 | module.exports = function (io) { 22 | console.log(__filename); 23 | io.next(); 24 | }; 25 | ``` 26 | 27 | The following shows the logs we'll get for different requests: 28 | 29 | **request**: `/` 30 | **logs**: 31 |   path/to/api/_in.js 32 |   path/to/api/index.js 33 |   path/to/api/_out.js 34 | 35 | **request**: `/A` 36 | **logs**: 37 |   path/to/api/_in.js 38 |   path/to/api/A/_in.js 39 |   path/to/api/A/index.js 40 |   path/to/api/A/_out.js 41 |   path/to/api/_out.js 42 | 43 | `_in` and `_out` always get called. `index` runs only in the target-controller. 44 | 45 | Now with verbs: 46 | ``` 47 | ├── api 48 | │ ├── _in.js 49 | │ ├── _before_verb.js ("index" alias) 50 | │ ├── _get.js <── 51 | │ ├── _post.js <── 52 | │ ├── _out.js 53 | │ └── A 54 | │ ├── _in.js 55 | │ ├── _before_verb.js 56 | │ ├── _get.js <── 57 | │ ├── _post.js <── 58 | │ └── _out.js 59 | ``` 60 | 61 | >**NOTE**: The full path to the `api` folder and file extensions (.js) were removed from log for readability: 62 | 63 | ``` 64 | request: GET `/` 65 | logs: 66 | api/_in 67 | api/index 68 | api/_get 69 | api/_out 70 | 71 | request: POST `/` 72 | logs: 73 | api/_in 74 | api/index 75 | api/_post 76 | api/_out 77 | 78 | request: GET `/A` 79 | logs: 80 | api/_in 81 | api/A/_in 82 | api/A/index 83 | api/A/_get 84 | api/A/_out 85 | api/_out 86 | 87 | request: POST `/A` 88 | logs: 89 | api/_in 90 | api/A/_in 91 | api/A/index 92 | api/A/_post 93 | api/A/_out 94 | api/_out 95 | ``` 96 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/setup-chains.js: -------------------------------------------------------------------------------- 1 | const has = require('./has'); 2 | const run = require('./run'); 3 | 4 | 5 | /* 6 | ┌──────────────────────────────────────────────────────────────── 7 | │ examples: 8 | │ 0. parentChain = [$in, runSub, $out] 9 | │ 1. targetChain = [$in, runVerb, $out] 10 | │ 2. methodChain = [$in, runMethod, $out] 11 | │ 12 | │ "setupChains" fn creates an array of 3 chains (3 arrays) of 13 | │ methods to run for each case. 14 | │ this.chains = [[...],[...],[...]]; 15 | │ 16 | */ 17 | module.exports = function setupChains (ctrl) { 18 | const targetChain = []; 19 | const methodChain = []; 20 | const parentChain = []; 21 | 22 | const $in = ctrl.in; 23 | 24 | if ($in) { 25 | targetChain.push($in); 26 | methodChain.push($in); 27 | parentChain.push($in); 28 | } 29 | 30 | // targetChain 31 | setTargetChain(ctrl, targetChain); 32 | 33 | // methodChain 34 | if (has.methods(ctrl)) { 35 | setMethodChain(ctrl, methodChain); 36 | } 37 | 38 | // parentChain 39 | if (has.subCtrls(ctrl)) { 40 | setParentChain(ctrl, parentChain); 41 | } 42 | 43 | const $out = ctrl.out; 44 | 45 | if ($out) { 46 | targetChain.push($out); 47 | methodChain.push($out); 48 | parentChain.push($out); 49 | } 50 | 51 | // 0: parentChain 52 | // 1: targetChain 53 | // 2: methodChain 54 | return [parentChain, targetChain, methodChain]; 55 | }; 56 | 57 | function setTargetChain (ctrl, chain) { 58 | if (ctrl.index) { 59 | chain.push(ctrl.index); 60 | } 61 | 62 | if (has.verbs(ctrl)) { 63 | chain.push(run.verb); 64 | } 65 | 66 | if (ctrl.afterVerb) { 67 | chain.push(ctrl.afterVerb); 68 | } 69 | } 70 | 71 | function setMethodChain (ctrl, chain) { 72 | if (ctrl.preMethod) { 73 | chain.push(ctrl.preMethod); 74 | } 75 | 76 | chain.push(run.method); 77 | 78 | if (ctrl.postMethod) { 79 | chain.push(ctrl.postMethod); 80 | } 81 | } 82 | 83 | function setParentChain (ctrl, chain) { 84 | if (ctrl.preSub) { 85 | chain.push(ctrl.preSub); 86 | } 87 | 88 | chain.push(run.subCtrl); 89 | 90 | if (ctrl.postSub) { 91 | chain.push(ctrl.postSub); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/parse-folder-map.js: -------------------------------------------------------------------------------- 1 | const error = require('../../errors'); 2 | const {FILE} = require('../../constants'); 3 | 4 | const { 5 | normalizeEntryName, 6 | getDuplicatedKeys, 7 | shouldBeIgnored, 8 | forIn, 9 | } = require('../../utils'); 10 | 11 | 12 | /* 13 | ┌─────────────────────────────────────────── 14 | | forEach entry in the folder (controller): 15 | | ignore? 16 | | handle if has an entryHandler 17 | | or 18 | | set as a ctrl's method/subCtrl 19 | */ 20 | module.exports = function parseFolderMap (ctrl) { 21 | const {app} = ctrl; 22 | const {ctrlHooks} = app; 23 | 24 | forIn(ctrl.folderMap.entries, (key, entryMap) => { 25 | const isFile = entryMap.type === FILE; 26 | const name = normalizeEntryName(key, isFile); 27 | 28 | // if hook exists 29 | if (ctrlHooks[name]) { 30 | ctrlHooks[name].call(ctrl, entryMap); 31 | } 32 | else if (shouldBeIgnored(app, key, name, isFile)) return; // eslint-disable-line 33 | else if (isFile || entryMap.entries._METHOD) { 34 | const method = require(entryMap.path); 35 | const params = extractFnParams(method); 36 | 37 | if (!params) { 38 | throw error.methodsWithNoParams(entryMap.path); 39 | } 40 | 41 | method.path = entryMap.path; 42 | method.params = params; 43 | 44 | ctrl.methods[name] = method; 45 | } 46 | else { 47 | ctrl.subCtrls[name] = entryMap; 48 | } 49 | }); 50 | 51 | const duplicated = getDuplicatedKeys(ctrl.methods, ctrl.subCtrls); 52 | 53 | if (duplicated.length > 0) { 54 | throw error.fileAndFolderNameCollision(duplicated, ctrl.folderMap.path); 55 | } 56 | }; 57 | 58 | /* 59 | | Matches the parens () and their content. 60 | | use extra parens for grouping the params and excluding the wrapping parens 61 | */ 62 | const REGEX_functionParams = /\((.*)\)/u; 63 | 64 | function extractFnParams (fn) { 65 | const fnString = fn.toString(); 66 | const paramsMatches = fnString.match(REGEX_functionParams); 67 | 68 | if (!paramsMatches || !paramsMatches[1]) return false; 69 | 70 | const params = paramsMatches[1].split(/\s*,\s*/u); 71 | 72 | // remove first param (io) 73 | params.shift(); 74 | 75 | const $params = params 76 | // leave only $params 77 | .filter(param => param[0] === '$') 78 | // remove $ 79 | .map(param => param.substr(1)); 80 | 81 | return $params; 82 | } 83 | -------------------------------------------------------------------------------- /Docs/App Hooks/io_init.md: -------------------------------------------------------------------------------- 1 | Hook: "io_init" 2 | =============== 3 | **Entry Type**: both a file or a folder 4 | **Exports**: a function 5 | 6 | The `IO` class is one of the main components in Bootstruct. `io` instances are created on every request and used to carry request related data while they "travel" between your controllers and methods. 7 | 8 | You can write a function that will get called for any request on the `io` initialization, before the `io` checks-in at your `RC` (before `RC`'s `first` method). It's the very first code to run for any request. This way you can design the `io` to hold all the properties you'll need in your app. For example, having an `.IP` property or `.isLoggedIn` on your `io`s. 9 | 10 | To do that, create in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) an `io_init.js` file or an `io_init` folder. 11 | 12 | **File** 13 | ``` 14 | ├── myProject 15 | │ ├── node_modules 16 | │ ├── server-index.js 17 | │ ├── api 18 | │ └── api_hooks 19 | │ └── io_init.js <── 20 | ``` 21 | When using a file it should export an object: 22 | ```js 23 | // io_init.js 24 | module.exports = function (app) {...} 25 | ``` 26 | 27 | If your `io_init` gets too big for a single file, turn it into a folder and export the function from an `index.js` file. 28 | 29 | **Folder** 30 | ``` 31 | ├── myProject 32 | │ ├── node_modules 33 | │ ├── server-index.js 34 | │ ├── api 35 | │ └── api_hooks 36 | │ └── io_init <── 37 | │ └── index.js 38 | ``` 39 | 40 | 41 | 42 | Context and arguments 43 | --------------------- 44 | The context of the "this" keyword within the `io_init` function refers to the `io` instance. 45 | 46 | The only argument the `io_init` function is called with is the `app` object. The `io_init` function hook could be considered as the lobby of your building, it's the first interaction point between your app and all incoming visitors but before any interaction with any controller. 47 | 48 | At the end of the function, you need to tell Bootstruct the visitor (i.e. the `io`) is ready for a check-in at the root-controller (`RC`). You do that by calling a check-in method on the `app` object and pass it the `io`, which is the context of `this`. 49 | 50 | ```js 51 | module.exports = function my_init_fn (app) { 52 | // ... your code ... 53 | 54 | // "this" refers to the current "io" instance 55 | app.checkIn(this); 56 | }; 57 | ``` 58 | -------------------------------------------------------------------------------- /tests/specs/full-use-case.spec.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {expect} = require('chai'); 3 | 4 | const bts = require('../../'); 5 | const makeRequest = require('../make-request'); 6 | 7 | describe('Full use case test', function () { 8 | const app = bts('./tests/app-folders/www'); 9 | const server = http.createServer(app); 10 | 11 | 12 | beforeEach(function () { 13 | server.listen(8181, '127.0.0.1'); 14 | }); 15 | afterEach(function () { 16 | server.close(); 17 | }); 18 | 19 | 20 | it('should pass', (done) => { 21 | makeRequest('GET', '/', (body) => { 22 | expect(body).to.equal('figal'); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should pass', (done) => { 28 | makeRequest('POST', '/', (body) => { 29 | expect(body).to.equal('fial'); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should pass', (done) => { 35 | makeRequest('POST', '/bla', (body) => { 36 | expect(body).to.equal('fiblaal'); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should pass', (done) => { 42 | makeRequest('PUT', '/qwe', (body) => { 43 | expect(body).to.equal('fprmqweptml'); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should pass', (done) => { 49 | makeRequest('GET', '/a', (body) => { 50 | expect(body).to.equal('fprsf1i1g1a1l1ptsl'); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should pass', (done) => { 56 | makeRequest('POST', '/a', (body) => { 57 | expect(body).to.equal('fprsf1i1nva1l1ptsl'); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should pass', (done) => { 63 | makeRequest('GET', '/a/asd', (body) => { 64 | expect(body).to.equal('fprsf1prm1asdptm1l1ptsl'); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should pass', (done) => { 70 | makeRequest('GET', '/a/asd/bla/blu', (body) => { 71 | expect(body).to.equal('fprsf1prm1asdblabluptm1l1ptsl'); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should pass', (done) => { 77 | makeRequest('GET', '/a/b', (body) => { 78 | expect(body).to.equal('fprsf1prs1f2i2g2a2l2pts1l1ptsl'); 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should pass', (done) => { 84 | makeRequest('POST', '/a/b', (body) => { 85 | expect(body).to.equal('fprsf1prs1f2i2nva2l2pts1l1ptsl'); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('should pass', (done) => { 91 | makeRequest('GET', '/a/b/zxc', (body) => { 92 | expect(body).to.equal('fprsf1prs1f2prm2zxcptm2l2pts1l1ptsl'); 93 | done(); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /Docs/Controller Hooks.md: -------------------------------------------------------------------------------- 1 | Controller Hooks 2 | ================ 3 | Bootstruct has some reserved names for files and folders ("entries", for short) that on parsing stage (app initialization) have a 4 | special meaning. 5 | 6 | Bootstruct parses two folders on init: the web root folder (e.g. `api`) and its related hooks folder (e.g. `api_hooks`, optional). 7 | Each has its own parser with its reserved names for entries (files and folders) and each entry with a reserved name plays its own role in 8 | your app's play. 9 | 10 | When the parser hits these certain names it does something with the entry's content so 11 | these entries are expected to export a specific type of data (mostly a function). 12 | 13 | 14 | 15 | 16 | Web-Root Reserved Names 17 | ----------------------- 18 | The web root hooks (controller level hooks) become methods in their controller's chains so 19 | they all must eventually export a single function that handles a single argument, the `io`. 20 | 21 | One exception is the `_verbs` name. 22 | 23 | >**NOTE**: Reserved names with under_scores have a dash-version as aliases e.g. `_no_verb`/`_no-verb`. 24 | 25 | The following image describes a controller's chains (explained in the 26 | [docs main page](https://github.com/taitulism/Bootstruct/blob/master/README.md#controllers-flow)): 27 | The target-chain is in the middle, the parent-chain is on the left and the method-chain is on the right. 28 | ![Controller Chart-Flow](https://raw.githubusercontent.com/taitulism/Bootstruct/master/Docs/controller-flowchart.png) 29 | 30 | 31 | 32 | All chains 33 | ---------- 34 | * [_in](./Controller%20Hooks/in%20%26%20out.md) 35 | * [_out](./Controller%20Hooks/in%20%26%20out.md) 36 | 37 | 38 | 39 | Target-chain 40 | ------------ 41 | * [index / _before_verb](./Controller%20Hooks/index.md) 42 | * [_get](./Controller%20Hooks/get%20post%20put%20delete.md) 43 | * [_post](./Controller%20Hooks/get%20post%20put%20delete.md) 44 | * [_put](./Controller%20Hooks/get%20post%20put%20delete.md) 45 | * [_delete](./Controller%20Hooks/get%20post%20put%20delete.md) 46 | * [_no_verb](./Controller%20Hooks/no_verb.md) 47 | * [_after_verb](./Controller%20Hooks/after_verb.md) 48 | * [_verbs](./Controller%20Hooks/verbs.md) (EXCEPTION: not a method) 49 | 50 | 51 | 52 | Parent-chain 53 | ------------ 54 | * [_pre_sub](./Controller%20Hooks/pre%20%26%20post%20sub.md) 55 | * [_post_sub](./Controller%20Hooks/pre%20%26%20post%20sub.md) 56 | 57 | 58 | 59 | Method-chain 60 | ------------ 61 | * [_pre_method](./Controller%20Hooks/pre%20%26%20post%20method.md) 62 | * [_post_method](./Controller%20Hooks/pre%20%26%20post%20method.md) 63 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | const {resolve} = require('path'); 2 | const {existsSync: exists} = require('fs'); 3 | 4 | const ctrlHooksProto = require('../ctrl/hooks'); 5 | const appHooksProto = require('./hooks'); 6 | const createCtrlClass = require('../ctrl'); 7 | const createIOClass = require('../io'); 8 | const error = require('../errors'); 9 | const {ROOT_CTRL_NAME, FOLDER} = require('../constants'); 10 | const { 11 | forIn, 12 | f2j, 13 | normalizeEntryName, 14 | } = require('../utils'); 15 | 16 | class App { 17 | constructor (folderName, debug) { 18 | if (!folderName) throw error.webRootFolderNotFound(folderName); 19 | const webRootFolderPath = resolve(folderName); 20 | 21 | if (!exists(webRootFolderPath)) { 22 | throw error.webRootFolderNotFound(webRootFolderPath); 23 | } 24 | 25 | this.webRootFolderName = folderName; 26 | this.webRootFolderPath = webRootFolderPath; 27 | 28 | this.ignoreStartWith = ['_', '.']; 29 | this.ignoreList = []; 30 | 31 | this.Ctrl = createCtrlClass(debug); 32 | this.IO = createIOClass(); 33 | this.hooks = Object.create(appHooksProto); 34 | this.ctrlHooks = Object.create(ctrlHooksProto); 35 | 36 | const hooksFolderPath = `${webRootFolderPath}_hooks`; 37 | 38 | if (exists(hooksFolderPath)) { 39 | this.hooksFolderPath = hooksFolderPath; 40 | 41 | this.parseAppHooks(); 42 | } 43 | 44 | this.setRequestHandler(); 45 | 46 | const folderMap = f2j(this.webRootFolderPath); 47 | const parent = null; 48 | 49 | this.RC = new this.Ctrl(ROOT_CTRL_NAME, folderMap, parent, this); 50 | } 51 | 52 | checkIn (io) { 53 | this.RC.checkIn(io); 54 | } 55 | 56 | setRequestHandler () { 57 | if (this.hasInitHook) { 58 | this.requestHandler = (req, res) => { 59 | const io = new this.IO(req, res); 60 | 61 | io.init(this); 62 | }; 63 | } 64 | else { 65 | this.requestHandler = (req, res) => { 66 | const io = new this.IO(req, res); 67 | 68 | this.RC.checkIn(io); 69 | }; 70 | } 71 | 72 | // TODO: should expose App instance on the requestHandler function? 73 | // this.requestHandler.app = this; 74 | } 75 | 76 | parseAppHooks () { 77 | const appHooks = this.hooks; 78 | 79 | this.hooksMap = f2j(this.hooksFolderPath); 80 | 81 | forIn(this.hooksMap.entries, (rawName, map) => { 82 | const name = normalizeEntryName(rawName, map.type); 83 | 84 | if (appHooks[name]) { 85 | appHooks[name].call(this, map); 86 | } 87 | else { 88 | if (map.type === FOLDER && !map.entries['index.js']) { 89 | throw error.expectingAnIndexFile(map.path); 90 | } 91 | 92 | this[name] = require(map.path); 93 | } 94 | }); 95 | } 96 | } 97 | 98 | // ------------------ 99 | module.exports = App; 100 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/use-case.md: -------------------------------------------------------------------------------- 1 | Use case example 2 | ---------------- 3 | ``` 4 | ├── api 5 | │ ├── _in 6 | │ │ ├── index.js 7 | │ │ └── helper.js 8 | │ ├── index.js 9 | │ ├── qwe.js 10 | │ ├── _out.js 11 | │ └── A 12 | │ ├── _in.js 13 | │ ├── index.js 14 | │ ├── _pre_sub.js 15 | │ ├── _post_sub.js 16 | │ ├── _out.js 17 | │ └── B 18 | │ ├── _in.js 19 | │ ├── _before_verb 20 | │ │ ├── index.js 21 | │ │ └── helper.js 22 | │ ├── _get.js 23 | │ ├── _no_verb.js 24 | │ ├── _after_verb.js 25 | │ ├── _post_sub.js 26 | │ ├── _pre_sub.js 27 | │ ├── _out.js 28 | │ └── C 29 | │ ├── _in.js 30 | │ ├── index.js 31 | │ ├── verbs 32 | │ │ ├── get.js 33 | │ │ └── post 34 | │ │ ├── index.js 35 | │ │ └── helper.js 36 | │ └── _out.js 37 | ``` 38 | 39 | Assume all files logs their paths (full path ommited for readability): 40 | ```js 41 | module.exports = function (io) { 42 | console.log(__filename); 43 | io.next(); 44 | }; 45 | ``` 46 | 47 | ``` 48 | request: ALL `/` 49 | logs: 50 | api/_in 51 | api/index 52 | api/_out 53 | 54 | request: ALL `/qwe` 55 | logs: 56 | api/_in 57 | api/qwe 58 | api/_out 59 | 60 | request: ALL `/A` 61 | logs: 62 | api/_in 63 | api/A/_in 64 | api/A/index 65 | api/A/_out 66 | api/_out 67 | 68 | request: GET `/A/B` 69 | logs: 70 | api/_in 71 | api/A/_in 72 | api/A/_pre_sub 73 | api/A/B/_in 74 | api/A/B/_before_verb 75 | api/A/B/get 76 | api/A/B/_after_verb 77 | api/A/B/_out 78 | api/A/_post_sub 79 | api/A/_out 80 | api/_out 81 | 82 | request: POST `/A/B` 83 | logs: 84 | api/_in 85 | api/A/_in 86 | api/A/_pre_sub 87 | api/A/B/_in 88 | api/A/B/_before_verb 89 | api/A/B/no_verb 90 | api/A/B/_after_verb 91 | api/A/B/_out 92 | api/A/_post_sub 93 | api/A/_out 94 | api/_out 95 | 96 | request: GET | POST `/A/B/C` 97 | logs: 98 | api/_in 99 | api/A/_in 100 | api/A/_pre_sub 101 | api/A/B/_in 102 | api/A/B/_pre_sub 103 | api/A/B/C/_in 104 | api/A/B/C/index 105 | api/A/B/C/verbs/get|post (respectively) 106 | api/A/B/C/_out 107 | api/A/B/_post_sub 108 | api/A/B/_out 109 | api/A/_post_sub 110 | api/A/_out 111 | api/_out 112 | 113 | request: PUT & DELETE `/A/B/C` 114 | logs: 115 | api/_in 116 | api/A/_in 117 | api/A/_pre_sub 118 | api/A/B/_in 119 | api/A/B/_pre_sub 120 | api/A/B/C/_in 121 | api/A/B/C/index 122 | api/A/B/no_verb ──> delegated 123 | api/A/B/C/_out 124 | api/A/B/_post_sub 125 | api/A/B/_out 126 | api/A/_post_sub 127 | api/A/_out 128 | api/_out 129 | ``` 130 | -------------------------------------------------------------------------------- /src/ctrl/index.js: -------------------------------------------------------------------------------- 1 | const debugFNs = require('../debug'); 2 | 3 | const setID = require('./private-methods/set-id'); 4 | const parseFolderMap = require('./private-methods/parse-folder-map'); 5 | const delegateNoVerb = require('./private-methods/delegate-no-verb'); 6 | const setupChains = require('./private-methods/setup-chains'); 7 | const initSubCtrls = require('./private-methods/init-sub-ctrls'); 8 | const removeSelfName = require('./private-methods/remove-self-name'); 9 | const registerState = require('./private-methods/register-state'); 10 | const getNextFn = require('./private-methods/get-next-fn'); 11 | 12 | const {ROOT_CTRL_NAME} = require('../constants'); 13 | 14 | module.exports = function getCtrlClass (debug) { 15 | function Ctrl (name, folderMap, parent, app) { 16 | app = app || parent.app; 17 | 18 | this.isRootCtrl = name === ROOT_CTRL_NAME; 19 | 20 | this.name = name; 21 | this.folderMap = folderMap; 22 | this.parent = parent; 23 | this.app = app; 24 | this.id = setID(this); 25 | 26 | this.verbs = Object.create(null); 27 | this.methods = Object.create(app.shared_methods || null); 28 | this.subCtrls = Object.create(app.shared_ctrls || null); 29 | 30 | // TODO: register ctrls? 31 | // this.app.ctrls[this.id] = this; 32 | 33 | this.init(); 34 | } 35 | 36 | 37 | Ctrl.prototype = { 38 | constructor: Ctrl, 39 | 40 | folderMap: null, 41 | noVerb: null, 42 | chains: null, 43 | 44 | init () { 45 | parseFolderMap(this); 46 | delegateNoVerb(this); 47 | this.chains = setupChains(this); 48 | this.subCtrls = initSubCtrls(this); 49 | }, 50 | 51 | 52 | /* 53 | ┌────────────────────────────────────────────────── 54 | | remove self name from io.params 55 | | profile io 56 | | run first in chain 57 | */ 58 | checkIn (io) { 59 | !this.isRootCtrl && removeSelfName(this, io); 60 | 61 | registerState(this, io); 62 | 63 | this.next(io); 64 | }, 65 | 66 | next (io) { 67 | // set current handling ctrl 68 | io.ctrl = this; 69 | 70 | const nextFn = getNextFn(this, io); 71 | 72 | if (nextFn) { 73 | nextFn.call(this, io, ...io.params); 74 | } 75 | else { 76 | this.checkOut(io); 77 | } 78 | }, 79 | 80 | checkOut (io) { 81 | if (this.isRootCtrl) { 82 | io.exit(this.app); 83 | } 84 | else { 85 | this.parent.next(io); 86 | } 87 | }, 88 | }; 89 | 90 | const ctrlProto = Ctrl.prototype; 91 | 92 | if (debug) { 93 | ctrlProto.checkIn = debugFNs.checkInWrapper(ctrlProto.checkIn); 94 | ctrlProto.next = debugFNs.nextWrapper(ctrlProto.next); 95 | ctrlProto.checkOut = debugFNs.checkOutWrapper(ctrlProto.checkOut); 96 | } 97 | 98 | return Ctrl; 99 | }; 100 | -------------------------------------------------------------------------------- /Docs/Controller Hooks/get post put delete.md: -------------------------------------------------------------------------------- 1 | _get, _post, _put, _delete 2 | -------------------------- 3 | **Chain**: Target. 4 | 5 | >**NOTE**: In Bootstruct's docs, referred to as "\". Not to be confused with [_verbs](https://github.com/taitulism/Bootstruct/blob/entry-names/Docs/Reserved%20Entry%20Names/WebRoot/%24verbs.md) which is another reserved entry name. 6 | 7 | These 4 HTTP verbs are **currently** the only supported HTTP verbs in Bootstruct. These verb methods should hold the core functionality of their controller for [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations. 8 | 9 | Example structure: 10 | ``` 11 | ├── api 12 | │ ├── _get.js ──> GET requests to '/' 13 | │ ├── _post.js ──> POST requests to '/' 14 | │ └── A 15 | │ ├── _get.js ──> GET requests to '/A' 16 | │ └── _post.js ──> POST requests to '/A' 17 | ``` 18 | 19 | If you have an `index` method as well, it will get called **before** any verb does. That's why `index` has `_before_verb` as an alias. 20 | 21 | >**NOTE**: There's also an `_after_verb` method. 22 | 23 | ``` 24 | ├── api 25 | │ ├── index.js <── ALL requests to '/' 26 | │ ├── _get.js <── GET requests to '/' 27 | │ ├── _post.js <── POST requests to '/' 28 | │ └── A 29 | │ ├── index.js <── ALL requests to '/A' 30 | │ ├── _get.js <── GET requests to '/A' 31 | │ └── _post.js <── POST requests to '/A' 32 | ``` 33 | 34 | You'll have to call `io.next()` to make the `io` move on from the `index` method to the verb method. 35 | 36 | A `api/index.js` file like: 37 | ```js 38 | module.exports = function (io) { 39 | console.log(__filename); 40 | io.next(); 41 | }; 42 | ``` 43 | and a `api/get.js` file like: 44 | ```js 45 | module.exports = function (io) { 46 | console.log(__filename); 47 | io.res.end(); 48 | }; 49 | ``` 50 | will log (on a GET request): 51 |   path/to/api/index.js 52 |   path/to/api/_get.js 53 | 54 | If **any** reserved name file gets bigger, you can turn it into a folder. 55 | 56 | Example: 57 | ``` 58 | ├── app 59 | │ ├── index.js 60 | │ ├── _get.js 61 | │ ├── _post.js <── file 62 | │ ├── _put.js 63 | │ └── _delete.js 64 | ``` 65 | Our `post.js` file does a lot of stuff: validates, sanitizes, sends email, writes to database etc. We would probably want to do it in more than one file. We could: 66 | 67 | ``` 68 | ├── app 69 | │ ├── index.js 70 | │ ├── _get.js 71 | │ ├── _post <── folder 72 | │ │ ├── index.js 73 | │ │ ├── dependency_1.js 74 | │ │ └── dependency_2.js 75 | │ ├── _put.js 76 | │ └── _delete.js 77 | ``` 78 | Just remember to export your function from an `index.js` file. In this case the `index.js` is NOT parsed as the reserved entry name, it's just what Node is looking for when `require`-ing a folder. From that `index.js` file you can `require` anything. 79 | -------------------------------------------------------------------------------- /tests/specs/creation-errors.spec.js: -------------------------------------------------------------------------------- 1 | const {expect} = require('chai'); 2 | 3 | const bts = require('../../'); 4 | 5 | describe('Creation errors', function () { 6 | it('throws when web-root-folder path doesn\'t exist', function () { 7 | function failedApp () { 8 | bts('./not-exist'); 9 | } 10 | 11 | expect(failedApp).to.throw('find the web-root folder'); 12 | }); 13 | 14 | it('throws when an app hook folder doesn\'t contain an "index.js" file', function () { 15 | function failedApp () { 16 | bts('./tests/app-folders/creation-errors/app-hook-folder-no-index/www'); 17 | } 18 | 19 | expect(failedApp).to.throw('Expecting an "index.js" file'); 20 | }); 21 | 22 | it('throws when an `ignore` hook is not a string', function () { 23 | function failedApp () { 24 | bts('./tests/app-folders/creation-errors/ignore-not-string/www'); 25 | } 26 | 27 | expect(failedApp).to.throw('a string or an array of strings'); 28 | }); 29 | 30 | it('throws when an `ignore` hook item is not a string', function () { 31 | function failedApp () { 32 | bts('./tests/app-folders/creation-errors/ignore-item-not-string/www'); 33 | } 34 | 35 | expect(failedApp).to.throw('a string or an array of strings'); 36 | }); 37 | 38 | it('throws when a file and a folder share the same name (in the web-root folder)', function () { 39 | function failedApp () { 40 | bts('./tests/app-folders/creation-errors/file-folder-same-name/www'); 41 | } 42 | 43 | expect(failedApp).to.throw('a controller and a method with the same name'); 44 | }); 45 | 46 | it('throws when a controller method has no params', function () { 47 | function failedApp () { 48 | bts('./tests/app-folders/creation-errors/method-no-params/www'); 49 | } 50 | 51 | expect(failedApp).to.throw('must handle at least one param'); 52 | }); 53 | 54 | it('throws when an `io_init` hook in use and has no params', function () { 55 | function failedApp () { 56 | bts('./tests/app-folders/creation-errors/no-params-io-init/www'); 57 | } 58 | 59 | expect(failedApp).to.throw('"io.init" function must handle an argument'); 60 | }); 61 | 62 | it('throws when an `io_init` hook is not a function', function () { 63 | function failedApp () { 64 | bts('./tests/app-folders/creation-errors/io-init-must-be-function/www'); 65 | } 66 | 67 | expect(failedApp).to.throw('"io_init" expected to be a function'); 68 | }); 69 | 70 | it('throws when an `io_exit` hook is not a function', function () { 71 | function failedApp () { 72 | bts('./tests/app-folders/creation-errors/io-exit-must-be-function/www'); 73 | } 74 | 75 | expect(failedApp).to.throw('"io_exit" expected to be a function'); 76 | }); 77 | 78 | it('throws when `shared_ctrl` hook is not a folder', function () { 79 | function failedApp () { 80 | bts('./tests/app-folders/creation-errors/shared-ctrl-not-folder/www'); 81 | } 82 | 83 | expect(failedApp).to.throw('Controllers must be folders'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/utils/try-require.js: -------------------------------------------------------------------------------- 1 | const isArray = require('./is-array'); 2 | const log = require('./log'); 3 | 4 | const BTS_ERR = 'Bootstruct Error:'; 5 | 6 | module.exports = { 7 | tryRequireFn, 8 | tryRequireObj, 9 | getFn, 10 | getObj, 11 | }; 12 | 13 | 14 | function getFn (path) { 15 | const {content, err} = tryRequire(path); 16 | const type = typeof content; 17 | 18 | if (!content) { 19 | return { 20 | err, 21 | content: null, 22 | }; 23 | } 24 | 25 | if (type !== 'function') { 26 | return { 27 | err: new Error(`This entry is expected to export a single function. Got: ${type}`), 28 | content: null, 29 | }; 30 | } 31 | 32 | return { 33 | content, 34 | err: null, 35 | }; 36 | } 37 | 38 | function isObj (thing) { 39 | return typeof thing === 'object' && thing !== null && !isArray(thing); 40 | } 41 | 42 | function getObj (path) { 43 | const {content, err} = tryRequire(path); 44 | const type = typeof content; 45 | 46 | if (!content) { 47 | log(BTS_ERR, [ 48 | `${path}`, 49 | `Failed to require ${path}`, 50 | ]); 51 | log(err.message); 52 | 53 | return false; 54 | } 55 | 56 | if (!isObj(content)) { 57 | log(BTS_ERR, [ 58 | `${path}`, 59 | `This entry is expected to export an object. Got: ${type}`, 60 | ]); 61 | 62 | return false; 63 | } 64 | 65 | return content; 66 | } 67 | 68 | 69 | function tryRequire (path) { 70 | if (typeof path !== 'string') { 71 | const type = typeof path; 72 | 73 | return { 74 | content: null, 75 | err: new Error(`tryRequire(path): 'path' should be a string. Got: ${type}`), 76 | }; 77 | } 78 | 79 | try { 80 | return { 81 | content: require(path), 82 | err: null, 83 | }; 84 | } 85 | catch (exception) { 86 | return { 87 | content: null, 88 | err: exception, 89 | }; 90 | } 91 | } 92 | 93 | /* 94 | ┌────────────────────────────────────────────────── 95 | │ validate a require(path) returns a function 96 | │ returns: module | null (valid|invalid) 97 | */ 98 | function tryRequireFn (path) { 99 | const req = require(path); 100 | const type = typeof req; 101 | 102 | if (type === 'function') { 103 | return req; 104 | } 105 | 106 | return expectedAFunction(path, type); 107 | } 108 | 109 | /* 110 | ┌────────────────────────────────────────── 111 | │ validate a require(path) returns an object 112 | │ returns: module | null (valid|invalid) 113 | */ 114 | function tryRequireObj (path) { 115 | const req = require(path); 116 | const type = typeof req; 117 | 118 | // typeof null == 'object'. it would count as invalid 119 | if (type === 'object' && !isArray(req)) { 120 | return req; 121 | } 122 | 123 | return expectedAnObject(path, type); 124 | } 125 | 126 | 127 | function expectedAFunction (path, type) { 128 | log(BTS_ERR, [ 129 | `${path}`, 130 | `This entry is expected to export a single function. Got: ${type}`, 131 | ]); 132 | 133 | return null; 134 | } 135 | 136 | function expectedAnObject (path, type) { 137 | log(BTS_ERR, [ 138 | `${path}`, 139 | `This entry is expected to export an object. Got: ${type}`, 140 | ]); 141 | 142 | return null; 143 | } 144 | -------------------------------------------------------------------------------- /tests/specs/target-chain.spec.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const {expect} = require('chai'); 3 | 4 | const bts = require('../../'); 5 | const makeRequest = require('../make-request'); 6 | 7 | describe('Target-chain test', function () { 8 | const app = bts('./tests/app-folders/Target'); 9 | const server = http.createServer(app); 10 | 11 | beforeEach(function () { 12 | server.listen(8181, '127.0.0.1'); 13 | }); 14 | afterEach(function () { 15 | server.close(); 16 | }); 17 | 18 | it('should pass', function (done) { 19 | makeRequest('GET', '/', (body) => { 20 | expect(body).to.equal('b4getftr'); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('should pass', function (done) { 26 | makeRequest('POST', '/', (body) => { 27 | expect(body).to.equal('b4ftr'); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should pass', function (done) { 33 | makeRequest('GET', '/a', (body) => { 34 | expect(body).to.equal('b41nvaftr1'); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should pass', function (done) { 40 | makeRequest('POST', '/a', (body) => { 41 | expect(body).to.equal('b41postftr1'); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should pass', function (done) { 47 | makeRequest('GET', '/a/b', (body) => { 48 | expect(body).to.equal('b42nvbftr2'); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('should pass', function (done) { 54 | makeRequest('PUT', '/a/b', (body) => { 55 | expect(body).to.equal('b42putftr2'); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should pass', function (done) { 61 | makeRequest('GET', '/a/b/c', (body) => { 62 | expect(body).to.equal('b43nvcftr3'); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should pass', function (done) { 68 | makeRequest('DELETE', '/a/b/c', (body) => { 69 | expect(body).to.equal('b43delftr3'); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should pass', function (done) { 75 | makeRequest('GET', '/a/b/c/d', (body) => { 76 | expect(body).to.equal('b44getftr4'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('should pass', function (done) { 82 | makeRequest('POST', '/a/b/c/d', (body) => { 83 | expect(body).to.equal('b44postftr4'); 84 | done(); 85 | }); 86 | }); 87 | 88 | it('should pass', function (done) { 89 | makeRequest('PUT', '/a/b/c/d', (body) => { 90 | expect(body).to.equal('b44nvcftr4'); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('should pass', function (done) { 96 | makeRequest('GET', '/a/b/c/d/e', (body) => { 97 | expect(body).to.equal('getftr5'); 98 | done(); 99 | }); 100 | }); 101 | 102 | it('should pass', function (done) { 103 | makeRequest('POST', '/a/b/c/d/e', (body) => { 104 | expect(body).to.equal('postftr5'); 105 | done(); 106 | }); 107 | }); 108 | 109 | it('should pass', function (done) { 110 | makeRequest('PUT', '/a/b/c/d/e', (body) => { 111 | expect(body).to.equal('putftr5'); 112 | done(); 113 | }); 114 | }); 115 | 116 | it('should pass', function (done) { 117 | makeRequest('DELETE', '/a/b/c/d/e', (body) => { 118 | expect(body).to.equal('nvcftr5'); 119 | done(); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 2 | [![Build Status](https://travis-ci.org/taitulism/Bootstruct.svg?branch=master)](https://travis-ci.org/taitulism/Bootstruct) 3 | 4 | Bootstruct 5 | ========== 6 | Bootstruct is a web framework for Node, based on a folder-structure / file-name convention. 7 | 8 | >*Routing by structure.* 9 | 10 | 11 | 12 | Table of contents 13 | ----------------- 14 | 15 | * Overview (this page) 16 | * [Get Started](https://github.com/taitulism/Bootstruct/blob/master/Docs/Get%20Started.md) 17 | * [Docs](https://github.com/taitulism/Bootstruct/blob/master/Docs/README.md) 18 | 19 | 20 | 21 | 22 | 23 | Overview 24 | -------- 25 | Creating web apps with Node requires wiring up our routes, we need to bind different URL paths to their handlers. We usually do that by declerative coding. Something like: `bind('GET', '/api/books', handler)`. 26 | 27 | With Bootstruct you don't code your routes. You just export your handler and name the file with its route name. 28 | 29 | Technically, Bootstruct creates routes by parsing your routes folder and routes requests through that folder's structure, matching URLs to corresponding paths under that folder. 30 | 31 | Meaning, to support routes like: 32 | ``` 33 | domain.com/ 34 | domain.com/A 35 | domain.com/A/B 36 | domain.com/A/B/C 37 | ``` 38 | 39 | your routes folder tree would generally look like: 40 | ``` 41 | ├── routes 42 | │ ├── index.js 43 | │ └── A 44 | │ ├── index.js 45 | │ └── B 46 | │ ├── index.js 47 | │ └── C 48 | │ └──index.js 49 | ``` 50 | 51 |   52 | 53 | Controling Request Flow 54 | ----------------------- 55 | When working with middlware functions (`express`, `connect`...) you control request flow by binding 'this' route before 'that' route. The order in which you code your routes matters. 56 | 57 | Bootstruct provides you with an onion-like layered app by leveraging the parental folder chain. So a request to `/A/B/C` would go through: 58 | ``` 59 | 1. / 60 | 2. /A 61 | 3. /A/B 62 | 4. /A/B/C 63 | 5. /A/B 64 | 6. /A 65 | 7. / 66 | ``` 67 | >Do you see the onion? 68 | 69 |   70 | 71 | Naming Convention 72 | ----------------- 73 | Bootstruct uses files and folders with certain names as different hooks. 74 | 75 | For example, to handle `GET` requests, name your handler file `_get.js`. To handle `POST` requests, name it `_post.js`. 76 | 77 | >**You can create your own hooks** 78 | 79 |   80 | 81 | What Else? 82 | ---------- 83 | * Create your own hook 84 | * Handle dynamic url params (e.g. `/A/B/whatever`) 85 | * 86 | * Extend Bootstruct's different prototypes and more. 87 | 88 |   89 | 90 | Bootstruct: 91 | ----------- 92 | - [x] saves you from coding your routes. 93 | - [x] enforces a natural code separation by concept. 94 | - [x] provides you with great control over request flow. 95 | - [x] is extensible. 96 | 97 |   98 | 99 | ### Start Using Bootstruct 100 | * [Get Started](https://github.com/taitulism/Bootstruct/blob/master/Docs/Get%20Started.md) 101 | * [Docs](https://github.com/taitulism/Bootstruct/blob/master/Docs/README.md) 102 | 103 | 104 | 105 | 106 | ******************************************************************************* 107 | Questions, suggestions, criticism, bugs, hugs, typos and kudos are all welcome. 108 | 109 | *taitu.dev (at) gmail dot com* 110 | -------------------------------------------------------------------------------- /src/debug.js: -------------------------------------------------------------------------------- 1 | const $path = require('path'); 2 | 3 | const log = require('./utils/log'); 4 | const removeExt = require('./utils/remove-extension'); 5 | const getNextFn = require('./ctrl/private-methods/get-next-fn'); 6 | 7 | const getFileName = $path.basename; 8 | const resolvePath = $path.resolve; 9 | 10 | const SPACE = ' '; 11 | const SPACE_MULTIPLIER = 2; 12 | 13 | module.exports = { 14 | checkInWrapper, 15 | nextWrapper, 16 | checkOutWrapper, 17 | }; 18 | 19 | function checkInWrapper (checkIn) { 20 | return function checkInDebug (io) { 21 | if (!this.parent) { 22 | debugRequest(io); 23 | } 24 | 25 | debugCtrlName(this); 26 | 27 | checkIn.call(this, io); 28 | }; 29 | } 30 | 31 | const DO_NOT_INCREMENT_INDEX = false; 32 | 33 | function nextWrapper (next) { 34 | return function nextDebug (io) { 35 | const nextInChain = getNextFn(this, io, DO_NOT_INCREMENT_INDEX); 36 | 37 | if (nextInChain) { 38 | debugNext(this, io, nextInChain); 39 | } 40 | 41 | next.call(this, io); 42 | }; 43 | } 44 | 45 | function checkOutWrapper (checkOut) { 46 | return function checkOutDebug (io) { 47 | this.parent && debugCtrlName(this.parent); 48 | 49 | checkOut.call(this, io); 50 | }; 51 | } 52 | 53 | function debugRequest (io) { 54 | const {req} = io; 55 | 56 | log(); 57 | log(`Request: ${req.method} ${req.url}`); 58 | log('========'); 59 | } 60 | 61 | function debugCtrlName (ctrl) { 62 | const appName = ctrl.app.webRootFolderName; 63 | 64 | const indent = getCtrlIndentLevel(ctrl); 65 | const spaces = getSpacesByIndent(indent); 66 | 67 | ctrl._debugIndentSpaces = spaces; 68 | 69 | const isShared = ctrl.isSharedCtrl 70 | ? '(shared ctrl)' 71 | : ''; 72 | 73 | log(`${spaces}${appName}${ctrl.id} ${isShared}`); 74 | } 75 | 76 | function getCtrlIndentLevel (ctrl) { 77 | if (!ctrl.parent) { 78 | return 0; 79 | } 80 | 81 | const ctrlId = ctrl.id; 82 | const idParts = ctrlId.split('/'); 83 | 84 | return idParts.length - 1; 85 | } 86 | 87 | function getSpacesByIndent (indent) { 88 | const totalIndent = indent * SPACE_MULTIPLIER; 89 | 90 | let spaces = ''; 91 | 92 | for (let index = 0; index < totalIndent; index += 1) { 93 | spaces += SPACE; 94 | } 95 | 96 | return spaces; 97 | } 98 | 99 | function debugNext (ctrl, io, nextInChain) { 100 | const {app} = ctrl; 101 | const nextName = nextInChain.name; 102 | const reqMethod = io.req.method.toLowerCase(); 103 | const nextVerb = ctrl.verbs[reqMethod]; 104 | 105 | if (nextInChain.path) { 106 | debugHandlerPath(app, ctrl, nextInChain.path); 107 | } 108 | else if (nextName) { 109 | if (nextName === 'verb' && nextVerb) { 110 | debugHandlerPath(app, ctrl, nextVerb.path); 111 | } 112 | else if (nextName === 'method') { 113 | const [methodName] = io.params; 114 | 115 | let methodPath = ctrl.methods[methodName].path; 116 | 117 | if (!methodPath) { 118 | methodPath = getSharedMethodPath(methodName, app); 119 | } 120 | 121 | debugHandlerPath(app, ctrl, methodPath); 122 | } 123 | } 124 | } 125 | 126 | function debugHandlerPath (app, ctrl, path) { 127 | const spaces = `${ctrl._debugIndentSpaces}${SPACE}${SPACE}`; 128 | 129 | const filename = getFileName(path); 130 | const withoutExtension = removeExt(filename); 131 | 132 | log(`${spaces}${withoutExtension}`); 133 | } 134 | 135 | function getSharedMethodPath (methodName, app) { 136 | const sharedMethodPath = resolvePath(app.hooksFolderPath, 'shared_methods', methodName); 137 | 138 | return sharedMethodPath; 139 | } 140 | -------------------------------------------------------------------------------- /src/ctrl/private-methods/run.js: -------------------------------------------------------------------------------- 1 | const getLoweredFirstParam = require('./get-lowered-first-param'); 2 | const isFunction = require('../../utils/is-function'); 3 | 4 | 5 | /* 6 | ┌───────────────────────────────────────────────────────────────────── 7 | │ run = { 8 | │ .verb() - on request (in chain) 9 | │ .method() - on request (in chain) 10 | │ .subCtrl() - on request (in chain) 11 | │ } 12 | │ all 3 are being pushed to chains and called with: .call(this, io, ...io.params) 13 | */ 14 | module.exports = { 15 | verb (io) { 16 | const reqMethod = io.req.method.toLowerCase(); 17 | const verbFn = this.verbs[reqMethod]; 18 | 19 | if (isFunction(verbFn)) { 20 | verbFn.call(this, io, ...io.params); 21 | } 22 | else if (isFunction(this.noVerb)) { 23 | this.noVerb(io, ...io.params); 24 | } 25 | else { 26 | this.next(io); 27 | } 28 | }, 29 | 30 | method (io) { 31 | const next = getLoweredFirstParam(io); 32 | const method = this.methods[next]; 33 | 34 | if (isFunction(method)) { 35 | const ioParams = io.params; 36 | 37 | // remove first item like in ctrl.check-in 38 | ioParams.shift(); 39 | 40 | const methodParams = method.params; 41 | const runParams = methodParams 42 | ? matchParams(methodParams, ioParams) 43 | : ioParams; 44 | 45 | method.call(this, io, ...runParams); 46 | } 47 | }, 48 | 49 | subCtrl (io) { 50 | const next = getLoweredFirstParam(io); 51 | const subCtrl = this.subCtrls[next]; 52 | 53 | if (subCtrl) { 54 | subCtrl.parent = this; 55 | subCtrl.checkIn(io); 56 | } 57 | }, 58 | }; 59 | 60 | 61 | /* 62 | ┌───────────────────────────────────────────────────────────────────────────── 63 | │ smart matching params for key-value urls (e.g. /bookId/13/chapter/22). 64 | │ returns a new params array, based on method.params (init) & io.params (request). 65 | │ it loops over the method's params (e.g. function (a, b, c)...) 66 | │ if a param found in request - push its value. 67 | │ single values like "bla" in "/a/1/bla/b/2" will be pushed last. 68 | │ each key-value pair is removed from the "workingCopy" (a copy of io.params array) 69 | │ until all there is left in the "workingCopy" are the single values. 70 | │ 71 | │ without param matching: 72 | │ method: function (io, bookId, value_1, chapter, value_2) { 73 | │ // bookId = 'bookId' (always) 74 | │ // value_1 = 13 (dynamic) 75 | │ // chapter = 'chapter' (always) 76 | │ // value_2 = 22 (dynamic) 77 | │ } 78 | │ 79 | │ with param matching: 80 | │ method: function (io, $bookId, $chapter) { 81 | │ // $bookId = 13 82 | │ // $chapter = 22 83 | │ } 84 | */ 85 | function matchParams (methodParams, ioParams) { 86 | // a copy of io.params 87 | const workingCopy = ioParams.slice(0); 88 | 89 | // the returned value 90 | const params = []; 91 | 92 | const len = methodParams.length; 93 | 94 | for (let i = 0; i < len; i += 1) { 95 | const param = methodParams[i]; 96 | const isRequested = ioParams.includes(param); 97 | 98 | if (isRequested) { 99 | // index in io.params 100 | const index = workingCopy.indexOf(param); 101 | const next = workingCopy[index + 1]; 102 | 103 | if (next) { 104 | const nextIsTheValue = !methodParams.includes(next); 105 | 106 | if (nextIsTheValue) { 107 | params.push(next); 108 | workingCopy.splice(index, 2); 109 | } 110 | else { 111 | params.push(true); 112 | workingCopy.splice(index, 1); 113 | } 114 | } 115 | else { 116 | params.push(true); 117 | workingCopy.splice(index, 1); 118 | } 119 | } 120 | else { 121 | params.push(null); 122 | } 123 | } 124 | 125 | return [...params, ...workingCopy]; 126 | } 127 | -------------------------------------------------------------------------------- /Docs/Get Started.md: -------------------------------------------------------------------------------- 1 | Get Started 2 | =========== 3 | 4 | ### 1. Install locally 5 | ```sh 6 | $ npm install --save bootstruct 7 | ``` 8 | 9 | ### 2. Create a server and initialize Bootstruct app 10 | Your server's `index.js` file: 11 | ```js 12 | const http = require('http'); 13 | const bootstruct = require('bootstruct'); 14 | 15 | const app = bootstruct(folderPath); // <-- web root folder 16 | 17 | http.createServer(app).listen(8080, () => { 18 | console.log('Listening on port 8080...'); 19 | }); 20 | ``` 21 | 22 | ### 3. Create your web root folder 23 | The web root folder holds your entire API. 24 | Pass whatever name you pick to the `bootstruct()` call above. 25 | 26 | > Suggested names: `routes` | `api` | `app` | `www`. 27 | 28 | We'll be using the name **"api"** for the rest of this tutorial. 29 | 30 |   31 | 32 | This is how your project folder tree should look like: 33 | ``` 34 | ├── myProject 35 | │ ├── node_modules 36 | │ ├── api 37 | │ └── index.js 38 | ``` 39 | 40 | ### **This is our starting point for this tutorial.** 41 | Now let's create our first route. 42 | 43 |   44 | 45 | 46 | "Hello World" 47 | ------------- 48 | Create an `index.js` file in your web root folder `api`. 49 | ``` 50 | ├── api 51 | │ └── index.js 52 | ``` 53 | 54 | Put the following in that `index.js`: 55 | ```js 56 | module.exports = function (io) { 57 | io.res.end('hello world'); 58 | }; 59 | ``` 60 | 61 | Start your server up: 62 | ```sh 63 | $ node index.js 64 | ``` 65 | and open your browser in `http://localhost:8080`. 66 | 67 | Your `api` folder becomes your app's root-controller and `index.js` is its only method so ANY request will be responded with "hello world". 68 | 69 | The `io` argument is an object that holds Node's native `request`/`response` objects you probably know. 70 | 71 | Method 72 | ------ 73 | Let's add another method (a file): `greet.js` 74 | ``` 75 | ├── api 76 | │ ├── greet.js 77 | │ └── index.js 78 | ``` 79 | 80 | `api/greet.js` contents: 81 | ```js 82 | module.exports = function (io, who) { 83 | if (who) { 84 | io.res.end('Hey ' + who); 85 | } 86 | else { 87 | io.res.end('Hello everyone'); 88 | } 89 | }; 90 | ``` 91 | 92 | | Request | Response | 93 | |------------|----------------| 94 | |/greet | Hello everyone | 95 | |/greet/john | Hey john | 96 | 97 | 98 | Our "api" controller now has another method named "greet". 99 | 100 | Hooks 101 | ----- 102 | Now let's create a file named "_in.js": 103 | ``` 104 | ├── api 105 | │ ├── _in.js 106 | │ ├── greet.js 107 | │ └── index.js 108 | ``` 109 | 110 | `api/_in.js` contents: 111 | ```js 112 | module.exports = function (io) { 113 | io.res.write('Yay! '); 114 | io.next(); 115 | }; 116 | ``` 117 | 118 | | Request | Response | 119 | |------------|---------------------| 120 | |/ | Yay! hello world | 121 | |/whatever | Yay! hello world | 122 | |/_in | Yay! hello world | 123 | |/greet | Yay! hello everyone | 124 | |/greet/john | Yay! hey john | 125 | 126 | 127 | `_in` is one of Bootstruct's hooks. Its exported function will run before the other two (`index` and `greet`). This is why all the responses start with "Yay! ". Because `_in` is a reserved name, it won't be parsed as a method like `greet` so requesting `/_in` will be handled by `api/index.js` just like requesting `/whatever`. 128 | 129 | `io.next()` is called to move the `io` forward in the chain, from one handler (_in) to the next one (index or greet). You call it at the end of your methods. 130 | 131 | 132 | 133 | 134 | Debugging 135 | --------- 136 | Bootstruct can be initiated with a second argument, a debug-mode flag (default: false). Start your app with a second truthy argument for console logs of the `io`'s different checkpoints along its way. 137 | ```js 138 | const app = bootstruct('api', true); 139 | ``` 140 | 141 | 142 | What's next? 143 | ------------ 144 | This page covered Bootstruct's basics, but there's more. 145 | 146 | [Read The Fabulous Manual](https://github.com/taitulism/Bootstruct/blob/master/Docs/README.md). 147 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | const log = require('./utils/log'); 2 | 3 | module.exports = { 4 | webRootFolderNotFound, 5 | ignoreItemIsNotAString, 6 | cannotIgnoreReservedEntryName, 7 | expectingAnIndexFile, 8 | fileAndFolderNameCollision, 9 | methodsWithNoParams, 10 | expectedFunctionArgument, 11 | ioInitExpectedAFunction, 12 | ioExitExpectedAFunction, 13 | sharedCtrlExpectedAFolder, 14 | objectKeyAlreadyExists, 15 | hookFunctionExpected, 16 | }; 17 | 18 | const BTS = 'Bootstruct'; 19 | const ERROR = 'Error'; 20 | const BTS_ERR = `${BTS} ${ERROR}:`; 21 | 22 | // notString.toString() max chars 23 | const MAX_CHARS = 50; 24 | 25 | // const RAWESC = '\x1b'; 26 | const UTF8ESC = '\u001b'; 27 | const ESC = UTF8ESC; 28 | const FgRed = `${ESC}[31m`; 29 | const FgGreen = `${ESC}[32m`; 30 | const Reset = `${ESC}[0m`; 31 | 32 | function createLogMessage (type, lines) { 33 | const TITLE = `\n ${FgRed}${BTS} ${type}:${Reset} `; 34 | 35 | if (typeof lines === 'string') { 36 | return `${TITLE}\n${lines}\n`; 37 | } 38 | const joined = lines.join('\n '); 39 | 40 | return `${TITLE}\n ${FgGreen}${joined}${Reset}\n`; 41 | } 42 | 43 | function webRootFolderNotFound (path) { 44 | const logText = createLogMessage(ERROR, [ 45 | 'Bootstruct couldn\'t find the web-root folder:', 46 | `${path}`, 47 | ]); 48 | 49 | return new Error(logText); 50 | } 51 | 52 | function ignoreItemIsNotAString (item) { 53 | item = item.toString(); 54 | 55 | if (item.length > MAX_CHARS) { 56 | const trimmed = item.substr(0, MAX_CHARS); 57 | 58 | item = `${trimmed}...`; 59 | } 60 | 61 | const logText = createLogMessage(ERROR, [ 62 | '"ignore" hook handler should export a string or an array of strings.', 63 | `Got: "${item}"`, 64 | ]); 65 | 66 | return new Error(logText); 67 | } 68 | 69 | function expectingAnIndexFile (path) { 70 | const logText = createLogMessage(ERROR, [ 71 | 'Expecting an "index.js" file in:', 72 | `${path}`, 73 | ]); 74 | 75 | return new Error(logText); 76 | } 77 | 78 | function fileAndFolderNameCollision (duplicatedNames, ctrlPath) { 79 | const logText = createLogMessage(ERROR, [ 80 | 'You have a controller and a method with the same name:', 81 | `(${duplicatedNames.join(', ')}) in: ${ctrlPath}`, 82 | ]); 83 | 84 | return new Error(logText); 85 | } 86 | 87 | function methodsWithNoParams (path) { 88 | const logText = createLogMessage(ERROR, [ 89 | 'Methods must handle at least one param (io)', 90 | `${path}`, 91 | ]); 92 | 93 | return new Error(logText); 94 | } 95 | 96 | function expectedFunctionArgument () { 97 | const logText = createLogMessage(ERROR, [ 98 | '"io.init" function must handle an argument: "app".', 99 | 'You should call "app.checkIn(this)" when the function is done.', 100 | ]); 101 | 102 | return new Error(logText); 103 | } 104 | 105 | function ioInitExpectedAFunction (err) { 106 | const logText = createLogMessage(ERROR, [ 107 | '"io_init" expected to be a function.', 108 | err.message, 109 | ]); 110 | 111 | return new Error(logText); 112 | } 113 | 114 | function ioExitExpectedAFunction (err) { 115 | const logText = createLogMessage(ERROR, [ 116 | '"io_exit" expected to be a function.', 117 | err.message, 118 | ]); 119 | 120 | return new Error(logText); 121 | } 122 | 123 | function sharedCtrlExpectedAFolder (path) { 124 | const logText = createLogMessage(ERROR, [ 125 | '"shared_ctrls" - Controllers must be folders:', 126 | `${path}`, 127 | ]); 128 | 129 | return new Error(logText); 130 | } 131 | 132 | 133 | function objectKeyAlreadyExists (objName, key) { 134 | log(BTS_ERR, [ 135 | ` Name collision on "${objName}" object.`, 136 | ` A prop/method named: "${key}" is already exists.`, 137 | ' Use another name.', 138 | ]); 139 | 140 | return false; 141 | } 142 | 143 | function hookFunctionExpected (objName, key) { 144 | log(BTS_ERR, [ 145 | ` Expecting "${key}" to be a function.`, 146 | ` For: "${objName}" object.`, 147 | ]); 148 | 149 | return false; 150 | } 151 | 152 | function cannotIgnoreReservedEntryName (item) { 153 | log(BTS_ERR, [ 154 | '"ignore" hook: Trying to ignore a reserved entry name.', 155 | `skipping: "${item}"`, 156 | ]); 157 | 158 | return false; 159 | } 160 | -------------------------------------------------------------------------------- /Docs/Extending Bootstruct.md: -------------------------------------------------------------------------------- 1 | Extending Bootstruct 2 | ==================== 3 | Bootstruct provides you with hooks to some key points in its architecture. 4 | These hooks allow you to create your own API over Bootstruct's infrastructure. 5 | 6 | * You can add methods to the `Ctrl`'s prototype and the `IO` prototype. Create a `this.kick(io)` or a `io.getIP()` methods. 7 | 8 | * You can load your own stuff on your `app` instance and access them from your controllers and methods (`this.app`). A database connection, a reference to the server, a log-to-file function or whatever. 9 | 10 | * You can add your own [Hooks](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks/README.md) on the parser. For example, precompile "views" folders. 11 | 12 | * You can create shared methods and shared controllers instead of copy-pasting the same files in every folder (when you need the same functionality in more than one place). A shared `help` method/controller will allow: `/any-url/help`). 13 | 14 | * You can run some code on `io` initialization. This is the very first thing to run on each request, before the `io` checks-in at your app. Set some request related props like `io.isAuthorized` or `io.isIdle` ready for use in your methods. You can create methods on the `io.prototype` (another hook) and invoke them on `io` initialization. 15 | 16 | * You can run some code when the `io` checks-out from your app and choose what to do at the end of the request cycle (end the response? log it? pass it to another framework?). 17 | 18 | With these hooks you can create yourself your own set of tools and use Bootstruct as a platform or infrastructure with your own syntax. 19 | 20 | Read more about [Bootstruct Hooks](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md). 21 | 22 | 23 | Bootstruct Classes 24 | ------------------ 25 | The three main classes are `App`, `Ctrl` and `IO`. 26 | 27 | The story, in short: 28 | * The `api` web root folder is an office building with different departments (`controllers`) and sub-departments. 29 | * Client requests are the visitors (`IO`s) checking in and out at this office. 30 | * All departments have access to the building infrastructure (the `app` object). 31 | 32 | So controllers are nested inside each other and they all have a reference to the app object. `IO`s check in and out at this structure. 33 | 34 | 35 | The App Hooks Folder 36 | -------------------- 37 | Up until now it was all about the web root folder, `api`. Bootstruct app level hooks are put in another folder. On init Bootstruct looks for a 38 | folder whose name is like your web root folder's name with a trailing: "**_hooks**" in its name. If your web root folder is `api`, Bootstruct will 39 | look for a folder named `api_hooks`: 40 | ``` 41 | ├── myProject 42 | │ ├── node_modules 43 | │ ├── server-index.js 44 | │ ├── api 45 | │ └── api_hooks <── 46 | ``` 47 | 48 | 49 | 50 | Extend your App 51 | --------------- 52 | The hooks folder is parsed when Bootstruct is initialized BEFORE the web root folder. You use hooks by creating entries in the hooks folder and 53 | naming them with certain names. 54 | 55 | By default, any entry in your hooks folder with a non-reserved name (e.g. `WhatEver.js`) will be `require`-d as a property on 56 | your `app` instance and you could access it by using `this.app` in your methods. If the entry is a file (e.g. `WhatEver.js`), its 57 | extension (`.js`) will be ommited from the prop name: 58 | ``` js 59 | // this is kind of what's going on behind the scenes: 60 | app = { 61 | WhatEver: require('api_hooks/WhatEver'); 62 | } 63 | ``` 64 | 65 | If the entry is a folder, make sure to include an `index.js` file within. 66 | ``` 67 | ├── myProject 68 | │ ├── node_modules 69 | │ ├── server-index.js 70 | │ ├── api 71 | │ └── api_hooks 72 | │ └── WhatEver <── 73 | │ └── index.js 74 | ``` 75 | This is how you extend your `app` instance. It loads props on your app's main object,. Use it for properties and methods you need access to, from anywhere in 76 | your app: a database connection, log methods, error methods or whatever you'd like. 77 | 78 | Here are the rest of Bootstruct's hooks (click to read about): 79 | * [ignore](./App%20Hooks/ignore.md) 80 | * [io_init](./App%20Hooks/io_init.md) 81 | * [io_exit](./App%20Hooks/io_exit.md) 82 | * [io_proto](./App%20Hooks/io_proto.md) 83 | * [ctrl_proto](./App%20Hooks/ctrl_proto.md) 84 | * [ctrl_hooks](./App%20Hooks/ctrl_hooks.md) 85 | * [shared_methods](./App%20Hooks/shared_methods.md) 86 | * [shared_ctrls](./App%20Hooks/shared_ctrls.md) 87 | -------------------------------------------------------------------------------- /src/ctrl/hooks.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | const {FILE, FOLDER} = require('../constants'); 4 | const {tryRequireFn, tryRequireObj} = require('../utils/try-require'); 5 | const { 6 | normalizeEntryName, 7 | forIn, 8 | isFunction, 9 | } = require('../utils'); 10 | 11 | 12 | const DONT_REMOVE_EXT = false; 13 | 14 | const ctrlHooks = { 15 | _in (entryMap) { 16 | this.in = tryRequireFn(entryMap.path); 17 | this.in.path = entryMap.path; 18 | }, 19 | index (entryMap) { 20 | this.index = tryRequireFn(entryMap.path); 21 | this.index.path = entryMap.path; 22 | }, 23 | _before_verb (entryMap) { 24 | this.index = tryRequireFn(entryMap.path); 25 | this.index.path = entryMap.path; 26 | }, 27 | '_before-verb' (entryMap) { 28 | this.index = tryRequireFn(entryMap.path); 29 | this.index.path = entryMap.path; 30 | }, 31 | _get (entryMap) { 32 | this.verbs.get = tryRequireFn(entryMap.path); 33 | this.verbs.get.path = entryMap.path; 34 | }, 35 | _post (entryMap) { 36 | this.verbs.post = tryRequireFn(entryMap.path); 37 | this.verbs.post.path = entryMap.path; 38 | }, 39 | _put (entryMap) { 40 | this.verbs.put = tryRequireFn(entryMap.path); 41 | this.verbs.put.path = entryMap.path; 42 | }, 43 | _delete (entryMap) { 44 | this.verbs.delete = tryRequireFn(entryMap.path); 45 | this.verbs.delete.path = entryMap.path; 46 | }, 47 | '_no-verb' (entryMap) { 48 | this.noVerb = tryRequireFn(entryMap.path); 49 | this.noVerb.path = entryMap.path; 50 | }, 51 | _no_verb (entryMap) { 52 | this.noVerb = tryRequireFn(entryMap.path); 53 | this.noVerb.path = entryMap.path; 54 | }, 55 | _after_verb (entryMap) { 56 | this.afterVerb = tryRequireFn(entryMap.path); 57 | this.afterVerb.path = entryMap.path; 58 | }, 59 | '_after-verb' (entryMap) { 60 | this.afterVerb = tryRequireFn(entryMap.path); 61 | this.afterVerb.path = entryMap.path; 62 | }, 63 | _pre_method (entryMap) { 64 | this.preMethod = tryRequireFn(entryMap.path); 65 | this.preMethod.path = entryMap.path; 66 | }, 67 | '_pre-method' (entryMap) { 68 | this.preMethod = tryRequireFn(entryMap.path); 69 | this.preMethod.path = entryMap.path; 70 | }, 71 | _post_method (entryMap) { 72 | this.postMethod = tryRequireFn(entryMap.path); 73 | this.postMethod.path = entryMap.path; 74 | }, 75 | '_post-method' (entryMap) { 76 | this.postMethod = tryRequireFn(entryMap.path); 77 | this.postMethod.path = entryMap.path; 78 | }, 79 | _pre_sub (entryMap) { 80 | this.preSub = tryRequireFn(entryMap.path); 81 | this.preSub.path = entryMap.path; 82 | }, 83 | '_pre-sub' (entryMap) { 84 | this.preSub = tryRequireFn(entryMap.path); 85 | this.preSub.path = entryMap.path; 86 | }, 87 | _post_sub (entryMap) { 88 | this.postSub = tryRequireFn(entryMap.path); 89 | this.postSub.path = entryMap.path; 90 | }, 91 | '_post-sub' (entryMap) { 92 | this.postSub = tryRequireFn(entryMap.path); 93 | this.postSub.path = entryMap.path; 94 | }, 95 | _out (entryMap) { 96 | this.out = tryRequireFn(entryMap.path); 97 | this.out.path = entryMap.path; 98 | }, 99 | _verbs (entryMap) { 100 | if (entryMap.type === FOLDER) { 101 | forIn(entryMap.entries, (verbName, verbMap) => { 102 | verbName = normalizeEntryName(verbName, verbMap.type); 103 | 104 | const [isVerbOk, isNoVerbOk] = validateVerbName(this, verbName); 105 | 106 | if (isVerbOk || isNoVerbOk) { 107 | verbName = remove$(verbName); 108 | ctrlHooks[`_${verbName}`].call(this, verbMap); 109 | } 110 | }); 111 | } 112 | else if (entryMap.type === FILE) { 113 | const verbsObj = tryRequireObj(entryMap.path); 114 | 115 | if (verbsObj) { 116 | forIn(verbsObj, (verbName, fn) => { 117 | if (!isFunction(fn)) return; 118 | 119 | verbName = normalizeEntryName(verbName, DONT_REMOVE_EXT); 120 | 121 | const [isVerbOk, isNoVerbOk] = validateVerbName(this, verbName); 122 | 123 | if (isVerbOk) { 124 | this.verbs[verbName] = fn; 125 | this.verbs[verbName].path = entryMap.path; 126 | } 127 | else if (isNoVerbOk) { 128 | this.noVerb = fn; 129 | this.noVerb.path = entryMap.path; 130 | } 131 | }); 132 | } 133 | } 134 | }, 135 | }; 136 | 137 | function validateVerbName (ctrl, verbName) { 138 | verbName = remove$(verbName); 139 | 140 | const isVerbOk = isVerb(verbName) && !ctrl.verbs[verbName]; 141 | const isNoVerbOk = isNoVerb(verbName) && !ctrl.noVerb; 142 | 143 | return [isVerbOk, isNoVerbOk]; 144 | } 145 | 146 | function remove$ (verb) { 147 | if (verb[0] === '_') { 148 | verb = verb.substr(1); 149 | } 150 | 151 | return verb; 152 | } 153 | 154 | const supportedVerbs = [ 155 | 'get', 156 | 'post', 157 | 'put', 158 | 'delete', 159 | ]; 160 | 161 | const noVerb = ['no-verb', 'no_verb']; 162 | 163 | function isVerb (verb) { 164 | return supportedVerbs.includes(verb); 165 | } 166 | 167 | function isNoVerb (verb) { 168 | return noVerb.includes(verb); 169 | } 170 | 171 | module.exports = ctrlHooks; 172 | -------------------------------------------------------------------------------- /src/app/hooks.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | const error = require('../errors'); 4 | const {FILE, FOLDER} = require('../constants'); 5 | const {tryRequireFn, tryRequireObj, getFn} = require('../utils/try-require'); 6 | const { 7 | normalizeEntryName, 8 | forIn, 9 | removeExtension, 10 | } = require('../utils'); 11 | 12 | function addToIgnoreList (app, ignoreItem) { 13 | const hooksNames = Object.keys(app.Ctrl.prototype); 14 | 15 | ignoreItem = ignoreItem.toLowerCase(); 16 | 17 | if (hooksNames.includes(ignoreItem)) { 18 | return error.cannotIgnoreReservedEntryName(ignoreItem); 19 | } 20 | 21 | app.ignoreList.push(ignoreItem); 22 | } 23 | 24 | module.exports = { 25 | ignore (entryMap) { 26 | if (!hasIndex(entryMap)) return; 27 | 28 | const ignore = require(entryMap.path); 29 | 30 | if (typeof ignore === 'string') { 31 | addToIgnoreList(this, ignore); 32 | } 33 | else if (Array.isArray(ignore)) { 34 | ignore.forEach((item) => { 35 | if (typeof item === 'string') { 36 | addToIgnoreList(this, item); 37 | } 38 | else { 39 | throw error.ignoreItemIsNotAString(item); 40 | } 41 | }); 42 | } 43 | else { 44 | throw error.ignoreItemIsNotAString(ignore); 45 | } 46 | }, 47 | 48 | io_init (entryMap) { 49 | if (!hasIndex(entryMap)) return; 50 | 51 | this.hasInitHook = true; 52 | 53 | const {content, err} = getFn(entryMap.path); 54 | 55 | if (content) { 56 | // check function's arguments count 57 | if (content.length === 0) { 58 | throw error.expectedFunctionArgument(); 59 | } 60 | 61 | this.IO.prototype.init = content; 62 | } 63 | else { 64 | throw error.ioInitExpectedAFunction(err); 65 | } 66 | }, 67 | 68 | io_exit (entryMap) { 69 | if (!hasIndex(entryMap)) return; 70 | 71 | const {content, err} = getFn(entryMap.path); 72 | 73 | if (content) { 74 | this.IO.prototype.exit = content; 75 | } 76 | else { 77 | throw error.ioExitExpectedAFunction(err); 78 | } 79 | }, 80 | 81 | io_proto (entryMap) { 82 | const extending = this.IO.prototype; 83 | 84 | if (entryMap.type === FILE) { 85 | simpleHookFile('io_proto', entryMap.path, extending); 86 | } 87 | else { 88 | simpleHookFolder('io_proto', entryMap.entries, extending); 89 | } 90 | }, 91 | 92 | ctrl_proto (entryMap) { 93 | const extending = this.Ctrl.prototype; 94 | 95 | if (entryMap.type === FILE) { 96 | simpleHookFile('ctrl_proto', entryMap.path, extending); 97 | } 98 | else { 99 | simpleHookFolder('ctrl_proto', entryMap.entries, extending); 100 | } 101 | }, 102 | 103 | ctrl_hooks (entryMap) { 104 | const MUST_BE_A_FUNCTION = true; 105 | const extending = this.ctrlHooks; 106 | 107 | if (entryMap.type === FILE) { 108 | simpleHookFile('ctrl_hooks', entryMap.path, extending, MUST_BE_A_FUNCTION); 109 | } 110 | else { 111 | simpleHookFolder('ctrl_hooks', entryMap.entries, extending); 112 | } 113 | }, 114 | 115 | shared_methods (entryMap) { 116 | const MUST_BE_A_FUNCTION = true; 117 | const extending = Object.create(null); 118 | 119 | if (entryMap.type === FILE) { 120 | this.shared_methods = simpleHookFile('shared_methods', entryMap.path, extending, MUST_BE_A_FUNCTION); 121 | } 122 | else { 123 | this.shared_methods = simpleHookFolder('shared_methods', entryMap.entries, extending); 124 | } 125 | }, 126 | 127 | shared_ctrls (entryMap) { 128 | if (entryMap.type !== FOLDER) { 129 | throw error.sharedCtrlExpectedAFolder(entryMap.path); 130 | } 131 | 132 | const shared_ctrls = Object.create(null); 133 | 134 | forIn(entryMap.entries, (entryName, _entryMap) => { 135 | entryName = normalizeEntryName(entryName, false); 136 | 137 | shared_ctrls[entryName] = new this.Ctrl(entryName, _entryMap, null, this); 138 | 139 | shared_ctrls[entryName].isSharedCtrl = true; 140 | }); 141 | 142 | this.shared_ctrls = shared_ctrls; 143 | }, 144 | }; 145 | 146 | 147 | /* 148 | | requires a file 149 | | validate it's an object {name:method} pairs (or other type than fn) 150 | */ 151 | function simpleHookFile (hookName, hookPath, targetObj, onlyIfFunction) { 152 | const exportedModule = tryRequireObj(hookPath); 153 | 154 | if (exportedModule) { 155 | extend(hookName, targetObj, exportedModule, onlyIfFunction); 156 | } 157 | 158 | return targetObj; 159 | } 160 | 161 | function simpleHookFolder (hookName, entryMaps, targetObj) { 162 | forIn(entryMaps, (entryName, entryMap) => { 163 | const {type, path} = entryMap; 164 | 165 | let exportedModule; 166 | 167 | if (type === FOLDER) { 168 | if (hasIndex(entryMap)) { 169 | exportedModule = tryRequireFn(path); 170 | } 171 | } 172 | else if (type === FILE) { 173 | exportedModule = tryRequireFn(path); 174 | } 175 | 176 | if (exportedModule) { 177 | entryName = removeExtension(entryName); 178 | 179 | trySetObjKey(hookName, targetObj, entryName, exportedModule); 180 | } 181 | }); 182 | 183 | return targetObj; 184 | } 185 | 186 | 187 | /* 188 | | copy from one obj to another 189 | | optional function validation: copy only if value is a function) 190 | */ 191 | function extend (objName, obj, extObj, functionsOnly) { 192 | if (functionsOnly) { 193 | forIn(extObj, (key, val) => { 194 | if (typeof val === 'function') { 195 | trySetObjKey(objName, obj, key, val); 196 | } 197 | else { 198 | return error.hookFunctionExpected(objName, key); 199 | } 200 | }); 201 | } 202 | else { 203 | forIn(extObj, (key, val) => { 204 | trySetObjKey(objName, obj, key, val); 205 | }); 206 | } 207 | } 208 | 209 | 210 | function trySetObjKey (objName, obj, key, val) { 211 | if (typeof obj[key] === 'undefined') { 212 | obj[key] = val; 213 | } 214 | else { 215 | return error.objectKeyAlreadyExists(objName, key); 216 | } 217 | } 218 | 219 | 220 | function hasIndex (entryMap) { 221 | if (entryMap.type === FOLDER && !entryMap.entries['index.js']) { 222 | return error.expectingAnIndexFile(entryMap.path); 223 | } 224 | 225 | return true; 226 | } 227 | -------------------------------------------------------------------------------- /Docs/App Hooks/ctrl_hooks.md: -------------------------------------------------------------------------------- 1 | Hook: "ctrl_hooks" 2 | ================== 3 | **Entry Type**: both a file or a folder 4 | **Exports**: an object of functions (if file) or functions files (if folder) 5 | 6 | When Bootstruct is initialized it parses the web root folder. User folders become controllers, user files become methods and reserved name entries become methods in the controllers' chains. 7 | 8 | You can add your own reserved names and write their parser handlers. 9 | 10 | An "entry handler" is a named function. The name is for the parser to match with entries in your web root different folders (soon to become controllers). The handler, a function, is what should the parser do when it "hits" this name. "ctrl_hooks" are used to do something with a matched entry's contents in the context of the current holding folder/controller. 11 | 12 | >**NOTE**: Bootstruct has an entry handler for each reserved name. 13 | 14 | Let's say you want to read and cache some static resources or precompile some views (templates) in your different controllers. You can add the parser with a `views` handler or a `public` handler to run when it "hits" these names in your controllers. For example: 15 | ``` 16 | ├── api (RC) 17 | │ ├── blogPost 18 | │ │ ├── get.js 19 | │ │ ├── post.js 20 | │ │ └── Views ──> precompile and set on "blogPost" controller 21 | │ └── Public ──> read and cache on "RC" 22 | ``` 23 | 24 | To create controller-hooks, create in your [hooks folder](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) a `ctrl_hooks.js` file or an `ctrl_hooks` folder. 25 | 26 | **File** 27 | ``` 28 | ├── myProject 29 | │ ├── node_modules 30 | │ ├── server-index.js 31 | │ ├── api 32 | │ └── api_hooks 33 | │ └── ctrl_hooks.js <── 34 | ``` 35 | When using a file it should export an object of named functions like: 36 | ```js 37 | // ctrl_hooks.js 38 | module.exports = { 39 | views: function (){...}, 40 | public: function (){...} 41 | }; 42 | ``` 43 | 44 | **Folder** 45 | ``` 46 | ├── myProject 47 | │ ├── node_modules 48 | │ ├── server-index.js 49 | │ ├── api 50 | │ └── api_hooks 51 | │ └── ctrl_hooks <── 52 | │ ├── views 53 | │ └── public.js 54 | ``` 55 | When using a folder, its entry names are the function names and they export the functions. Files like `public.js` here should export a function and folders like `views` should have an `index.js` file that exports the function. 56 | 57 | 58 | 59 | 60 | Handler Function 61 | ---------------- 62 | You have the controller in one hand (the context of `this`) and the content on the other hand (the function argument) so in your handler you access the content, parse it according to your needs and set the result as a property on the controller. That happens on init. Later on you could use these props from your web root methods. 63 | 64 | **Context** 65 | Inside the handler functions the "this" keyword refers to the current holding controller. In the `views` handler, the "this" keyword will refer to the "blogPost" controller (see above) and in the `public` handler, it will refer to the `RC`. 66 | 67 | **Arguments** 68 | The only argument passed to your handler functions is an object called `entryMap`. Its purpose is to give you a good starting point to access the contents of your entries, the views in the `Views` folder and the static resources in the `Public` folder. 69 | 70 | This object is a map of the parsed entry (e.g. `Public`, `Views`). It has two or three properties: 71 | 72 | * `path` (string) - The absolute path to the entry (e.g. `'c:/api/blogPost/views'`) 73 | 74 | * `type` (number) - The type of the entry. `0` for folders and `1` for files (symlinks, junctions or any other types are currently not supported). 75 | 76 | * `entries` (object) - This property exist only for folders. The entries inside the current folder are mapped with the same format (entryMaps). This object's keys are those entries names and its values are entryMap objects. 77 | 78 | A generic example: 79 | ```js 80 | entryMap = { 81 | type: 0|1, 82 | path: 'absolute/path/to/entry', 83 | entries: { 84 | entryName1: {entryMap}, 85 | entryName2: {entryMap}, 86 | entryName3: {entryMap} 87 | } 88 | }; 89 | ``` 90 | 91 | 92 | 93 | 94 | Example 95 | ------- 96 | ``` 97 | ├── api 98 | │ ├── index.js 99 | │ └── Views 100 | │ ├── home.jade 101 | │ └── contactForm.jade 102 | ``` 103 | "jade" is a template engine. `.jade` files need to be compiled before served as HTML. 104 | 105 | Let's say we want the parser to do something every time it hits a `Views` folder: compile the files within (as jade temaplates) and set the result on the controller the `Views` folder was found in. 106 | 107 | The `views` entry handler file could look like this: 108 | ```js 109 | // api_hooks/ctrl_hooks/views.js 110 | var fs = require('fs'); 111 | var jade = require('jade'); 112 | 113 | module.exports = function (entryMap) { 114 | /* 115 | | "this" refers to the "RC" 116 | | 117 | | "entryMap" object contains: 118 | | { 119 | | path: 'path/to/api/Views', 120 | | type: 0, 121 | | entries: { 122 | | 'home.jade': { 123 | | path: 'path/to/api/Views/home.jade', 124 | | type: 1 125 | | }, 126 | | 'contactForm.jade': { 127 | | path: 'path/to/api/Views/contactForm.jade', 128 | | type: 1 129 | | } 130 | | } 131 | | } 132 | */ 133 | var template, tplName; 134 | 135 | // set a new "views" prop on the controller 136 | this.views = {}; 137 | 138 | /* 139 | | loop through the templates, compile them and 140 | | populate "views" on the controller 141 | */ 142 | for (tplName in entryMap.entries) { 143 | // read the template 144 | template = fs.readFileSync( entryMap.entries[tplName].path ); 145 | 146 | // pseudo code to remove the ".jade" extension. 147 | // to store it on the controller without the '.jade' extension. 148 | tplName = removeExt(tplName); 149 | 150 | // compile the template 151 | this.views[tplName] = jade.compile(template); 152 | } 153 | }; 154 | ``` 155 | 156 | To serve the compiled templates (HTML) you can use `this.views[tplName]` from your `api/index.js` file. 157 | ```js 158 | // api/index.js 159 | module.exports = function (io, action) { 160 | if (action == 'contactUs') { // request: '/contactUs' 161 | io.res.end( this.views['contactForm']() ); 162 | } 163 | else { 164 | io.res.end( this.views['home']() ); 165 | } 166 | }; 167 | ``` 168 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "mocha": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | // "sourceType": "module" 17 | }, 18 | "overrides": [{ 19 | "files": 'tests/**', 20 | "env": { 21 | "mocha": true 22 | }, 23 | "rules": { 24 | "no-unused-expressions": "off", 25 | "no-magic-numbers": "off", 26 | "max-lines-per-function": "off", 27 | "max-statements": "off", 28 | "func-names": "off", 29 | "prefer-arrow-callback": "off", 30 | "global-require": "off", 31 | "no-new": "off", 32 | "no-shadow": "off", 33 | }, 34 | }], 35 | "rules": { 36 | "accessor-pairs": "error", 37 | "array-bracket-newline": "error", 38 | "array-bracket-spacing": "error", 39 | "array-callback-return": "error", 40 | // "array-element-newline": ["off", { "minItems": 4, "multiline": true, }], 41 | "arrow-body-style": "error", 42 | "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }], 43 | "arrow-spacing": "error", 44 | "block-scoped-var": "error", 45 | "block-spacing": "error", 46 | "brace-style": ["error", "stroustrup"], 47 | "callback-return": "warn", 48 | "camelcase": ["error", { 49 | allow: ["^REGEX_"] 50 | }], 51 | // "capitalized-comments": "off", 52 | "class-methods-use-this": "error", 53 | "comma-dangle": ["error", "always-multiline"], 54 | "comma-spacing": "error", 55 | "comma-style": "error", 56 | "complexity": "error", 57 | "computed-property-spacing": "error", 58 | // "consistent-return": "off", 59 | "consistent-this": "error", 60 | "curly": ["error", "multi-line"], 61 | "default-case": "error", 62 | "dot-location": ["error", "property"], 63 | "dot-notation": "error", 64 | "eol-last": "error", 65 | "eqeqeq": "error", 66 | "func-call-spacing": "error", 67 | "func-name-matching": "error", 68 | "func-names": [ 69 | "error", 70 | "as-needed" 71 | ], 72 | // "func-style": "off", 73 | "function-paren-newline": "error", 74 | "generator-star-spacing": "error", 75 | // "global-require": "off", 76 | "guard-for-in": "error", 77 | "handle-callback-err": "error", 78 | "id-blacklist": "error", 79 | "id-length": ["error", {"exceptions": ["i"]}], 80 | "id-match": "error", 81 | "implicit-arrow-linebreak": "error", 82 | "indent": ["error", "tab", {"ignoreComments": true}], 83 | // "indent-legacy": "off", 84 | // "init-declarations": "off", 85 | "jsx-quotes": "error", 86 | // "key-spacing": ["off", { "align": "value" }], 87 | "keyword-spacing": "error", 88 | // "line-comment-position": "off", 89 | "linebreak-style": [ 90 | "error", 91 | "unix" 92 | ], 93 | "lines-around-comment": "error", 94 | "lines-around-directive": "error", 95 | "lines-between-class-members": "error", 96 | "max-classes-per-file": "error", 97 | "max-depth": "error", 98 | "max-len": ["error", { 99 | "code": 100, 100 | "tabWidth": 4, 101 | "ignoreStrings": true, 102 | "ignoreTemplateLiterals": true, 103 | }], 104 | "max-lines": "error", 105 | "max-lines-per-function": ["warn", 90], 106 | "max-nested-callbacks": "error", 107 | "max-params": ["error", { "max": 4 }], 108 | "max-statements": ["warn", { "max": 20 }], 109 | "max-statements-per-line": "error", 110 | // "multiline-comment-style": "off", 111 | "multiline-ternary": "error", 112 | "new-cap": ["error", { 113 | "capIsNew": false, 114 | }], 115 | "new-parens": "error", 116 | "newline-after-var": [ 117 | "error", 118 | "always" 119 | ], 120 | "newline-before-return": "error", 121 | "newline-per-chained-call": ["error", { 122 | "ignoreChainWithDepth": 3 123 | }], 124 | "no-alert": "error", 125 | "no-array-constructor": "error", 126 | "no-async-promise-executor": "error", 127 | "no-await-in-loop": "error", 128 | "no-bitwise": "error", 129 | "no-buffer-constructor": "error", 130 | "no-caller": "error", 131 | "no-catch-shadow": "error", 132 | "no-confusing-arrow": "error", 133 | "no-console": "error", 134 | "no-continue": "error", 135 | "no-div-regex": "error", 136 | "no-duplicate-imports": "error", 137 | "no-else-return": "error", 138 | "no-empty-function": "error", 139 | "no-eq-null": "error", 140 | "no-eval": "error", 141 | "no-extend-native": "error", 142 | "no-extra-bind": "error", 143 | "no-extra-label": "error", 144 | // "no-extra-parens": "off", 145 | "no-floating-decimal": "error", 146 | "no-global-assign": "error", 147 | "no-implicit-coercion": "error", 148 | "no-implicit-globals": "error", 149 | "no-implied-eval": "error", 150 | // "no-inline-comments": "off", 151 | "no-invalid-this": "error", 152 | "no-iterator": "error", 153 | "no-label-var": "error", 154 | "no-labels": "error", 155 | "no-lone-blocks": "error", 156 | "no-lonely-if": "error", 157 | "no-loop-func": "error", 158 | "no-magic-numbers": ["error", { "ignore": [0, 1, 2] }], 159 | "no-misleading-character-class": "error", 160 | "no-mixed-operators": "error", 161 | "no-mixed-requires": "error", 162 | "no-multi-assign": "error", 163 | // "no-multi-spaces": ["off", { "exceptions": { "VariableDeclarator": true, "ImportDeclaration": true, "Property": true, }}], 164 | "no-multi-str": "error", 165 | "no-multiple-empty-lines": ["error", { 166 | "max": 4, 167 | "maxEOF": 1, 168 | }], 169 | "no-native-reassign": "error", 170 | "no-negated-condition": "error", 171 | "no-negated-in-lhs": "error", 172 | "no-nested-ternary": "error", 173 | "no-new": "error", 174 | "no-new-func": "error", 175 | "no-new-object": "error", 176 | "no-new-require": "error", 177 | "no-new-wrappers": "error", 178 | "no-octal-escape": "error", 179 | // "no-param-reassign": "off", 180 | "no-path-concat": "error", 181 | // "no-plusplus": "off", 182 | "no-process-env": "error", 183 | "no-process-exit": "error", 184 | "no-proto": "error", 185 | "no-prototype-builtins": "error", 186 | "no-restricted-globals": "error", 187 | "no-restricted-imports": "error", 188 | "no-restricted-modules": "error", 189 | "no-restricted-properties": "error", 190 | "no-restricted-syntax": "error", 191 | "no-return-assign": "error", 192 | "no-return-await": "error", 193 | "no-script-url": "error", 194 | "no-self-compare": "error", 195 | "no-sequences": "error", 196 | "no-shadow": "error", 197 | "no-shadow-restricted-names": "error", 198 | "no-spaced-func": "error", 199 | // "no-sync": "off", 200 | // "no-tabs": "off", 201 | "no-template-curly-in-string": "error", 202 | // "no-ternary": "off", 203 | "no-throw-literal": "error", 204 | "no-trailing-spaces": "error", 205 | "no-undef-init": "error", 206 | "no-undefined": "error", 207 | "no-underscore-dangle": ["error", { 208 | "allow": ["_METHOD", "_debugIndentSpaces", "_headers"] 209 | }], 210 | "no-unmodified-loop-condition": "error", 211 | "no-unneeded-ternary": "error", 212 | "no-unsafe-negation": "error", 213 | "no-unused-expressions": ["error", { 214 | "allowShortCircuit": true 215 | }], 216 | "no-use-before-define": ["error", { 217 | "functions": false, 218 | "classes": false 219 | }], 220 | "no-useless-call": "error", 221 | "no-useless-catch": "error", 222 | "no-useless-computed-key": "error", 223 | "no-useless-concat": "error", 224 | "no-useless-constructor": "error", 225 | "no-useless-escape": "error", 226 | "no-useless-rename": "error", 227 | "no-useless-return": "error", 228 | "no-var": "error", 229 | "no-void": "error", 230 | // "no-warning-comments": "off", 231 | "no-whitespace-before-property": "error", 232 | "no-with": "error", 233 | "nonblock-statement-body-position": "error", 234 | "object-curly-newline": "error", 235 | "object-curly-spacing": "error", 236 | "object-property-newline": "error", 237 | "object-shorthand": "error", 238 | // "one-var": "off", 239 | "one-var-declaration-per-line": "error", 240 | "operator-assignment": "error", 241 | "operator-linebreak": "error", 242 | // "padded-blocks": "off", 243 | "padding-line-between-statements": "error", 244 | "prefer-arrow-callback": "error", 245 | "prefer-const": "error", 246 | "prefer-destructuring": "error", 247 | // "prefer-named-capture-group": "off", 248 | "prefer-numeric-literals": "error", 249 | "prefer-object-spread": "error", 250 | "prefer-promise-reject-errors": "error", 251 | // "prefer-reflect": "off", // Deprecated 252 | "prefer-rest-params": "error", 253 | "prefer-spread": "error", 254 | "prefer-template": "error", 255 | "quote-props": ["error", "as-needed"], 256 | "quotes": [ 257 | "error", 258 | "single" 259 | ], 260 | "radix": "error", 261 | "require-atomic-updates": "error", 262 | "require-await": "error", 263 | // "require-jsdoc": "off", 264 | "require-unicode-regexp": "error", 265 | "rest-spread-spacing": "error", 266 | "semi": "error", 267 | "semi-spacing": "error", 268 | // "semi-style": [ "off", "last" ], 269 | "sort-imports": ["error", { 270 | "ignoreCase": true, 271 | "ignoreMemberSort": true, 272 | "memberSyntaxSortOrder": [ 273 | "single", 274 | "multiple", 275 | "all", 276 | "none" 277 | ] 278 | }], 279 | // "sort-keys": "off", 280 | // "sort-vars": "off", 281 | "space-before-blocks": "error", 282 | "space-before-function-paren": "error", 283 | "space-in-parens": [ 284 | "error", 285 | "never" 286 | ], 287 | "space-infix-ops": "error", 288 | "space-unary-ops": "error", 289 | "spaced-comment": [ 290 | "error", 291 | "always" 292 | ], 293 | "strict": [ 294 | "error", 295 | "never" 296 | ], 297 | "switch-colon-spacing": "error", 298 | "symbol-description": "error", 299 | "template-curly-spacing": "error", 300 | "template-tag-spacing": "error", 301 | "unicode-bom": [ 302 | "error", 303 | "never" 304 | ], 305 | "valid-jsdoc": "error", 306 | "vars-on-top": "error", 307 | "wrap-iife": "error", 308 | "wrap-regex": "error", 309 | "yield-star-spacing": "error", 310 | "yoda": "error" 311 | } 312 | }; 313 | -------------------------------------------------------------------------------- /Docs/README.md: -------------------------------------------------------------------------------- 1 | Bootstruct Docs 2 | =============== 3 | 4 | Table of Contents 5 | ----------------- 6 | * [Main Page (Overview)](https://github.com/taitulism/Bootstruct) 7 | * [Get Started](https://github.com/taitulism/Bootstruct/blob/master/Docs/Get%20Started.md) 8 | * Docs (this page) 9 | * [Terminology](#terminology) 10 | * [General](#general) 11 | * [App's Flow](#apps-flow) 12 | * [Controller's Flow](#controllers-flow) 13 | * [Methods](#methods) 14 | * [Files and Folders](#files-and-folders) 15 | * [URL parameters](#url-parameters) 16 | * [Argument Smart Matching](#argument-smart-matching) 17 | * More Docs 18 | * [Controller Hooks](./Controller%20Hooks.md) 19 | * [Extend Bootstruct](./Extending%20Bootstruct.md) 20 | 21 | 22 | 23 | 24 | Terminology 25 | ----------- 26 | Before we start, let's clarify some terms used in these docs: 27 | 28 | * The **web root folder**: Bootstruct turns folders and files into url url handlers. The web root folder is the top most folder Bootstruct parses inside your project. You can give it whatever name you want but in the docs we'll use the name: **"api"**. 29 | 30 | * The **RC**: The Root-Controller. The main controller parsed from the web root folder. 31 | 32 | * An **Entry**: Either a file or a folder. Folder's **entries** are the files and folders inside it. 33 | 34 | * An HTTP **Verb**: From Google: *"The primary or most-commonly-used HTTP verbs (or methods, as they are properly called) are POST, GET, PUT, and DELETE"*. 35 | The word "method" is used in these docs in the context of a function so "HTTP verbs" is used instead of "HTTP methods". 36 | 37 | 38 | 39 | 40 | General 41 | ------- 42 | Learning Bootstruct is more about understanding how it behaves based on your files and folders than code and syntax. 43 | 44 | Bootstruct is based on a mix of two quite close conventions: a folder structure convention and a filename convention. 45 | 46 | When Bootstruct is initialized it parses your web root folder recursively. Basically, folders become URL-controllers and files become their methods. Certain names (start with a `_` sign) are parsed into specific kinds of methods, whether they are files or folders. Eventually, your web root folder and its sub-folders are translated into a root-controller, a nested structure of controllers and their sub-controllers. This Root-Controller (`RC` from now on) is your Bootstruct app's core object. 47 | 48 | >**NOTE**: Bootstruct ignores files and folders that their names start with a dot or an underscore like `.ignored` or `_ignored`). 49 | 50 | 51 | 52 | 53 | App's Flow 54 | ---------- 55 | The "moving parts" in your app are `io` objects which hold the `request` and the `response`. They move between controllers and their methods. 56 | 57 | On request, a new `io` object "checks-in" at your `RC`. It does its way in through sub-controllers to its target-controller and then "checks-out", going through the same controllers back to the `RC`. 58 | 59 | As mention at the [overview](https://github.com/taitulism/Bootstruct/blob/master/README.md) on the main page, with this structure: 60 | ``` 61 | ├── api (RC) 62 | │ └── A 63 | │ └── B 64 | ``` 65 | 66 | a request to `/A/B` would go through: 67 | ``` 68 | 1. RC 69 | 2. RC/A 70 | 3. RC/A/B 71 | 4. RC/A 72 | 5. RC 73 | ``` 74 | 75 | With this behavior you can execute some code on controller "A" before and after controller "B" (steps 2 and 4). Of course, you can end the response whenever you like or use just a "half an onion": 76 | ``` 77 | 1. RC 78 | 2. RC/A 79 | 3. RC/A/B 80 | ``` 81 | 82 | 83 | 84 | 85 | Controller's Flow 86 | ----------------- 87 | Every request has a target-controller. A request's target-controller is the last existing controller whose name found in the URL. A controller can act as the request's target-controller or as one of the target-controller's parents. For example: 88 | ``` 89 | ├── api 90 | │ └── A 91 | │ └── B 92 | │ └── C.js 93 | ``` 94 | The **"A"** controller is **the target**-controller for requests to `/A` (and `/A/whatever`). 95 | The **"A"** controller is **a parent**-controller for requests to `/A/B` (and `/A/B/whatever`). 96 | For requests to `/A/B/C` (and `/A/B/C/whatever`), the **"B"** controller is **the target**-controller and `C.js` is its method. 97 | 98 | Controllers have three chains of methods they execute in each case: a target-chain, a parent-chain and a method-chain. These chains are arrays of functions (created on init) and when you name an entry with one of [Bootstruct's reserved names](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks/README.md) you actually mount its exported function on one of these chains. 99 | When a request checks-in at a controller, the controller routes the request through one of its chains according to its role (parent/target/method). 100 | 101 | >**NOTE**: All methods get called with an `io` as their first argument. 102 | 103 | The following image describes these chains: The method-chain is on the right, the parent-chain is on the left and the target-chain is in the middle. 104 | ![Controller Flowchart](https://raw.githubusercontent.com/taitulism/Bootstruct/master/Docs/controller-flowchart.png) 105 | 106 | >**NOTE**: Those are NOT all of Bootstruct's reserved names. 107 | 108 | All of the three chains start with `_in` and end with `_out` methods. These are the very first and last methods a controller (who has them) would call, regardless of its role per request. 109 | 110 | The principle is pretty simple: **each chain has a center, which is its main point, and you can run some code before and after that main point.** 111 | 112 | The target-chain is all about the verbs (GET, POST, PUT, DELETE). They are for controllers' core functionality (see wiki: [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)). 113 | 114 | You can run some code before or after the verb method. A "verb method" would be the exported function from a `_post.js` file or a `_post` folder for example. `_before_verb` is an alias of `index`, mentioned in the [Get started](https://github.com/taitulism/Bootstruct/blob/master/Docs/Get%20Started.md) page. Their exported function gets called "before" any \. 115 | 116 | As `_before_verb` and `_after_verb` run in the target-chain before and after any verb method does, `_pre_method` and `_post_method` will run in the method-chain before and after any user \ (e.g. `C.js`) and `_pre_sub`/`_post_sub` (parent-chain) will run before and after any \ ("pre" = before, "post" = after, not to be confused with the `_post` HTTP verb). 117 | 118 | The \ part is where the recursion happens, where an `io` checks in and out at controllers, parent -> child -> parent. The child, a controller itself, has its own chains and child-controllers (sub-controllers). 119 | 120 | See [Bootstruct's reserved names](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks/README.md) 121 | 122 | 123 | 124 | 125 | Methods 126 | ------- 127 | Both types of methods, reserved methods and non-reserved methods (user methods) are being `require`-d to the controller on init so they must export a single function. On request, methods handle at least one argument, an `io` (mentioned above). It holds the `request` and the `response` (and some other props and methods. To move the `io` forward in the chain you call: `io.next()`, unless you choose to end the response with `io.res.end()`. 128 | 129 | Your Bootstruct methods will generally look like: 130 | ```js 131 | module.exports = function (io) { 132 | /* 133 | "this" is the holding controller 134 | 135 | the "io" holds the request/response as props: 136 | io.req === request 137 | io.res === response 138 | 139 | */ 140 | 141 | // some code... 142 | 143 | 144 | io.next(); 145 | }; 146 | ``` 147 | 148 | `io` is not the only argument your methods handle. Read about named params on the [URL parameters](#url-parameters) section below. 149 | 150 | Methods get called with the context of their holding controller so the keyword "this" refers to the controller you're in. 151 | 152 | All controllers have access to the same scope (`this.app`), which is the `app` instance itself so you could share data in your app without polluting any global scope or reapeating the same `require`-s. 153 | 154 | See [Bootstruct Hooks](https://github.com/taitulism/Bootstruct/blob/master/Docs/Hooks.md) to learn how to populate your app instance with properties on init. 155 | 156 | 157 | 158 | 159 | Files and Folders 160 | ----------------- 161 | As said, Bootstruct translates user folders to controllers and user files to methods but reserved name entries (both files and folders) become methods in the controllers' chains regardless of the entry type (file/folder). Since those are being `require`-d on controllers, when using a folder with a reserved name, you must include an `index.js` file with the exported function. Node expects an `index.js` file when: `require('path/to/a/FOLDER')`. 162 | 163 | Consider this structure for example: 164 | ``` 165 | ├── api 166 | │ └── friends 167 | │ ├── index.js 168 | │ ├── _get.js 169 | │ └── _post.js 170 | ``` 171 | `index`, `_get` and `_post` are all reserved names for target-chain methods. 172 | 173 | Let's say we have a user's "/friends" page in our social network app. Our `index` checks for authentication before both verbs. Our `_get` method is pretty simple, it gets the user's friends list and sends it back to the client. Our `_post` method, from the other hand, is quite messy: it has a lot of dependencies, it validates, sends an email, etc. In this case it would be better to turn the `_post.js` file into a `_post` folder: 174 | ``` 175 | ├── api 176 | │ └── friends 177 | │ ├── index.js // reserved name `index` 178 | │ ├── _get.js 179 | │ └── _post 180 | │ ├── index.js // = _post.js 181 | │ ├── validate.js 182 | │ ├── email.js 183 | │ └── etc.js 184 | ``` 185 | 186 | The `_post` folder does NOT parsed as a controller because of its meaningful name, therefore the `index.js` file inside it is not treated as a reserved name (like `api/friends/index.js` does). It's just what Node is looking for when `require`-ing a folder. Consider: `require('friends/post')`. 187 | 188 | In another case we have a tiny controller with `index` as its only method: 189 | ``` 190 | ├── api ──> controller 191 | │ └── home ──> controller 192 | │ └── index.js ──> method 193 | ``` 194 | Here it would be wise to cut the overhead of a controller and turn it into a method (a file): 195 | ``` 196 | ├── api ──> controller 197 | │ └── home.js ──> method 198 | ``` 199 | Both handle requests to `/home` but now instead of having two controllers and a method we have only one controller with a single method. 200 | 201 | Generally folders become controllers but let's say we want a `home` method but due to its complexity we would like to turn it into a folder. In this case, we can put a flag entry named **_METHOD** inside the `home` folder and it won't be parsed as a controller but as a method. 202 | ``` 203 | ├── api 204 | │ └── home ──> becomes a method 205 | │ ├── index.js 206 | │ ├── dependency.js 207 | │ └── _METHOD ──> a file or a folder 208 | ``` 209 | 210 | If you want a certain entry to be ignored by the parser, add a preceding underscore or a dot to its name: 211 | ``` 212 | ├── api 213 | │ ├── _myModules <── 214 | │ │ ├── helper1.js 215 | │ │ └── helper2.js 216 | │ ├── _myUtils <── 217 | │ └── index.js 218 | ``` 219 | 220 | >**NOTE**: Non-`.js` files are always ignored (e.g. 'file.txt'). 221 | 222 | 223 | 224 | 225 | URL parameters 226 | -------------- 227 | On request Bootstruct splits the URL pathname by slashs so a request to `/A/B/whatever` becomes an array: `['A','B','whatever']`. It is stored on the `io` as `io.params`. 228 | 229 | >**NOTE**: Bootstruct ignores trailing slashes in URLs and merges repeating slashes (e.g. `/A//B//` is treated like `/A/B`). Also, due to its nature, Bootstruct is CaSe-InSeNsItIvE when it comes to URLs that correspond with your folder/file names (e.g. `/A/B` is the same as `/a/b`). 230 | 231 | The different items in that `io.params` array could be a controller name, a method, or just a parameter like "whatever" above: 232 | ``` 233 | domain.com/ 234 | domain.com/ctrl 235 | domain.com/method 236 | domain.com/param 237 | domain.com/ctrl/param 238 | domain.com/ctrl/ctrl/method/param1/param2 239 | etc. 240 | ``` 241 | 242 | **IMPORTANT NOTE:** Bootstruct uses `io.params` to find the target-controller. Don't manipulate this array unless you know what you're doing. `.push`-ing and `.pop`-ing your own items is cool. Changing the existing items could cause unexpected behavior. 243 | 244 | Controllers (with the `RC` as an exception) remove their names from the array (always the first item) so your target-controller's methods are only left with the params that doesn't stand for a controller or a method (e.g. `['whatever']`). 245 | ``` 246 | api ──> io.params = [A,B,whatever] 247 | api/A ──> io.params = [B,whatever] 248 | api/A/B ──> io.params = [whatever] 249 | api/A ──> io.params = [whatever] 250 | api ──> io.params = [whatever] 251 | ``` 252 | 253 | Methods always get called with `io` as their first argument. The rest of the arguments are the items in `io.params` (if any). On request to `/A/B/whatever` your methods in "B" controller's will get called with `io` as the first argument, and `whatever` as the second one so you can use named params in addition to the `io.params`: 254 | ```js 255 | module.exports = function (io, param) { 256 | 257 | console.log(io.params); // -> ['whatever'] 258 | console.log(param); // -> 'whatever' 259 | 260 | // ... 261 | 262 | io.next(); 263 | 264 | }; 265 | ``` 266 | 267 | 268 | 269 | 270 | Argument Smart Matching 271 | ----------------------- 272 | Let's say you want to support requests like: `/bookId/351/chapter/12`. Without the smart-matching your method will probably look like: 273 | ```js 274 | module.exports = function (io, bookId, bookIdValue, chapter, chapterValue) { 275 | // ... 276 | } 277 | ``` 278 | 279 | This makes your key params ("bookId" and "chpater") redundant: 280 | ```js 281 | module.exports = function (io, bookId, bookIdValue, chapter, chapterValue) { 282 | bookId === 'bookId' // static value - not very usefull 283 | bookIdValue === 351 // dynamic value 284 | chapter === 'chapter' // static value - not very usefull 285 | chapterValue === 12 // dynamic value 286 | } 287 | ``` 288 | 289 | You can skip the duplication by doing: 290 | ```js 291 | module.exports = function (io, $bookId, $chapter) { 292 | $bookId === 351 293 | $chapter === 12 294 | } 295 | ``` 296 | 297 | Bootstruct reads your methods on initialization and looks at their params. For any parameter that starts with a `$` sign (e.g. `$myParam`), Bootstruct will "smart-match" its value. 298 | 299 | single values like "blah" in `/bookId/351/blah/chapter/12` will be pushed last: 300 | ```js 301 | module.exports = function (io, $bookId, $chapter, single) { 302 | $bookId === 351 303 | $chapter === 12 304 | single === 'blah' 305 | } 306 | ``` 307 | 308 | **IMPORTANT NOTE:** The way Bootstruct extracts $params is by calling `.toString()` on your methods and a regular string match to get the $params. Currently ES6's default value feature is not supported: 309 | `function (io, $a, $b = 'default') {...}` 310 | --------------------------------------------------------------------------------