diff --git a/.editorconfig b/.editorconfig index 91422397..1dfdf296 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,22 @@ -# editorconfig.org -root = true +# http://editorconfig.org [*] -indent_size = 2 indent_style = space +indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +[*.json] +insert_final_newline = ignore + +[**.min.js] +indent_style = ignore +insert_final_newline = ignore + +[MakeFile] +indent_style = space + [*.md] trim_trailing_whitespace = false diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 00000000..8646cd44 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,16 @@ +name: checks +on: + - push + - pull_request + - workflow_call +jobs: + test: + uses: adonisjs/core/.github/workflows/test.yml@main + with: + install-pnpm: true + + lint: + uses: adonisjs/.github/.github/workflows/lint.yml@main + + typecheck: + uses: adonisjs/.github/.github/workflows/typecheck.yml@main diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 00000000..af9b6f06 --- /dev/null +++ b/.github/workflows/labels.yml @@ -0,0 +1,15 @@ +name: Sync labels +on: + workflow_dispatch: +permissions: + issues: write +jobs: + labels: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EndBug/label-sync@v2 + with: + config-file: 'https://raw.githubusercontent.com/thetutlage/static/main/labels.yml' + delete-other-labels: true + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..fde89b42 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: release +on: workflow_dispatch +permissions: + contents: write + id-token: write +jobs: + checks: + uses: ./.github/workflows/checks.yml + release: + needs: checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: git config + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + - name: Init npm config + run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: npm install + - run: npm run release -- --ci + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..cad5c917 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,17 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 0 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: 'This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue' + stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request' + close-issue-message: 'This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue' + close-pr-message: 'This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request' + days-before-stale: 21 + days-before-close: 5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..3fe314d2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +on: + workflow_call: + inputs: + disable-windows: + description: Disable running tests on Windows + type: boolean + default: false + required: false + install-pnpm: + description: Install pnpm before running tests + type: boolean + default: false + required: false + +jobs: + test_linux: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: ["lts/iron", "lts/jod", "latest"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + if: ${{ inputs.install-pnpm }} + uses: pnpm/action-setup@v2 + with: + version: 8.6.3 + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test + + test_windows: + if: ${{ !inputs.disable-windows }} + runs-on: windows-latest + strategy: + matrix: + node-version: ["lts/iron", "lts/jod", "latest"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + if: ${{ inputs.install-pnpm }} + uses: pnpm/action-setup@v2 + with: + version: 8.6.3 + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index 2ec220db..f138f534 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,17 @@ -coverage node_modules -.DS_Store -npm-debug.log +coverage +.DS_STORE +.nyc_output .idea -test/unit/storage/sessions +.vscode/ +*.sublime-project +*.sublime-workspace +*.log +build +dist +yarn.lock +shrinkwrap.yaml +package-lock.json +test/__app +.env +backup diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 0e95cfb7..00000000 --- a/.npmignore +++ /dev/null @@ -1,9 +0,0 @@ -coverage -node_modules -.DS_Store -npm-debug.log -test -.travis.yml -.editorconfig -benchmarks -.idea diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..17edef57 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +build +docs +coverage diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 499a8541..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: node_js -node_js: -- node -- 5.3.0 -- 4.0.0 -sudo: false -services: -- redis-server -install: -- npm install --no-optional -notifications: - slack: - secure: m91zkX2cLVDRDMBAUnR1d+hbZqtSHXLkuPencHadhJ3C3wm53Box8U25co/goAmjnW5HNJ1SMSIg+DojtgDhqTbReSh5gSbU0uU8YaF8smbvmUv3b2Q8PRCA7f6hQiea+a8+jAb7BOvwh66dV4Al/1DJ2b4tCjPuVuxQ96Wll7Pnj1S7yW/Hb8fQlr9wc+INXUZOe8erFin+508r5h1L4Xv0N5ZmNw+Gqvn2kPJD8f/YBPpx0AeZdDssTL0IOcol1+cDtDzMw5PAkGnqwamtxhnsw+i8OW4avFt1GrRNlz3eci5Cb3NQGjHxJf+JIALvBeSqkOEFJIFGqwAXMctJ9q8/7XyXk7jVFUg5+0Z74HIkBwdtLwi/BTyXMZAgsnDjndmR9HsuBP7OSTJF5/V7HCJZAaO9shEgS8DwR78owv9Fr5er5m9IMI+EgSH3qtb8iuuQaPtflbk+cPD3nmYbDqmPwkSCXcXRfq3IxdcV9hkiaAw52AIqqhnAXJWZfL6+Ct32i2mtSaov9FYtp/G0xb4tjrUAsDUd/AGmMJNEBVoHtP7mKjrVQ35cEtFwJr/8SmZxGvOaJXPaLs43dhXKa2tAGl11wF02d+Rz1HhbOoq9pJvJuqkLAVvRdBHUJrB4/hnTta5B0W5pe3mIgLw3AmOpk+s/H4hAP4Hp0gOWlPA= diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 94196d7c..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,203 +0,0 @@ - -## [3.0.1](https://github.com/adonisjs/adonis-framework/compare/v3.0.0...v3.0.1) (2016-07-29) - - -### Features - -* **resource:middleware:** chain middleware method on route resource([04d6acc](https://github.com/adonisjs/adonis-framework/commit/04d6acc)) -* **route:resource:** resource members & coll accepts callback([6603d4f](https://github.com/adonisjs/adonis-framework/commit/6603d4f)) - - - - -# [3.0.0](https://github.com/adonisjs/adonis-framework/compare/v2.0.9...v3.0.0) (2016-06-26) - -### Bug Fixes - -* **server:** Fix for #128 ([4aa4f80](https://github.com/adonisjs/adonis-framework/commit/4aa4f80)), closes [#128](https://github.com/adonisjs/adonis-framework/issues/128) -* **static-server:** Fix for #124 where node-static was crashing ([38efbd4](https://github.com/adonisjs/adonis-framework/commit/38efbd4)), closes [#124](https://github.com/adonisjs/adonis-framework/issues/124) [#124](https://github.com/adonisjs/adonis-framework/issues/124) -======= -* **linting:** fix linting error inside view extensions file([3095839](https://github.com/adonisjs/adonis-framework/commit/3095839)) -* **middleware:** Fix middleware layer to execute middleware in reverse after controller call([97857ab](https://github.com/adonisjs/adonis-framework/commit/97857ab)) -* **request:** handle multiple file uploads([2d0dfcb](https://github.com/adonisjs/adonis-framework/commit/2d0dfcb)) -* **route:test:** improve test expectations to match instead of strict equal([31e8d6c](https://github.com/adonisjs/adonis-framework/commit/31e8d6c)) -* **server:** add try/catch block to handle errors outside of co([36a40b6](https://github.com/adonisjs/adonis-framework/commit/36a40b6)) -* **server:** Fix for [#128](https://github.com/adonisjs/adonis-framework/issues/128)([4aa4f80](https://github.com/adonisjs/adonis-framework/commit/4aa4f80)), closes [#128](https://github.com/adonisjs/adonis-framework/issues/128) -* **session:** fix sessions provider after node-cookie upgrade([8990f0d](https://github.com/adonisjs/adonis-framework/commit/8990f0d)) -* **static-server:** Fix for [#124](https://github.com/adonisjs/adonis-framework/issues/124) where node-static was crashing([38efbd4](https://github.com/adonisjs/adonis-framework/commit/38efbd4)), closes [#124](https://github.com/adonisjs/adonis-framework/issues/124) [#124](https://github.com/adonisjs/adonis-framework/issues/124) -* **static-server:** Fix for [#124](https://github.com/adonisjs/adonis-framework/issues/124) where node-static was crashing([787573d](https://github.com/adonisjs/adonis-framework/commit/787573d)), closes [#124](https://github.com/adonisjs/adonis-framework/issues/124) [#124](https://github.com/adonisjs/adonis-framework/issues/124) - - -### Features - -* **Env:** add support to load .env from different location([2503fbb](https://github.com/adonisjs/adonis-framework/commit/2503fbb)) -* **event:** add property eventName to scope emitter object([fee2a36](https://github.com/adonisjs/adonis-framework/commit/fee2a36)) -* **event:** add support for event emitter([8a7b3a7](https://github.com/adonisjs/adonis-framework/commit/8a7b3a7)) -* **file:** allow file instance to take validation options([36e3f1b](https://github.com/adonisjs/adonis-framework/commit/36e3f1b)) -* **form-helper:** added form.open method to create html form tag([e6367ff](https://github.com/adonisjs/adonis-framework/commit/e6367ff)) -* **form-helper:** added method to add labels([e56a834](https://github.com/adonisjs/adonis-framework/commit/e56a834)) -* **form-helper:** implemented all required html tags([082e8c8](https://github.com/adonisjs/adonis-framework/commit/082e8c8)) -* **helpers:** add makeNameSpace method([651972f](https://github.com/adonisjs/adonis-framework/commit/651972f)) -* **helpers:** add method to know whether process is for ace command([4beb8a2](https://github.com/adonisjs/adonis-framework/commit/4beb8a2)) -* **helpers:** add methods to get path to database directories([85688c8](https://github.com/adonisjs/adonis-framework/commit/85688c8)) -* **request:** Add collect method to form group of arrays with keys([d0e5303](https://github.com/adonisjs/adonis-framework/commit/d0e5303)) -* **request:** add support for adding macros([cfd129b](https://github.com/adonisjs/adonis-framework/commit/cfd129b)) -* **response:** add macro support([d7ad0ee](https://github.com/adonisjs/adonis-framework/commit/d7ad0ee)) -* **Route:** add router helper to render view([0f517cb](https://github.com/adonisjs/adonis-framework/commit/0f517cb)) -* **server:** add support to disable _method spoofing([43f21a2](https://github.com/adonisjs/adonis-framework/commit/43f21a2)) -* **server:** added the ability to obtain instance http.createServer () for socket.io ([#165](https://github.com/adonisjs/adonis-framework/issues/165))([8d221d0](https://github.com/adonisjs/adonis-framework/commit/8d221d0)) -* **static:** Switch to server-static for serving static files([cc5be2a](https://github.com/adonisjs/adonis-framework/commit/cc5be2a)) -* **view:** Add config option to disable service injection([9a9f3d4](https://github.com/adonisjs/adonis-framework/commit/9a9f3d4)) -* **view:** add globals for linkTo and linkToAction([3e6530d](https://github.com/adonisjs/adonis-framework/commit/3e6530d)) -* **view:** added makeString method and filter for making urls using controller methods([da7d080](https://github.com/adonisjs/adonis-framework/commit/da7d080)) -* **view:** Make all views to have .nunjucks extension [#133](https://github.com/adonisjs/adonis-framework/issues/133)([8535172](https://github.com/adonisjs/adonis-framework/commit/8535172)), closes [#133](https://github.com/adonisjs/adonis-framework/issues/133) - - -## [2.0.11](https://github.com/adonisjs/adonis-framework/compare/v2.0.9...v2.0.11) (2016-03-30) - - - -## [2.0.10](https://github.com/adonisjs/adonis-framework/compare/v2.0.9...v2.0.10) (2016-03-26) - - -### Bug Fixes - -* **static-server:** Fix for #124 where node-static was crashing ([38efbd4](https://github.com/adonisjs/adonis-framework/commit/38efbd4)), closes [#124](https://github.com/adonisjs/adonis-framework/issues/124) [#124](https://github.com/adonisjs/adonis-framework/issues/124) - - - - -## [2.0.9](https://github.com/adonisjs/adonis-framework/compare/v2.0.3...v2.0.9) (2016-01-30) - - -### Bug Fixes - -* **request:** method is and accepts have been fixed to treat arrays ([9d8e963](https://github.com/adonisjs/adonis-framework/commit/9d8e963)) -* **session:** fixed session manager to keep updated session payload within one request #88 ([1fe8b4b](https://github.com/adonisjs/adonis-framework/commit/1fe8b4b)), closes [#88](https://github.com/adonisjs/adonis-framework/issues/88) -* **session:** now list of drivers is set to an empty object by default ([3bb75e0](https://github.com/adonisjs/adonis-framework/commit/3bb75e0)) -* **session-manager:** avoiding reparsing of session body ([0d8394d](https://github.com/adonisjs/adonis-framework/commit/0d8394d)) - -### Features - -* **middleware:** middleware now accepts runtime parameters ([6907053](https://github.com/adonisjs/adonis-framework/commit/6907053)) -* **package:** passing coverage report to coveralls ([579ab3e](https://github.com/adonisjs/adonis-framework/commit/579ab3e)) -* **package.json:** make the repository commitizen-friendly ([0082c4a](https://github.com/adonisjs/adonis-framework/commit/0082c4a)) -* **request:** add match method to match an array of patterns to current url ([a81a4f7](https://github.com/adonisjs/adonis-framework/commit/a81a4f7)) -* **request:** added hasBody and format methods to request instance ([30739db](https://github.com/adonisjs/adonis-framework/commit/30739db)) -* **request:** Added raw method to access raw data sent to a given request ([00de598](https://github.com/adonisjs/adonis-framework/commit/00de598)) -* **response:** added descriptive methods to make response like ok,unauthorized ([b092407](https://github.com/adonisjs/adonis-framework/commit/b092407)) -* **response:** added sendView method to end the response immediately by sending view ([1655667](https://github.com/adonisjs/adonis-framework/commit/1655667)) -* **route:** added options to add format to routes ([cfe6c5c](https://github.com/adonisjs/adonis-framework/commit/cfe6c5c)) -* **route-resource:** added support for nested resources and resources filters ([907014e](https://github.com/adonisjs/adonis-framework/commit/907014e)) -* **routes:** added middleware alias and added support for multiple params ([51cf673](https://github.com/adonisjs/adonis-framework/commit/51cf673)) - -### Performance Improvements - -* **config:** remove auto-load with require-all for performance ([806aae2](https://github.com/adonisjs/adonis-framework/commit/806aae2)) -* **env,file,helpers:** improved initial datatypes to help v8 set hidden classes ([79bd6b4](https://github.com/adonisjs/adonis-framework/commit/79bd6b4)) -* **middleware,route,server,session:** improved variables initialization to keep v8 happy ([20080ec](https://github.com/adonisjs/adonis-framework/commit/20080ec)) -* **request.format:** added acceptance for request.format ([4ed82c2](https://github.com/adonisjs/adonis-framework/commit/4ed82c2)) - - - - -## [2.0.8](https://github.com/adonisjs/adonis-framework/compare/v2.0.3...v2.0.8) (2016-01-29) - - -### Bug Fixes - -* **request:** method is and accepts have been fixed to treat arrays ([9d8e963](https://github.com/adonisjs/adonis-framework/commit/9d8e963)) -* **session:** fixed session manager to keep updated session payload within one request #88 ([1fe8b4b](https://github.com/adonisjs/adonis-framework/commit/1fe8b4b)), closes [#88](https://github.com/adonisjs/adonis-framework/issues/88) -* **session:** now list of drivers is set to an empty object by default ([3bb75e0](https://github.com/adonisjs/adonis-framework/commit/3bb75e0)) - -### Features - -* **middleware:** middleware now accepts runtime parameters ([6907053](https://github.com/adonisjs/adonis-framework/commit/6907053)) -* **package:** passing coverage report to coveralls ([579ab3e](https://github.com/adonisjs/adonis-framework/commit/579ab3e)) -* **package.json:** make the repository commitizen-friendly ([0082c4a](https://github.com/adonisjs/adonis-framework/commit/0082c4a)) -* **request:** add match method to match an array of patterns to current url ([a81a4f7](https://github.com/adonisjs/adonis-framework/commit/a81a4f7)) -* **request:** added hasBody and format methods to request instance ([30739db](https://github.com/adonisjs/adonis-framework/commit/30739db)) -* **request:** Added raw method to access raw data sent to a given request ([00de598](https://github.com/adonisjs/adonis-framework/commit/00de598)) -* **response:** added descriptive methods to make response like ok,unauthorized ([b092407](https://github.com/adonisjs/adonis-framework/commit/b092407)) -* **response:** added sendView method to end the response immediately by sending view ([1655667](https://github.com/adonisjs/adonis-framework/commit/1655667)) -* **route:** added options to add format to routes ([cfe6c5c](https://github.com/adonisjs/adonis-framework/commit/cfe6c5c)) -* **route-resource:** added support for nested resources and resources filters ([907014e](https://github.com/adonisjs/adonis-framework/commit/907014e)) -* **routes:** added middleware alias and added support for multiple params ([51cf673](https://github.com/adonisjs/adonis-framework/commit/51cf673)) - -### Performance Improvements - -* **config:** remove auto-load with require-all for performance ([806aae2](https://github.com/adonisjs/adonis-framework/commit/806aae2)) -* **env,file,helpers:** improved initial datatypes to help v8 set hidden classes ([79bd6b4](https://github.com/adonisjs/adonis-framework/commit/79bd6b4)) -* **middleware,route,server,session:** improved variables initialization to keep v8 happy ([20080ec](https://github.com/adonisjs/adonis-framework/commit/20080ec)) -* **request.format:** added acceptance for request.format ([4ed82c2](https://github.com/adonisjs/adonis-framework/commit/4ed82c2)) - - - - -## [2.0.7](https://github.com/adonisjs/adonis-framework/compare/v2.0.3...v2.0.7) (2016-01-17) - - -### Bug Fixes - -* **request:** method is and accepts have been fixed to treat arrays ([9d8e963](https://github.com/adonisjs/adonis-framework/commit/9d8e963)) -* **session:** now list of drivers is set to an empty object by default ([3bb75e0](https://github.com/adonisjs/adonis-framework/commit/3bb75e0)) - -### Features - -* **package.json:** make the repository commitizen-friendly ([0082c4a](https://github.com/adonisjs/adonis-framework/commit/0082c4a)) -* **request:** add match method to match an array of patterns to current url ([a81a4f7](https://github.com/adonisjs/adonis-framework/commit/a81a4f7)) -* **request:** Added raw method to access raw data sent to a given request ([00de598](https://github.com/adonisjs/adonis-framework/commit/00de598)) - - - - -## 2.0.6 (2016-01-16) - - -### docs - -* docs: add CONTRIBUTING.md file ([ab7afdb](https://github.com/adonisjs/adonis-framework/commit/ab7afdb)) -* docs: update the build badge to get the status from master branch ([9c5c61f](https://github.com/adonisjs/adonis-framework/commit/9c5c61f)) - -* add trello badge ([7c57fe3](https://github.com/adonisjs/adonis-framework/commit/7c57fe3)) -* Config provider now only reads .js files ([dcc7aee](https://github.com/adonisjs/adonis-framework/commit/dcc7aee)) -* correct all license date ([9f5fd24](https://github.com/adonisjs/adonis-framework/commit/9f5fd24)) -* delete lowercase readme ([6c12f92](https://github.com/adonisjs/adonis-framework/commit/6c12f92)) -* Improved tests coverage ([07efb17](https://github.com/adonisjs/adonis-framework/commit/07efb17)) -* Merge branch 'master' of github.com:adonisjs/adonis-framework ([2eaa793](https://github.com/adonisjs/adonis-framework/commit/2eaa793)) -* Merge branch 'release-2.0.3' into develop ([d5a3cb4](https://github.com/adonisjs/adonis-framework/commit/d5a3cb4)) -* Merge branch 'release-2.0.4' ([c4405bf](https://github.com/adonisjs/adonis-framework/commit/c4405bf)) -* Merge pull request #42 from alexbooker/patch-1 ([9a2d4be](https://github.com/adonisjs/adonis-framework/commit/9a2d4be)) -* Merge pull request #45 from RomainLanz/develop ([643ff72](https://github.com/adonisjs/adonis-framework/commit/643ff72)) -* Merge pull request #46 from adonisjs/revert-42-patch-1 ([c7e6471](https://github.com/adonisjs/adonis-framework/commit/c7e6471)) -* Merge pull request #47 from RomainLanz/develop ([27cb1d5](https://github.com/adonisjs/adonis-framework/commit/27cb1d5)) -* Merge pull request #48 from RomainLanz/develop ([949a06f](https://github.com/adonisjs/adonis-framework/commit/949a06f)) -* Merge pull request #61 from RomainLanz/feature/improving-readme ([0dbafa8](https://github.com/adonisjs/adonis-framework/commit/0dbafa8)) -* Merge pull request #63 from RomainLanz/update-readme-badges ([c52f989](https://github.com/adonisjs/adonis-framework/commit/c52f989)) -* Merge pull request #64 from RomainLanz/update-lodash ([bcaf01a](https://github.com/adonisjs/adonis-framework/commit/bcaf01a)) -* Merge pull request #65 from RomainLanz/contributing ([4f5fd0b](https://github.com/adonisjs/adonis-framework/commit/4f5fd0b)) -* Merge pull request #67 from RomainLanz/commitizen ([ff6d94f](https://github.com/adonisjs/adonis-framework/commit/ff6d94f)) -* Merged release 2.0.5 ([222bab7](https://github.com/adonisjs/adonis-framework/commit/222bab7)) -* Moved route resolution to callback method, required for method spoofing ([839791a](https://github.com/adonisjs/adonis-framework/commit/839791a)) -* new readme version ([81169c9](https://github.com/adonisjs/adonis-framework/commit/81169c9)) -* Now all files are dependent upon config directory and not reading from .env file ([f2ff04f](https://github.com/adonisjs/adonis-framework/commit/f2ff04f)) -* Now param method accepts a default value ([adcd7fb](https://github.com/adonisjs/adonis-framework/commit/adcd7fb)) -* npm version bump ([0b6693e](https://github.com/adonisjs/adonis-framework/commit/0b6693e)) -* npm version bump ([0d6b456](https://github.com/adonisjs/adonis-framework/commit/0d6b456)) -* Revert "Updated the licence date" ([102ad50](https://github.com/adonisjs/adonis-framework/commit/102ad50)) -* update license date and add license file ([2aa3412](https://github.com/adonisjs/adonis-framework/commit/2aa3412)) -* update shields badges ([6e932f5](https://github.com/adonisjs/adonis-framework/commit/6e932f5)) -* Updated the licence date ([e881bd6](https://github.com/adonisjs/adonis-framework/commit/e881bd6)) - -### feat - -* feat(package.json): make the repository commitizen-friendly ([0082c4a](https://github.com/adonisjs/adonis-framework/commit/0082c4a)) -* feat(request): add match method to match an array of patterns to current url ([a81a4f7](https://github.com/adonisjs/adonis-framework/commit/a81a4f7)) -* feat(request): Added raw method to access raw data sent to a given request ([00de598](https://github.com/adonisjs/adonis-framework/commit/00de598)) - -### refactor - -* refactor: update lodash to 4.0.0 ([ad1cbdc](https://github.com/adonisjs/adonis-framework/commit/ad1cbdc)) -* refactor(response): Capitalized x-powered-by ([ed3d3dc](https://github.com/adonisjs/adonis-framework/commit/ed3d3dc)) -* refactor(server): Increased static server priority over route handler ([30cfe41](https://github.com/adonisjs/adonis-framework/commit/30cfe41)) -* refactor(session): improved session drivers handling and exposing session manager ([a17a49b](https://github.com/adonisjs/adonis-framework/commit/a17a49b)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 256a12e0..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,6 +0,0 @@ -# Contributing - -In favor of active development we accept contributions from everyone. You can contribute by submitting a bug, creating pull requests or even by improving documentation. - -Below is the guide to be followed strictly before submitting your pull requests. -http://adonisjs.com/docs/2.0/contributing diff --git a/LICENSE.txt b/LICENSE.md similarity index 82% rename from LICENSE.txt rename to LICENSE.md index ce1901fa..1c194285 100644 --- a/LICENSE.txt +++ b/LICENSE.md @@ -1,8 +1,9 @@ -The MIT License (MIT) -Copyright (c) 2016 Harminder Virk +# The MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright 2022 Harminder Virk, contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 92a9c24f..e5829b7c 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,47 @@ -# AdonisJS Framework - -[![Gitter](https://img.shields.io/badge/+%20GITTER-JOIN%20CHAT%20%E2%86%92-1DCE73.svg?style=flat-square)](https://gitter.im/adonisjs/adonis-framework) -[![Trello](https://img.shields.io/badge/TRELLO-%E2%86%92-89609E.svg?style=flat-square)](https://trello.com/b/yzpqCgdl/adonis-for-humans) -[![Version](https://img.shields.io/npm/v/adonis-framework.svg?style=flat-square)](https://www.npmjs.com/package/adonis-framework) -[![Build Status](https://img.shields.io/travis/adonisjs/adonis-framework/master.svg?style=flat-square)](https://travis-ci.org/adonisjs/adonis-framework) -[![Coverage Status](https://img.shields.io/coveralls/adonisjs/adonis-framework/master.svg?style=flat-square)](https://coveralls.io/github/adonisjs/adonis-framework?branch=master) -[![Downloads](https://img.shields.io/npm/dt/adonis-framework.svg?style=flat-square)](https://www.npmjs.com/package/adonis-framework) -[![License](https://img.shields.io/npm/l/adonis-framework.svg?style=flat-square)](https://opensource.org/licenses/MIT) - -> :pray: This repository contains the core of the AdonisJS framework. - -Adonis is a MVC framework for NodeJS built on solid foundations. - -It is the first NodeJS framework with support for [Dependency Injection](http://adonisjs.com/docs/2.0/dependency-injection) and has a lean [IoC Container](http://adonisjs.com/docs/2.0/ioc-container) to resolve and mock dependencies. It borrows the concept of [Service Providers](http://adonisjs.com/docs/2.0/service-providers) from the popular [PHP framework Laravel](https://laravel.com) to write scalable applications. - -You can learn more about AdonisJS and all of its awesomeness on http://adonisjs.com :evergreen_tree: - -## Table of Contents - -* [Team Members](#team-members) -* [Requirements](#requirements) -* [Getting Started](#getting-started) -* [Contribution Guidelines](#contribution-guidelines) - -## Team Members - -* Harminder Virk ([Caffiene Blogging](http://amanvirk.me/)) - -## Requirements - -AdonisJS is build on the top of ES2015, which makes the code more enjoyable and cleaner to read. It doesn't make use of any transpiler and depends upon Core V8 implemented features. - -For these reasons, AdonisJS require you to use `node >= 4.0` and `npm >= 3.0`. - -## Getting Started - -AdonisJS provide a [CLI tool](https://github.com/AdonisJs/adonis-cli) to scaffold and generate a project with all required dependencies. - -```bash -$ npm install -g adonis-cli -``` - -```bash -$ adonis new awesome-project -$ cd awesome-project -$ npm run start -``` - -[Official Documentation](http://adonisjs.com/docs/2.0/installation) - -## Contribution Guidelines - -In favor of active development we accept contributions for everyone. You can contribute by submitting a bug, creating pull requests or even improving documentation. - -You can find a complete guide to be followed strictly before submitting your pull requests in the [Official Documentation](http://adonisjs.com/docs/2.0/contributing). +# @adonisjs/core + +![](https://github.com/thetutlage/static/blob/main/sponsorkit/sponsors.png?raw=true) + +
+
+ +
+

Fullstack MVC framework for Node.js

+

AdonisJs is a fullstack Web framework with focus on ergonomics and speed . It takes care of much of the Web development hassles, offering you a clean and stable API to build Web apps and micro services.

+
+ +
+ +
+ +[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] + +
+ +
+

+ + Website + + | + + Guides + + | + + Contributing + +

+
+ +
+ Built with ❤︎ by Harminder Virk +
+ +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/core/checks.yml?branch=develop&label=Tests&style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/core/actions/workflows/checks.yml 'Github action' +[npm-image]: https://img.shields.io/npm/v/@adonisjs/core/latest.svg?style=for-the-badge&logo=npm +[npm-url]: https://www.npmjs.com/package/@adonisjs/core/v/latest 'npm' +[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript +[license-url]: LICENSE.md +[license-image]: https://img.shields.io/github/license/adonisjs/adonis-framework?style=for-the-badge diff --git a/benchmarks/autoload.js b/benchmarks/autoload.js deleted file mode 100644 index f1cedb0b..00000000 --- a/benchmarks/autoload.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict' - -const Benchmark = require('benchmark') -const suite = new Benchmark.Suite -const requireDirectory = require('require-directory') -const autoLoad = require('auto-loader') -const path = require('path') -const dir = path.join(__dirname, '../src') - -const loadViaReqDir = function () { - return requireDirectory(module,dir) -} - -const loadViaAutoLoad = function () { - return autoLoad(dir) -} - - -suite.add('via require-directory', function () { - loadViaReqDir() -}) -.add('via auto-load', function () { - loadViaAutoLoad() -}) -.on('cycle', function(event) { - console.log(String(event.target)); -}) -.on('complete', function() { - console.log('Fastest is ' + this.filter('fastest').pluck('name')); -}) -// run async -.run({ 'async': true }); diff --git a/bin/test.ts b/bin/test.ts new file mode 100644 index 00000000..d1e6fa70 --- /dev/null +++ b/bin/test.ts @@ -0,0 +1,34 @@ +import { assert } from '@japa/assert' +import { snapshot } from '@japa/snapshot' +import { fileSystem } from '@japa/file-system' +import { expectTypeOf } from '@japa/expect-type' +import { processCLIArgs, configure, run } from '@japa/runner' + +/* +|-------------------------------------------------------------------------- +| Configure tests +|-------------------------------------------------------------------------- +| +| The configure method accepts the configuration to configure the Japa +| tests runner. +| +| The first method call "processCLIArgs" process the command line arguments +| and turns them into a config object. Using this method is not mandatory. +| +| Please consult japa.dev/runner-config for the config docs. +*/ +processCLIArgs(process.argv.slice(2)) +configure({ + files: ['tests/**/*.spec.ts'], + plugins: [assert(), expectTypeOf(), fileSystem(), snapshot()], +}) + +/* +|-------------------------------------------------------------------------- +| Run tests +|-------------------------------------------------------------------------- +| +| The following "run" method is required to execute all the tests. +| +*/ +run() diff --git a/commands/add.ts b/commands/add.ts new file mode 100644 index 00000000..34913fea --- /dev/null +++ b/commands/add.ts @@ -0,0 +1,165 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { detectPackageManager, installPackage } from '@antfu/install-pkg' + +import { CommandOptions } from '../types/ace.js' +import { args, BaseCommand, flags } from '../modules/ace/main.js' + +const KNOWN_PACKAGE_MANAGERS = ['npm', 'pnpm', 'bun', 'yarn', 'yarn@berry', 'pnpm@6'] as const + +/** + * The install command is used to `npm install` and `node ace configure` a new package + * in one go. + */ +export default class Add extends BaseCommand { + static commandName = 'add' + static description = 'Install and configure a package' + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'Package name' }) + declare name: string + + @flags.boolean({ description: 'Display logs in verbose mode' }) + declare verbose?: boolean + + @flags.string({ description: 'Select the package manager you want to use' }) + declare packageManager?: (typeof KNOWN_PACKAGE_MANAGERS)[number] + + @flags.boolean({ description: 'Should we install the package as a dev dependency', alias: 'D' }) + declare dev?: boolean + + @flags.boolean({ description: 'Forcefully overwrite existing files' }) + declare force?: boolean + + /** + * Detect the package manager to use + */ + async #getPackageManager() { + const packageManager = + this.packageManager || (await detectPackageManager(this.app.makePath())) || 'npm' + + if ( + KNOWN_PACKAGE_MANAGERS.some((knownPackageManager) => knownPackageManager === packageManager) + ) { + return packageManager as (typeof KNOWN_PACKAGE_MANAGERS)[number] | undefined + } + + throw new Error('Invalid package manager. Must be one of npm, pnpm, bun or yarn') + } + + /** + * Configure the package by delegating the work to the `node ace configure` command + */ + async #configurePackage() { + /** + * Sending unknown flags to the configure command + */ + const flagValueArray = this.parsed.unknownFlags + .filter((flag) => !!this.parsed.flags[flag]) + .map((flag) => [`--${flag}`, this.parsed.flags[flag].toString()]) + + const configureArgs = [ + this.name, + this.force ? '--force' : undefined, + this.verbose ? '--verbose' : undefined, + ...flagValueArray.flat(), + ].filter(Boolean) as string[] + + return await this.kernel.exec('configure', configureArgs) + } + + /** + * Install the package using the selected package manager + */ + async #installPackage(npmPackageName: string) { + const colors = this.colors + const spinner = this.logger + .await(`installing ${colors.green(this.name)} using ${colors.grey(this.packageManager!)}`) + .start() + + spinner.start() + + try { + await installPackage(npmPackageName, { + dev: this.dev, + silent: this.verbose === true ? false : true, + cwd: this.app.makePath(), + packageManager: this.packageManager, + }) + + spinner.update('package installed successfully') + spinner.stop() + + return true + } catch (error) { + spinner.update('unable to install the package') + spinner.stop() + + this.logger.fatal(error) + this.exitCode = 1 + return false + } + } + + /** + * Run method is invoked by ace automatically + */ + async run() { + const colors = this.colors + this.packageManager = await this.#getPackageManager() + + /** + * Handle special packages to configure + */ + let npmPackageName = this.name + if (this.name === 'vinejs') { + npmPackageName = '@vinejs/vine' + } else if (this.name === 'edge') { + npmPackageName = 'edge.js' + } + + /** + * Prompt the user to confirm the installation + */ + const cmd = colors.grey(`${this.packageManager} add ${this.dev ? '-D ' : ''}${this.name}`) + this.logger.info(`Installing the package using the following command : ${cmd}`) + + const shouldInstall = await this.prompt.confirm('Continue ?', { + name: 'install', + default: true, + }) + + if (!shouldInstall) { + this.logger.info('Installation cancelled') + return + } + + /** + * Install package + */ + const pkgWasInstalled = await this.#installPackage(npmPackageName) + if (!pkgWasInstalled) { + return + } + + /** + * Configure package + */ + const { exitCode } = await this.#configurePackage() + this.exitCode = exitCode + if (exitCode === 0) { + this.logger.success(`Installed and configured ${colors.green(this.name)}`) + } else { + this.logger.fatal(`Unable to configure ${colors.green(this.name)}`) + } + } +} diff --git a/commands/build.ts b/commands/build.ts new file mode 100644 index 00000000..c13d5743 --- /dev/null +++ b/commands/build.ts @@ -0,0 +1,129 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand, flags } from '../modules/ace/main.js' +import { detectAssetsBundler, importAssembler, importTypeScript } from '../src/internal_helpers.js' + +/** + * Create the production build by compiling TypeScript source and the + * frontend assets + */ +export default class Build extends BaseCommand { + static commandName = 'build' + static description = + 'Build application for production by compiling frontend assets and TypeScript source to JavaScript' + + static help = [ + 'Create the production build using the following command.', + '```', + '{{ binaryName }} build', + '```', + '', + 'The assets bundler dev server runs automatically after detecting vite config or webpack config files', + 'You may pass vite CLI args using the --assets-args command line flag.', + '```', + '{{ binaryName }} build --assets-args="--debug --base=/public"', + '```', + ] + + @flags.boolean({ description: 'Ignore TypeScript errors and continue with the build process' }) + declare ignoreTsErrors?: boolean + + @flags.string({ + description: 'Define the package manager to copy the appropriate lock file', + }) + declare packageManager?: 'npm' | 'pnpm' | 'yarn' | 'yarn@berry' | 'bun' + + @flags.boolean({ + description: 'Build frontend assets', + showNegatedVariantInHelp: true, + default: true, + }) + declare assets?: boolean + + @flags.array({ + description: 'Define CLI arguments to pass to the assets bundler', + }) + declare assetsArgs?: string[] + + /** + * Log a development dependency is missing + */ + #logMissingDevelopmentDependency(dependency: string) { + this.logger.error( + [ + `Cannot find package "${dependency}"`, + '', + `The "${dependency}" package is a development dependency and therefore you should use the build command with development dependencies installed.`, + '', + 'If you are using the build command inside a CI or with a deployment platform, make sure the NODE_ENV is set to "development"', + ].join('\n') + ) + } + + /** + * Returns the assets bundler config + */ + async #getAssetsBundlerConfig() { + const assetsBundler = await detectAssetsBundler(this.app) + return assetsBundler + ? { + enabled: this.assets === false ? false : true, + driver: assetsBundler.name, + cmd: assetsBundler.build.command, + args: (assetsBundler.build.args || []).concat(this.assetsArgs || []), + } + : { + enabled: false as const, + } + } + + /** + * Build application + */ + async run() { + const assembler = await importAssembler(this.app) + if (!assembler) { + this.#logMissingDevelopmentDependency('@adonisjs/assembler') + this.exitCode = 1 + return + } + + const ts = await importTypeScript(this.app) + if (!ts) { + this.#logMissingDevelopmentDependency('typescript') + this.exitCode = 1 + return + } + + const bundler = new assembler.Bundler(this.app.appRoot, ts, { + assets: await this.#getAssetsBundlerConfig(), + metaFiles: this.app.rcFile.metaFiles, + hooks: { + onBuildStarting: this.app.rcFile.hooks?.onBuildStarting, + onBuildCompleted: this.app.rcFile.hooks?.onBuildCompleted, + }, + }) + + /** + * Share command logger with assembler, so that CLI flags like --no-ansi has + * similar impact for assembler logs as well. + */ + bundler.setLogger(this.logger) + + /** + * Bundle project for production + */ + const stopOnError = this.ignoreTsErrors === true ? false : true + const builtSuccessfully = await bundler.bundle(stopOnError, this.packageManager) + if (!builtSuccessfully) { + this.exitCode = 1 + } + } +} diff --git a/commands/configure.ts b/commands/configure.ts new file mode 100644 index 00000000..e8787290 --- /dev/null +++ b/commands/configure.ts @@ -0,0 +1,177 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../stubs/main.js' +import type { CommandOptions } from '../types/ace.js' +import { args, BaseCommand, flags } from '../modules/ace/main.js' +import { RuntimeException } from '@poppinss/utils' + +/** + * The configure command is used to configure packages after installation + */ +export default class Configure extends BaseCommand { + static commandName = 'configure' + static description = 'Configure a package after it has been installed' + static options: CommandOptions = { + allowUnknownFlags: true, + } + + /** + * Exposing all flags from the protected property "parsed" + */ + get parsedFlags() { + return this.parsed.flags + } + + /** + * Exposing all args from the protected property "parsed" + */ + get parsedArgs() { + return this.parsed._ + } + + /** + * Name of the package to configure + */ + @args.string({ description: 'Package name' }) + declare name: string + + /** + * Turn on verbose mode for packages installation + */ + @flags.boolean({ description: 'Display logs in verbose mode', alias: 'v' }) + declare verbose?: boolean + + /** + * Forcefully overwrite existing files. + */ + @flags.boolean({ description: 'Forcefully overwrite existing files', alias: 'f' }) + declare force?: boolean + + /** + * The root of the stubs directory. The value is defined after we import + * the package + */ + declare stubsRoot: string + + /** + * Returns the package main exports + */ + async #getPackageSource(packageName: string) { + try { + const packageExports = await this.app.import(packageName) + return packageExports + } catch (error) { + if ( + (error.code && error.code === 'ERR_MODULE_NOT_FOUND') || + error.message.startsWith('Cannot find module') + ) { + return null + } + throw error + } + } + + /** + * Registers VineJS provider + */ + async #configureVineJS() { + const codemods = await this.createCodemods() + await codemods.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/core/providers/vinejs_provider') + }) + } + + /** + * Registers Edge provider + */ + async #configureEdge() { + const codemods = await this.createCodemods() + await codemods.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/core/providers/edge_provider') + rcFile.addMetaFile('resources/views/**/*.edge', false) + }) + } + + /** + * Configure health checks + */ + async #configureHealthChecks() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, 'make/health/main.stub', { + flags: this.parsed.flags, + entity: this.app.generators.createEntity('health'), + }) + await codemods.makeUsingStub(stubsRoot, 'make/health/controller.stub', { + flags: this.parsed.flags, + entity: this.app.generators.createEntity('health_checks'), + }) + } + + /** + * Creates codemods as per configure command options + */ + async createCodemods() { + const codemods = await super.createCodemods() + codemods.overwriteExisting = this.force === true + codemods.verboseInstallOutput = this.verbose === true + return codemods + } + + /** + * Run method is invoked by ace automatically + */ + async run() { + if (this.name === 'vinejs') { + return this.#configureVineJS() + } + if (this.name === 'edge') { + return this.#configureEdge() + } + if (this.name === 'health_checks') { + return this.#configureHealthChecks() + } + + const packageExports = await this.#getPackageSource(this.name) + if (!packageExports) { + this.logger.error(`Cannot find module "${this.name}". Make sure to install it`) + this.exitCode = 1 + return + } + + /** + * Warn, there are not instructions to run + */ + if (!packageExports.configure) { + this.logger.error( + `Cannot configure module "${this.name}". The module does not export the configure hook` + ) + this.exitCode = 1 + return + } + + /** + * Set stubsRoot property when package exports it + */ + if (packageExports.stubsRoot) { + this.stubsRoot = packageExports.stubsRoot + } + + /** + * Run instructions + */ + try { + await packageExports.configure(this) + } catch (error) { + throw new RuntimeException(`Unable to configure package "${this.name}"`, { + cause: error, + }) + } + } +} diff --git a/commands/eject.ts b/commands/eject.ts new file mode 100644 index 00000000..f4cc140a --- /dev/null +++ b/commands/eject.ts @@ -0,0 +1,40 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { slash } from '@poppinss/utils' +import { args, BaseCommand, flags } from '../modules/ace/main.js' + +/** + * The eject command is used to eject templates to the user + * application codebase for customizing them + */ +export default class Eject extends BaseCommand { + static commandName = 'eject' + static description = 'Eject scaffolding stubs to your application root' + + @args.string({ description: 'Path to the stubs directory or a single stub file' }) + declare stubPath: string + + @flags.string({ + description: 'Mention package name for searching stubs', + default: '@adonisjs/core', + }) + declare pkg: string + + async run() { + const stubs = await this.app.stubs.create() + const copied = await stubs.copy(this.stubPath, { + pkg: this.pkg, + }) + + copied.forEach((stubPath) => { + this.logger.success(`eject ${slash(this.app.relativePath(stubPath))}`) + }) + } +} diff --git a/commands/env/add.ts b/commands/env/add.ts new file mode 100644 index 00000000..c70d60c2 --- /dev/null +++ b/commands/env/add.ts @@ -0,0 +1,120 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { CommandOptions } from '../../types/ace.js' +import stringHelpers from '../../src/helpers/string.js' +import { args, BaseCommand, flags } from '../../modules/ace/main.js' + +const ALLOWED_TYPES = ['string', 'boolean', 'number', 'enum'] as const +type AllowedTypes = (typeof ALLOWED_TYPES)[number] + +/** + * The env:add command is used to add a new environment variable to the + * `.env`, `.env.example` and `start/env.ts` files. + */ +export default class EnvAdd extends BaseCommand { + static commandName = 'env:add' + static description = 'Add a new environment variable' + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ + description: 'Variable name. Will be converted to screaming snake case', + required: false, + }) + declare name: string + + @args.string({ description: 'Variable value', required: false }) + declare value: string + + @flags.string({ description: 'Type of the variable' }) + declare type: AllowedTypes + + @flags.array({ + description: 'Allowed values for the enum type in a comma-separated list', + default: [''], + required: false, + }) + declare enumValues: string[] + + /** + * Validate the type flag passed by the user + */ + #isTypeFlagValid() { + return ALLOWED_TYPES.includes(this.type) + } + + async run() { + /** + * Prompt for missing name + */ + if (!this.name) { + this.name = await this.prompt.ask('Enter the variable name', { + validate: (value) => !!value, + format: (value) => stringHelpers.snakeCase(value).toUpperCase(), + }) + } + + /** + * Prompt for missing value + */ + if (!this.value) { + this.value = await this.prompt.ask('Enter the variable value') + } + + /** + * Prompt for missing type + */ + if (!this.type) { + this.type = await this.prompt.choice('Select the variable type', ALLOWED_TYPES) + } + + /** + * Prompt for missing enum values if the selected env type is `enum` + */ + if (this.type === 'enum' && !this.enumValues) { + this.enumValues = await this.prompt.ask('Enter the enum values separated by a comma', { + result: (value) => value.split(',').map((one) => one.trim()), + }) + } + + /** + * Validate inputs + */ + if (!this.#isTypeFlagValid()) { + this.logger.error(`Invalid type "${this.type}". Must be one of ${ALLOWED_TYPES.join(', ')}`) + return + } + + /** + * Add the environment variable to the `.env` and `.env.example` files + */ + const codemods = await this.createCodemods() + const transformedName = stringHelpers.snakeCase(this.name).toUpperCase() + await codemods.defineEnvVariables( + { [transformedName]: this.value }, + { omitFromExample: [transformedName] } + ) + + /** + * Add the environment variable to the `start/env.ts` file + */ + const validation = { + string: 'Env.schema.string()', + number: 'Env.schema.number()', + boolean: 'Env.schema.boolean()', + enum: `Env.schema.enum(['${this.enumValues.join("','")}'] as const)`, + }[this.type] + + await codemods.defineEnvValidations({ variables: { [transformedName]: validation } }) + + this.logger.success('Environment variable added successfully') + } +} diff --git a/commands/generate_key.ts b/commands/generate_key.ts new file mode 100644 index 00000000..e9731a83 --- /dev/null +++ b/commands/generate_key.ts @@ -0,0 +1,53 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import string from '@poppinss/utils/string' +import { EnvEditor } from '@adonisjs/env/editor' +import { BaseCommand, flags } from '../modules/ace/main.js' + +/** + * The generate key command is used to generate the app key + * and write it inside the .env file. + */ +export default class GenerateKey extends BaseCommand { + static commandName = 'generate:key' + static description = 'Generate a cryptographically secure random application key' + + @flags.boolean({ + description: 'Display the key on the terminal, instead of writing it to .env file', + }) + declare show: boolean + + @flags.boolean({ + description: 'Force update .env file in production environment', + }) + declare force: boolean + + async run() { + let writeToFile = process.env.NODE_ENV !== 'production' + if (this.force) { + writeToFile = true + } + + if (this.show) { + writeToFile = false + } + + const secureKey = string.random(32) + + if (writeToFile) { + const editor = await EnvEditor.create(this.app.appRoot) + editor.add('APP_KEY', secureKey, true) + await editor.save() + this.logger.action('add APP_KEY to .env').succeeded() + } else { + this.logger.log(`APP_KEY = ${secureKey}`) + } + } +} diff --git a/commands/inspect_rcfile.ts b/commands/inspect_rcfile.ts new file mode 100644 index 00000000..c8fc1cef --- /dev/null +++ b/commands/inspect_rcfile.ts @@ -0,0 +1,46 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand } from '../modules/ace/main.js' + +/** + * Prints the RcFile file contents to the terminal + */ +export default class InspectRCFile extends BaseCommand { + static commandName = 'inspect:rcfile' + static description = 'Inspect the RC file with its default values' + + async run() { + const { raw, providers, preloads, commands, ...rest } = this.app.rcFile + this.logger.log( + JSON.stringify( + { + ...rest, + providers: providers.map((provider) => { + return { + ...provider, + file: provider.file.toString(), + } + }), + preloads: preloads.map((preload) => { + return { + ...preload, + file: preload.file.toString(), + } + }), + commands: commands.map((command) => { + return command.toString() + }), + }, + null, + 2 + ) + ) + } +} diff --git a/commands/list/routes.ts b/commands/list/routes.ts new file mode 100644 index 00000000..f74156ac --- /dev/null +++ b/commands/list/routes.ts @@ -0,0 +1,121 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { CommandOptions } from '../../types/ace.js' +import { args, BaseCommand, flags } from '../../modules/ace/main.js' +import { RoutesListFormatter } from '../../src/cli_formatters/routes_list.js' + +/** + * The list routes command is used to view the list of registered routes + */ +export default class ListRoutes extends BaseCommand { + static commandName = 'list:routes' + static description = + 'List application routes. This command will boot the application in the console environment' + + /** + * Making sure to start the application so that the routes are + * imported + */ + static options: CommandOptions = { + startApp: true, + } + + /** + * The match filter is used to find route by name, pattern and controller name that + * includes the match keyword + */ + @args.string({ + description: + 'Find routes matching the given keyword. Route name, pattern and controller name will be searched against the keyword', + required: false, + }) + declare match: string + + /** + * The middleware flag searches for the routes using all the mentioned middleware + */ + @flags.array({ + description: + 'View routes that includes all the mentioned middleware names. Use * to see routes that are using one or more middleware', + }) + declare middleware: string[] + + /** + * The ignoreMiddleware flag searches for the routes not using all the mentioned middleware + */ + @flags.array({ + description: + 'View routes that does not include all the mentioned middleware names. Use * to see routes that are using zero middleware', + }) + declare ignoreMiddleware: string[] + + /** + * The json flag is used to view list of routes as a JSON string. + */ + @flags.boolean({ description: 'Get routes list as a JSON string' }) + declare json: boolean + + /** + * The table flag is used to view list of routes as a classic CLI table + */ + @flags.boolean({ description: 'View list of routes as a table' }) + declare table: boolean + + async run() { + const router = await this.app.container.make('router') + const formatter = new RoutesListFormatter( + router, + this.ui, + {}, + { + ignoreMiddleware: this.ignoreMiddleware, + middleware: this.middleware, + match: this.match, + } + ) + + /** + * Display as JSON + */ + if (this.json) { + this.logger.log(JSON.stringify(await formatter.formatAsJSON(), null, 2)) + return + } + + /** + * Display as a standard table + */ + if (this.table) { + const tables = await formatter.formatAsAnsiTable() + tables.forEach((table) => { + this.logger.log('') + if (table.heading) { + this.logger.log(table.heading) + this.logger.log('') + } + table.table.render() + }) + return + } + + /** + * Display as a list + */ + const list = await formatter.formatAsAnsiList() + list.forEach((item) => { + this.logger.log('') + if (item.heading) { + this.logger.log(item.heading) + this.logger.log('') + } + this.logger.log(item.rows.join('\n')) + }) + } +} diff --git a/commands/make/command.ts b/commands/make/command.ts new file mode 100644 index 00000000..9eb0a9a5 --- /dev/null +++ b/commands/make/command.ts @@ -0,0 +1,36 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args } from '../../modules/ace/main.js' +import { BaseCommand } from '../../modules/ace/main.js' + +/** + * Make a new ace command + */ +export default class MakeCommand extends BaseCommand { + static commandName = 'make:command' + static description = 'Create a new ace command class' + + @args.string({ description: 'Name of the command' }) + declare name: string + + /** + * The stub to use for generating the command class + */ + protected stubPath: string = 'make/command/main.stub' + + async run() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } +} diff --git a/commands/make/controller.ts b/commands/make/controller.ts new file mode 100644 index 00000000..5968988a --- /dev/null +++ b/commands/make/controller.ts @@ -0,0 +1,106 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import string from '@poppinss/utils/string' +import { stubsRoot } from '../../stubs/main.js' +import { args, flags, BaseCommand } from '../../modules/ace/main.js' +import { CommandOptions } from '../../types/ace.js' + +/** + * The make controller command to create an HTTP controller + */ +export default class MakeController extends BaseCommand { + static commandName = 'make:controller' + static description = 'Create a new HTTP controller class' + + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'The name of the controller' }) + declare name: string + + @args.spread({ description: 'Create controller with custom method names', required: false }) + declare actions?: string[] + + @flags.boolean({ + description: 'Generate controller in singular form', + alias: 's', + }) + declare singular: boolean + + @flags.boolean({ + description: + 'Generate resourceful controller with methods to perform CRUD actions on a resource', + alias: 'r', + }) + declare resource: boolean + + @flags.boolean({ + description: 'Generate resourceful controller without the "edit" and the "create" methods', + alias: 'a', + }) + declare api: boolean + + /** + * The stub to use for generating the controller + */ + protected stubPath: string = 'make/controller/main.stub' + + /** + * Preparing the command state + */ + async prepare() { + /** + * Use actions stub + */ + if (this.actions) { + this.stubPath = 'make/controller/actions.stub' + } + + /** + * Use resource stub + */ + if (this.resource) { + if (this.actions) { + this.logger.warning('Cannot use --resource flag with actions. Ignoring --resource') + } else { + this.stubPath = 'make/controller/resource.stub' + } + } + + /** + * Use api stub + */ + if (this.api) { + if (this.actions) { + this.logger.warning('Cannot use --api flag with actions. Ignoring --api') + } else { + this.stubPath = 'make/controller/api.stub' + } + } + + /** + * Log warning when both flags are used together + */ + if (this.resource && this.api && !this.actions) { + this.logger.warning('--api and --resource flags cannot be used together. Ignoring --resource') + } + } + + async run() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + actions: this.actions?.map((action) => string.camelCase(action)), + entity: this.app.generators.createEntity(this.name), + singular: this.singular, + }) + } +} diff --git a/commands/make/event.ts b/commands/make/event.ts new file mode 100644 index 00000000..82dc97dd --- /dev/null +++ b/commands/make/event.ts @@ -0,0 +1,40 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args, BaseCommand } from '../../modules/ace/main.js' +import { CommandOptions } from '../../types/ace.js' + +/** + * The make event command to create a class based event + */ +export default class MakeEvent extends BaseCommand { + static commandName = 'make:event' + static description = 'Create a new event class' + + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'Name of the event' }) + declare name: string + + /** + * The stub to use for generating the event + */ + protected stubPath: string = 'make/event/main.stub' + + async run() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } +} diff --git a/commands/make/exception.ts b/commands/make/exception.ts new file mode 100644 index 00000000..dfa006c5 --- /dev/null +++ b/commands/make/exception.ts @@ -0,0 +1,40 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args, BaseCommand } from '../../modules/ace/main.js' +import { CommandOptions } from '../../types/ace.js' + +/** + * Make a new exception class + */ +export default class MakeException extends BaseCommand { + static commandName = 'make:exception' + static description = 'Create a new custom exception class' + + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'Name of the exception' }) + declare name: string + + /** + * The stub to use for generating the command class + */ + protected stubPath: string = 'make/exception/main.stub' + + async run() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } +} diff --git a/commands/make/listener.ts b/commands/make/listener.ts new file mode 100644 index 00000000..fb8de2ab --- /dev/null +++ b/commands/make/listener.ts @@ -0,0 +1,72 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args, flags, BaseCommand } from '../../modules/ace/main.js' +import { CommandOptions } from '../../types/ace.js' + +/** + * The make listener command to create a class based event + * listener + */ +export default class MakeListener extends BaseCommand { + static commandName = 'make:listener' + static description = 'Create a new event listener class' + + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'Name of the event listener' }) + declare name: string + + @flags.string({ + description: 'Generate an event class alongside the listener', + alias: 'e', + }) + declare event: string + + /** + * The stub to use for generating the event listener + */ + protected stubPath: string = 'make/listener/main.stub' + + prepare() { + if (this.event) { + this.stubPath = 'make/listener/for_event.stub' + } + } + + async run() { + const codemods = await this.createCodemods() + + if (this.event) { + const { exitCode } = await this.kernel.exec('make:event', [this.event]) + + /** + * Create listener only when make:event is completed successfully + */ + if (exitCode === 0) { + const eventEntity = this.app.generators.createEntity(this.event) + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + event: eventEntity, + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } + + return + } + + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } +} diff --git a/commands/make/middleware.ts b/commands/make/middleware.ts new file mode 100644 index 00000000..a51676e7 --- /dev/null +++ b/commands/make/middleware.ts @@ -0,0 +1,99 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { slash } from '@poppinss/utils' +import string from '@poppinss/utils/string' +import { basename, extname, relative } from 'node:path' + +import { stubsRoot } from '../../stubs/main.js' +import { args, BaseCommand, flags } from '../../modules/ace/main.js' +import { CommandOptions } from '../../types/ace.js' + +/** + * The make middleware command to create a new middleware + * class. + */ +export default class MakeMiddleware extends BaseCommand { + static commandName = 'make:middleware' + static description = 'Create a new middleware class for HTTP requests' + + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'Name of the middleware' }) + declare name: string + + @flags.string({ description: 'The stack in which to register the middleware', alias: 's' }) + declare stack?: 'server' | 'named' | 'router' + + /** + * The stub to use for generating the middleware + */ + protected stubPath: string = 'make/middleware/main.stub' + + async run() { + const stackChoices = ['server', 'router', 'named'] + + /** + * Prompt to select the stack under which to register + * the middleware + */ + if (!this.stack) { + this.stack = await this.prompt.choice( + 'Under which stack you want to register the middleware?', + stackChoices + ) + } + + /** + * Error out when mentioned stack is invalid + */ + if (!stackChoices.includes(this.stack)) { + this.exitCode = 1 + this.logger.error( + `Invalid middleware stack "${this.stack}". Select from "${stackChoices.join(', ')}"` + ) + return + } + + /** + * Create middleware + */ + const codemods = await this.createCodemods() + const { destination } = await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + + /** + * Creative relative path for the middleware file from + * the "./app/middleware" directory + */ + const middlewareRelativePath = slash( + relative(this.app.middlewarePath(), destination).replace(extname(destination), '') + ) + + /** + * Take the middleware relative path, remove `_middleware` prefix from it + * and convert everything to camelcase + */ + const name = string.camelCase(basename(middlewareRelativePath).replace(/_middleware$/, '')) + + /** + * Register middleware + */ + await codemods.registerMiddleware(this.stack, [ + { + name: name, + path: `#middleware/${middlewareRelativePath}`, + }, + ]) + } +} diff --git a/commands/make/preload.ts b/commands/make/preload.ts new file mode 100644 index 00000000..d555cee4 --- /dev/null +++ b/commands/make/preload.ts @@ -0,0 +1,109 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { slash } from '@poppinss/utils' +import { extname, relative } from 'node:path' +import type { AppEnvironments } from '@adonisjs/application/types' + +import { stubsRoot } from '../../stubs/main.js' +import { args, flags, BaseCommand } from '../../modules/ace/main.js' + +const ALLOWED_ENVIRONMENTS = ['web', 'console', 'test', 'repl'] satisfies AppEnvironments[] +type AllowedAppEnvironments = typeof ALLOWED_ENVIRONMENTS + +/** + * Make a new preload file + */ +export default class MakePreload extends BaseCommand { + static commandName = 'make:preload' + static description = 'Create a new preload file inside the start directory' + + @args.string({ description: 'Name of the preload file' }) + declare name: string + + @flags.boolean({ + description: 'Auto register the preload file inside the .adonisrc.ts file', + showNegatedVariantInHelp: true, + alias: 'r', + }) + declare register?: boolean + + @flags.array({ + description: `Define the preload file's environment. Accepted values are "${ALLOWED_ENVIRONMENTS}"`, + alias: 'e', + }) + declare environments?: AllowedAppEnvironments + + /** + * The stub to use for generating the preload file + */ + protected stubPath: string = 'make/preload/main.stub' + + /** + * Validate the environments flag passed by the user + */ + #isEnvironmentsFlagValid() { + if (!this.environments || !this.environments.length) { + return true + } + + return this.environments.every((one) => ALLOWED_ENVIRONMENTS.includes(one)) + } + + /** + * Run command + */ + async run() { + /** + * Ensure the environments are valid when provided via flag + */ + if (!this.#isEnvironmentsFlagValid()) { + this.logger.error( + `Invalid environment(s) "${this.environments}". Only "${ALLOWED_ENVIRONMENTS}" are allowed` + ) + return + } + + /** + * Display prompt to know if we should register the preload + * file inside the ".adonisrc.ts" file. + */ + if (this.register === undefined) { + this.register = await this.prompt.confirm( + 'Do you want to register the preload file in .adonisrc.ts file?' + ) + } + + const codemods = await this.createCodemods() + const { destination } = await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + + /** + * Do not register when prompt has been denied or "--no-register" + * flag was used + */ + if (!this.register) { + return + } + + /** + * Creative relative path for the preload file from + * the "./start" directory + */ + const preloadFileRelativePath = slash( + relative(this.app.startPath(), destination).replace(extname(destination), '') + ) + + await codemods.updateRcFile((rcFile) => { + rcFile.addPreloadFile(`#start/${preloadFileRelativePath}`, this.environments) + }) + } +} diff --git a/commands/make/provider.ts b/commands/make/provider.ts new file mode 100644 index 00000000..ec975b6c --- /dev/null +++ b/commands/make/provider.ts @@ -0,0 +1,105 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { slash } from '@poppinss/utils' +import { extname, relative } from 'node:path' + +import { stubsRoot } from '../../stubs/main.js' +import type { AppEnvironments } from '../../types/app.js' +import { args, BaseCommand, flags } from '../../modules/ace/main.js' + +const ALLOWED_ENVIRONMENTS = ['web', 'console', 'test', 'repl'] satisfies AppEnvironments[] +type AllowedAppEnvironments = typeof ALLOWED_ENVIRONMENTS + +/** + * Make a new provider class + */ +export default class MakeProvider extends BaseCommand { + static commandName = 'make:provider' + static description = 'Create a new service provider class' + + @args.string({ description: 'Name of the provider' }) + declare name: string + + @flags.boolean({ + description: 'Auto register the provider inside the .adonisrc.ts file', + showNegatedVariantInHelp: true, + alias: 'r', + }) + declare register?: boolean + + @flags.array({ + description: `Define the provider environment. Accepted values are "${ALLOWED_ENVIRONMENTS}"`, + alias: 'e', + }) + declare environments?: AllowedAppEnvironments + + /** + * The stub to use for generating the provider class + */ + protected stubPath: string = 'make/provider/main.stub' + + /** + * Validate the environments flag passed by the user + */ + #isEnvironmentsFlagValid() { + if (!this.environments || !this.environments.length) { + return true + } + return this.environments.every((one) => ALLOWED_ENVIRONMENTS.includes(one)) + } + + async run() { + /** + * Ensure the environments are valid when provided via flag + */ + if (!this.#isEnvironmentsFlagValid()) { + this.logger.error( + `Invalid environment(s) "${this.environments}". Only "${ALLOWED_ENVIRONMENTS}" are allowed` + ) + return + } + + /** + * Display prompt to know if we should register the provider + * file inside the ".adonisrc.ts" file. + */ + if (this.register === undefined) { + this.register = await this.prompt.confirm( + 'Do you want to register the provider in .adonisrc.ts file?' + ) + } + + const codemods = await this.createCodemods() + const { destination } = await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + + /** + * Do not register when prompt has been denied or "--no-register" + * flag was used + */ + if (!this.register) { + return + } + + /** + * Creative relative path for the provider file from + * the "./start" directory + */ + const providerRelativePath = slash( + relative(this.app.providersPath(), destination).replace(extname(destination), '') + ) + + await codemods.updateRcFile((rcFile) => { + rcFile.addProvider(`#providers/${providerRelativePath}`, this.environments) + }) + } +} diff --git a/commands/make/service.ts b/commands/make/service.ts new file mode 100644 index 00000000..e730c5d0 --- /dev/null +++ b/commands/make/service.ts @@ -0,0 +1,40 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args, BaseCommand } from '../../modules/ace/main.js' +import { CommandOptions } from '../../types/ace.js' + +/** + * Make a new service class + */ +export default class MakeService extends BaseCommand { + static commandName = 'make:service' + static description = 'Create a new service class' + + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'Name of the service' }) + declare name: string + + /** + * The stub to use for generating the service class + */ + protected stubPath: string = 'make/service/main.stub' + + async run() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } +} diff --git a/commands/make/test.ts b/commands/make/test.ts new file mode 100644 index 00000000..173f623f --- /dev/null +++ b/commands/make/test.ts @@ -0,0 +1,115 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args, flags, BaseCommand } from '../../modules/ace/main.js' + +/** + * Make a new test file + */ +export default class MakeTest extends BaseCommand { + static commandName = 'make:test' + static description = 'Create a new Japa test file' + + @args.string({ description: 'Name of the test file' }) + declare name: string + + @flags.string({ description: 'The suite for which to create the test file', alias: 's' }) + declare suite?: string + + /** + * The stub to use for generating the test file + */ + protected stubPath: string = 'make/test/main.stub' + + /** + * Returns the suite name for creating the test file + */ + async #getSuite(): Promise { + if (this.suite) { + return this.suite + } + + /** + * Use the first suite from the rcFile when there is only + * one suite + */ + const rcFileSuites = this.app.rcFile.tests.suites + if (rcFileSuites.length === 1) { + return rcFileSuites[0].name + } + + /** + * Prompt the user to select a suite manually + */ + return this.prompt.choice( + 'Select the suite for the test file', + this.app.rcFile.tests.suites.map((suite) => { + return suite.name + }), + { + validate(choice) { + return choice ? true : 'Please select a suite' + }, + } + ) + } + + /** + * Returns the directory path for the selected suite. + */ + async #getSuiteDirectory(directories: string[]): Promise { + if (directories.length === 1) { + return directories[0] + } + + return this.prompt.choice('Select directory for the test file', directories, { + validate(choice) { + return choice ? true : 'Please select a directory' + }, + }) + } + + /** + * Find suite info from the rcFile file + */ + #findSuite(suiteName: string) { + return this.app.rcFile.tests.suites.find((suite) => { + return suite.name === suiteName + }) + } + + /** + * Executed by ace + */ + async run() { + const suite = this.#findSuite(await this.#getSuite()) + + /** + * Show error when mentioned/selected suite does not exist + */ + if (!suite) { + this.logger.error(`The "${this.suite}" suite is not configured inside the "adonisrc.js" file`) + this.exitCode = 1 + return + } + + /** + * Generate entity + */ + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + suite: { + directory: await this.#getSuiteDirectory(suite.directories), + }, + }) + } +} diff --git a/commands/make/validator.ts b/commands/make/validator.ts new file mode 100644 index 00000000..30a0c47f --- /dev/null +++ b/commands/make/validator.ts @@ -0,0 +1,57 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args, flags, BaseCommand } from '../../modules/ace/main.js' +import { CommandOptions } from '../../types/ace.js' + +/** + * Make a new VineJS validator + */ +export default class MakeValidator extends BaseCommand { + static commandName = 'make:validator' + static description = 'Create a new file to define VineJS validators' + + static options: CommandOptions = { + allowUnknownFlags: true, + } + + @args.string({ description: 'Name of the validator file' }) + declare name: string + + @flags.boolean({ + description: 'Create a file with pre-defined validators for create and update actions', + }) + declare resource: boolean + + /** + * The stub to use for generating the validator + */ + protected stubPath: string = 'make/validator/main.stub' + + /** + * Preparing the command state + */ + async prepare() { + /** + * Use resource stub + */ + if (this.resource) { + this.stubPath = 'make/validator/resource.stub' + } + } + + async run() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } +} diff --git a/commands/make/view.ts b/commands/make/view.ts new file mode 100644 index 00000000..fa216523 --- /dev/null +++ b/commands/make/view.ts @@ -0,0 +1,35 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { stubsRoot } from '../../stubs/main.js' +import { args, BaseCommand } from '../../modules/ace/main.js' + +/** + * Make a new EdgeJS template file + */ +export default class MakeView extends BaseCommand { + static commandName = 'make:view' + static description = 'Create a new Edge.js template file' + + @args.string({ description: 'Name of the template' }) + declare name: string + + /** + * The stub to use for generating the template + */ + protected stubPath: string = 'make/view/main.stub' + + async run() { + const codemods = await this.createCodemods() + await codemods.makeUsingStub(stubsRoot, this.stubPath, { + flags: this.parsed.flags, + entity: this.app.generators.createEntity(this.name), + }) + } +} diff --git a/commands/repl.ts b/commands/repl.ts new file mode 100644 index 00000000..55e95512 --- /dev/null +++ b/commands/repl.ts @@ -0,0 +1,35 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand } from '../modules/ace/main.js' +import { CommandOptions } from '../types/ace.js' + +/** + * The ReplCommand class is used to start the Repl server + */ +export default class ReplCommand extends BaseCommand { + static commandName = 'repl' + static description = 'Start a new REPL session' + + static options: CommandOptions = { + startApp: true, + staysAlive: true, + } + + /** + * Starts the REPL server process + */ + async run() { + const repl = await this.app.container.make('repl') + repl.start() + repl.server!.on('exit', async () => { + await this.terminate() + }) + } +} diff --git a/commands/serve.ts b/commands/serve.ts new file mode 100644 index 00000000..ed6f9cc4 --- /dev/null +++ b/commands/serve.ts @@ -0,0 +1,180 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { DevServer } from '@adonisjs/assembler' +import type { CommandOptions } from '../types/ace.js' +import { BaseCommand, flags } from '../modules/ace/main.js' +import { detectAssetsBundler, importAssembler, importTypeScript } from '../src/internal_helpers.js' + +/** + * Serve command is used to run the AdonisJS HTTP server during development. The + * command under the hood runs the "bin/server.ts" file and watches for file + * system changes + */ +export default class Serve extends BaseCommand { + static commandName = 'serve' + static description = + 'Start the development HTTP server along with the file watcher to perform restarts on file change' + + static help = [ + 'Start the development server with file watcher using the following command.', + '```', + '{{ binaryName }} serve --watch', + '```', + '', + 'You can also start the server with HMR support using the following command.', + '```', + '{{ binaryName }} serve --hmr', + '```', + '', + 'The assets bundler dev server runs automatically after detecting vite config or webpack config files', + 'You may pass vite CLI args using the --assets-args command line flag.', + '```', + '{{ binaryName }} serve --assets-args="--debug --base=/public"', + '```', + ] + + static options: CommandOptions = { + staysAlive: true, + } + + declare devServer: DevServer + + @flags.boolean({ description: 'Start the server with HMR support' }) + declare hmr?: boolean + + @flags.boolean({ + description: 'Watch filesystem and restart the HTTP server on file change', + alias: 'w', + }) + declare watch?: boolean + + @flags.boolean({ description: 'Use polling to detect filesystem changes', alias: 'p' }) + declare poll?: boolean + + @flags.boolean({ + description: 'Clear the terminal for new logs after file change', + showNegatedVariantInHelp: true, + default: true, + }) + declare clear?: boolean + + @flags.boolean({ + description: 'Start assets bundler dev server', + showNegatedVariantInHelp: true, + default: true, + }) + declare assets?: boolean + + @flags.array({ + description: 'Define CLI arguments to pass to the assets bundler', + }) + declare assetsArgs?: string[] + + /** + * Log a development dependency is missing + */ + #logMissingDevelopmentDependency(dependency: string) { + this.logger.error( + [ + `Cannot find package "${dependency}"`, + '', + `The "${dependency}" package is a development dependency and therefore you should use the serve command during development only.`, + '', + 'If you are running your application in production, then use "node bin/server.js" command to start the HTTP server', + ].join('\n') + ) + } + + /** + * Returns the assets bundler config + */ + async #getAssetsBundlerConfig() { + const assetsBundler = await detectAssetsBundler(this.app) + return assetsBundler + ? { + enabled: this.assets === false ? false : true, + driver: assetsBundler.name, + cmd: assetsBundler.devServer.command, + args: (assetsBundler.devServer.args || []).concat(this.assetsArgs || []), + } + : { + enabled: false as const, + } + } + + /** + * Runs the HTTP server + */ + async run() { + const assembler = await importAssembler(this.app) + if (!assembler) { + this.#logMissingDevelopmentDependency('@adonisjs/assembler') + this.exitCode = 1 + return + } + + if (this.watch && this.hmr) { + this.logger.error('Cannot use --watch and --hmr flags together. Choose one of them') + this.exitCode = 1 + return + } + + this.devServer = new assembler.DevServer(this.app.appRoot, { + hmr: this.hmr === true ? true : false, + clearScreen: this.clear === false ? false : true, + nodeArgs: this.parsed.nodeArgs, + scriptArgs: [], + assets: await this.#getAssetsBundlerConfig(), + metaFiles: this.app.rcFile.metaFiles, + hooks: { + onDevServerStarted: this.app.rcFile.hooks?.onDevServerStarted, + onSourceFileChanged: this.app.rcFile.hooks?.onSourceFileChanged, + }, + }) + + /** + * Share command logger with assembler, so that CLI flags like --no-ansi has + * similar impact for assembler logs as well. + */ + this.devServer.setLogger(this.logger) + + /** + * Exit command when the dev server is closed + */ + this.devServer.onClose((exitCode) => { + this.exitCode = exitCode + this.terminate() + }) + + /** + * Exit command when the dev server crashes + */ + this.devServer.onError(() => { + this.exitCode = 1 + this.terminate() + }) + + /** + * Start the development server + */ + if (this.watch) { + const ts = await importTypeScript(this.app) + if (!ts) { + this.#logMissingDevelopmentDependency('typescript') + this.exitCode = 1 + return + } + + await this.devServer.startAndWatch(ts, { poll: this.poll || false }) + } else { + await this.devServer.start() + } + } +} diff --git a/commands/test.ts b/commands/test.ts new file mode 100644 index 00000000..f68864bc --- /dev/null +++ b/commands/test.ts @@ -0,0 +1,224 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { TestRunner } from '@adonisjs/assembler' + +import type { CommandOptions } from '../types/ace.js' +import { BaseCommand, flags, args } from '../modules/ace/main.js' +import { detectAssetsBundler, importAssembler, importTypeScript } from '../src/internal_helpers.js' + +/** + * Test command is used to run tests with optional file watcher. Under the + * hood, we run "bin/test.js" file. + */ +export default class Test extends BaseCommand { + static commandName = 'test' + static description = 'Run tests along with the file watcher to re-run tests on file change' + + static options: CommandOptions = { + allowUnknownFlags: true, + staysAlive: true, + } + + declare testsRunner: TestRunner + + @args.spread({ + description: 'Mention suite names to run tests for selected suites', + required: false, + }) + declare suites?: string[] + + @flags.array({ description: 'Filter tests by the filename' }) + declare files?: string[] + + @flags.array({ description: 'Filter tests by tags' }) + declare tags?: string[] + + @flags.array({ description: 'Filter tests by parent group title' }) + declare groups?: string[] + + @flags.array({ description: 'Filter tests by test title' }) + declare tests?: string[] + + @flags.array({ description: 'Activate one or more test reporters' }) + declare reporters?: string[] + + @flags.boolean({ description: 'Watch filesystem and re-run tests on file change' }) + declare watch?: boolean + + @flags.boolean({ description: 'Use polling to detect filesystem changes' }) + declare poll?: boolean + + @flags.number({ description: 'Define default timeout for all tests' }) + declare timeout?: number + + @flags.number({ description: 'Define default retries for all tests' }) + declare retries?: number + + @flags.boolean({ description: 'Execute tests failed during the last run' }) + declare failed?: boolean + + @flags.boolean({ + description: 'Clear the terminal for new logs after file change', + showNegatedVariantInHelp: true, + default: true, + }) + declare clear?: boolean + + @flags.boolean({ + description: 'Start assets bundler dev server.', + showNegatedVariantInHelp: true, + default: true, + }) + declare assets?: boolean + + @flags.array({ + description: 'Define CLI arguments to pass to the assets bundler', + }) + declare assetsArgs?: string[] + + /** + * Log a development dependency is missing + */ + #logMissingDevelopmentDependency(dependency: string) { + this.logger.error( + [ + `Cannot find package "${dependency}"`, + '', + `The "${dependency}" package is a development dependency and therefore you should run tests with development dependencies installed.`, + '', + 'If you are run tests inside a CI, make sure the NODE_ENV is set to "development"', + ].join('\n') + ) + } + + /** + * Collection of unknown flags to pass to Japa + */ + #getPassthroughFlags(): string[] { + return this.parsed.unknownFlags + .map((flag) => { + const value = this.parsed.flags[flag] + + /** + * Not mentioning value when value is "true" + */ + if (value === true) { + return [`--${flag}`] as string[] + } + + /** + * Repeating flag multiple times when value is an array + */ + if (Array.isArray(value)) { + return value.map((v) => [`--${flag}`, v]) as string[][] + } + + return [`--${flag}`, value] as string[] + }) + .flat(2) + } + + /** + * Returns the assets bundler config + */ + async #getAssetsBundlerConfig() { + const assetsBundler = await detectAssetsBundler(this.app) + return assetsBundler + ? { + enabled: this.assets === false ? false : true, + driver: assetsBundler.name, + cmd: assetsBundler.devServer.command, + args: (assetsBundler.devServer.args || []).concat(this.assetsArgs || []), + } + : { + enabled: false as const, + } + } + + /** + * Runs tests + */ + async run() { + process.env.NODE_ENV = 'test' + + const assembler = await importAssembler(this.app) + if (!assembler) { + this.#logMissingDevelopmentDependency('@adonisjs/assembler') + this.exitCode = 1 + return + } + + this.testsRunner = new assembler.TestRunner(this.app.appRoot, { + clearScreen: this.clear === false ? false : true, + nodeArgs: this.parsed.nodeArgs, + scriptArgs: this.#getPassthroughFlags(), + assets: await this.#getAssetsBundlerConfig(), + filters: { + suites: this.suites, + files: this.files, + groups: this.groups, + tags: this.tags, + tests: this.tests, + }, + failed: this.failed, + retries: this.retries, + timeout: this.timeout, + reporters: this.reporters, + suites: this.app.rcFile.tests.suites.map((suite) => { + return { + name: suite.name, + files: suite.files, + } + }), + env: { + NODE_ENV: 'test', + }, + metaFiles: this.app.rcFile.metaFiles, + }) + + /** + * Share command logger with assembler, so that CLI flags like --no-ansi has + * similar impact for assembler logs as well. + */ + this.testsRunner.setLogger(this.logger) + + /** + * Exit command when the test runner is closed + */ + this.testsRunner.onClose((exitCode) => { + this.exitCode = exitCode + this.terminate() + }) + + /** + * Exit command when the dev server crashes + */ + this.testsRunner.onError(() => { + this.exitCode = 1 + this.terminate() + }) + + /** + * Start the test runner in watch mode + */ + if (this.watch) { + const ts = await importTypeScript(this.app) + if (!ts) { + this.#logMissingDevelopmentDependency('typescript') + this.exitCode = 1 + return + } + + await this.testsRunner.runAndWatch(ts, { poll: this.poll || false }) + } else { + await this.testsRunner.run() + } + } +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..6c99b74d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,4 @@ +import { configPkg } from '@adonisjs/eslint-config' +export default configPkg({ + ignores: ['coverage'], +}) diff --git a/factories/app.ts b/factories/app.ts new file mode 100644 index 00000000..07f3c54f --- /dev/null +++ b/factories/app.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/application/factories' diff --git a/factories/bodyparser.ts b/factories/bodyparser.ts new file mode 100644 index 00000000..4185d29d --- /dev/null +++ b/factories/bodyparser.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/bodyparser/factories' diff --git a/factories/core/ace.ts b/factories/core/ace.ts new file mode 100644 index 00000000..beb50e2c --- /dev/null +++ b/factories/core/ace.ts @@ -0,0 +1,38 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { IgnitorFactory } from './ignitor.js' +import { Ignitor } from '../../src/ignitor/main.js' +import type { IgnitorOptions } from '../../src/types.js' +import type { Kernel } from '../../modules/ace/kernel.js' +import { createAceKernel } from '../../modules/ace/create_kernel.js' + +/** + * Creates an instance of Ace kernel + */ +export class AceFactory { + async make(ignitor: Ignitor): Promise + async make(appRoot: URL, options?: IgnitorOptions): Promise + async make(ignitorOrAppRoot: URL | Ignitor, options?: IgnitorOptions): Promise { + if (ignitorOrAppRoot instanceof Ignitor) { + const app = ignitorOrAppRoot.createApp('console') + await app.init() + return createAceKernel(app) + } + + const app = new IgnitorFactory() + .withCoreConfig() + .withCoreProviders() + .create(ignitorOrAppRoot, options!) + .createApp('console') + + await app.init() + return createAceKernel(app) + } +} diff --git a/factories/core/ignitor.ts b/factories/core/ignitor.ts new file mode 100644 index 00000000..c87c346c --- /dev/null +++ b/factories/core/ignitor.ts @@ -0,0 +1,134 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Ignitor } from '../../src/ignitor/main.js' +import type { ProviderNode } from '../../types/app.js' +import { drivers } from '../../modules/hash/define_config.js' +import { defineConfig as defineHttpConfig } from '../../modules/http/main.js' +import type { ApplicationService, IgnitorOptions } from '../../src/types.js' +import { defineConfig as defineLoggerConfig } from '../../modules/logger.js' +import { defineConfig as defineHashConfig } from '../../modules/hash/main.js' +import { defineConfig as defineBodyParserConfig } from '../../modules/bodyparser/main.js' + +type FactoryParameters = { + rcFileContents: Record + config: Record +} + +/** + * Ignitor factory creates an instance of the AdonisJS ignitor + */ +export class IgnitorFactory { + #preloadActions: ((app: ApplicationService) => Promise | void)[] = [] + #parameters: Partial = {} + + /** + * A flag to know if we should load the core providers + */ + #loadCoreProviders: boolean = false + + /** + * Define preload actions to run. + */ + preload(action: (app: ApplicationService) => void | Promise): this { + this.#preloadActions.push(action) + return this + } + + /** + * Merge core providers with user defined providers + */ + #mergeCoreProviders(providers?: ProviderNode['file'][]): ProviderNode['file'][] { + const coreProviders: ProviderNode['file'][] = [ + () => import('@adonisjs/core/providers/app_provider'), + () => import('@adonisjs/core/providers/hash_provider'), + () => import('@adonisjs/core/providers/repl_provider'), + ] + + return coreProviders.concat(providers || []) + } + + /** + * Merge custom factory parameters + */ + merge(params: Partial): this { + if (params.config) { + this.#parameters.config = Object.assign(this.#parameters.config || {}, params.config) + } + + if (params.rcFileContents) { + this.#parameters.rcFileContents = Object.assign( + this.#parameters.rcFileContents || {}, + params.rcFileContents + ) + } + + return this + } + + /** + * Load core provider when booting the app + */ + withCoreProviders(): this { + this.#loadCoreProviders = true + return this + } + + /** + * Merge default config for the core features. A shallow merge + * is performed. + */ + withCoreConfig(): this { + this.merge({ + config: { + app: { + appKey: 'averylongrandomsecretkey', + http: defineHttpConfig({}), + }, + validator: {}, + bodyparser: defineBodyParserConfig({}), + hash: defineHashConfig({ + default: 'scrypt', + list: { + scrypt: drivers.scrypt({}), + }, + }), + logger: defineLoggerConfig({ + default: 'app', + loggers: { + app: {}, + }, + }), + }, + }) + return this + } + + /** + * Create ignitor instance + */ + create(appRoot: URL, options?: IgnitorOptions): Ignitor { + return new Ignitor(appRoot, options).tap((app) => { + app.booted(async () => { + for (let action of this.#preloadActions) { + await action(app) + } + }) + + if (this.#loadCoreProviders) { + this.#parameters.rcFileContents = this.#parameters.rcFileContents || {} + this.#parameters.rcFileContents.providers = this.#mergeCoreProviders( + this.#parameters.rcFileContents.providers + ) + } + this.#parameters.rcFileContents && app.rcContents(this.#parameters.rcFileContents) + this.#parameters.config && app.useConfig(this.#parameters.config) + }) + } +} diff --git a/factories/core/main.ts b/factories/core/main.ts new file mode 100644 index 00000000..b4f2d15d --- /dev/null +++ b/factories/core/main.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { AceFactory } from './ace.js' +export { IgnitorFactory } from './ignitor.js' +export { TestUtilsFactory } from './test_utils.js' diff --git a/factories/core/test_utils.ts b/factories/core/test_utils.ts new file mode 100644 index 00000000..82e29175 --- /dev/null +++ b/factories/core/test_utils.ts @@ -0,0 +1,34 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Ignitor } from '../../index.js' +import { IgnitorFactory } from './ignitor.js' +import type { IgnitorOptions } from '../../src/types.js' +import { TestUtils } from '../../src/test_utils/main.js' + +/** + * Creates an instance of TestUtils class + */ +export class TestUtilsFactory { + create(ignitor: Ignitor): TestUtils + create(appRoot: URL, options?: IgnitorOptions): TestUtils + create(ignitorOrAppRoot: URL | Ignitor, options?: IgnitorOptions): TestUtils { + if (ignitorOrAppRoot instanceof Ignitor) { + return new TestUtils(ignitorOrAppRoot.createApp('test')) + } + + return new TestUtils( + new IgnitorFactory() + .withCoreConfig() + .withCoreProviders() + .create(ignitorOrAppRoot, options!) + .createApp('console') + ) + } +} diff --git a/factories/encryption.ts b/factories/encryption.ts new file mode 100644 index 00000000..b5664545 --- /dev/null +++ b/factories/encryption.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/encryption/factories' diff --git a/factories/events.ts b/factories/events.ts new file mode 100644 index 00000000..d7a48545 --- /dev/null +++ b/factories/events.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/events/factories' diff --git a/factories/hash.ts b/factories/hash.ts new file mode 100644 index 00000000..1c84fd38 --- /dev/null +++ b/factories/hash.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/hash/factories' diff --git a/factories/http.ts b/factories/http.ts new file mode 100644 index 00000000..b1772c75 --- /dev/null +++ b/factories/http.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/http-server/factories' diff --git a/factories/logger.ts b/factories/logger.ts new file mode 100644 index 00000000..de0d3f37 --- /dev/null +++ b/factories/logger.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/logger/factories' diff --git a/factories/stubs.ts b/factories/stubs.ts new file mode 100644 index 00000000..7394d30c --- /dev/null +++ b/factories/stubs.ts @@ -0,0 +1,54 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { AppFactory } from '@adonisjs/application/factories' + +import { stubsRoot } from '../index.js' +import type { ApplicationService } from '../src/types.js' + +type FactoryParameters = { + app: ApplicationService +} + +/** + * Prepares stubs from "@adonisjs/core" package. We do not publish this class as it + * is for internal testing only using the "stubsRoot" of the core package + */ +export class StubsFactory { + #parameters: Partial = {} + + /** + * Returns an instance of application + */ + #getApp() { + return this.#parameters.app || new AppFactory().create(new URL('./', import.meta.url)) + } + + /** + * Merge custom factory parameters + */ + merge(params: Partial): this { + this.#parameters = Object.assign(this.#parameters, params) + return this + } + + /** + * Prepares a stub + */ + async prepare(stubPath: string, data: Record) { + const app = this.#getApp() + await app.init() + + const stubs = await app.stubs.create() + const stub = await stubs.build(stubPath, { + source: stubsRoot, + }) + return stub.prepare(data) + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..5afcae24 --- /dev/null +++ b/index.ts @@ -0,0 +1,52 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { errors as aceErrors } from '@adonisjs/ace' +import { errors as envErrors } from '@adonisjs/env' +import { errors as appErrors } from '@adonisjs/application' +import { errors as encryptionErrors } from '@adonisjs/encryption' +import { errors as httpServerErrors } from '@adonisjs/http-server' + +export { stubsRoot } from './stubs/main.js' +export { inject } from './modules/container.js' +export { Ignitor } from './src/ignitor/main.js' +export { configProvider } from './src/config_provider.js' + +/** + * Aggregated errors from all modules. + */ +export const errors: typeof encryptionErrors & + typeof httpServerErrors & + typeof appErrors & + typeof aceErrors & + typeof envErrors = { + ...encryptionErrors, + ...httpServerErrors, + ...appErrors, + ...aceErrors, + ...envErrors, +} + +/** + * Pretty prints an error with colorful output using + * Youch terminal + */ +export async function prettyPrintError(error: any) { + if (error && typeof error === 'object' && error.code === 'E_DUMP_DIE_EXCEPTION') { + console.error(error) + return + } + + // @ts-expect-error + const { default: youchTerminal } = await import('youch-terminal') + const { default: Youch } = await import('youch') + + const youch = new Youch(error, {}) + console.error(youchTerminal(await youch.toJSON(), { displayShortPath: true })) +} diff --git a/lib/util.js b/lib/util.js deleted file mode 100644 index 24bdda19..00000000 --- a/lib/util.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const _ = require('lodash') - -const toStr = Object.prototype.toString -const fnToStr = Function.prototype.toString -const isFnRegex = /^\s*(?:function)?\*/ - -const util = exports = module.exports = {} - -/** - * tells whether value exists or not by checking - * it type - * - * @param {Mixed} value - * @return {Boolean} - * - * @private - */ -util.existy = function (value) { - return value !== undefined && value !== null -} - -/** - * @description returns an array from method arguments - * - * @method spread - * - * @return {Array} - * - * @private - */ -util.spread = function () { - return _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments) -} - -/** - * tells whether a method is a genetator function or - * not - * - * @method isGenerator - * - * @param {Function} method - * @return {Boolean} - * - * @private - */ -util.isGenerator = function (method) { - const viaToStr = toStr.call(method) - const viaFnToStr = fnToStr.call(method) - return (viaToStr === '[object Function]' || viaToStr === '[object GeneratorFunction]') && isFnRegex.test(viaFnToStr) -} diff --git a/modules/ace/codemods.ts b/modules/ace/codemods.ts new file mode 100644 index 00000000..006ae804 --- /dev/null +++ b/modules/ace/codemods.ts @@ -0,0 +1,380 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { slash } from '@poppinss/utils' +import { EventEmitter } from 'node:events' +import { EnvEditor } from '@adonisjs/env/editor' +import type { UIPrimitives } from '@adonisjs/ace/types' +import type { CodeTransformer } from '@adonisjs/assembler/code_transformer' +import type { + MiddlewareNode, + EnvValidationNode, + BouncerPolicyNode, +} from '@adonisjs/assembler/types' + +import type { Application } from '../app.js' + +/** + * Codemods to modify AdonisJS source files. The codemod APIs relies on + * "@adonisjs/assembler" package and it must be installed as a dependency + * inside user application. + */ +export class Codemods extends EventEmitter { + /** + * Reference to lazily imported assembler code transformer + */ + #codeTransformer?: CodeTransformer + + /** + * Reference to AdonisJS application + */ + #app: Application + + /** + * Reference to CLI logger to write logs + */ + #cliLogger: UIPrimitives['logger'] + + /** + * Overwrite existing files when generating files + * from stubs + */ + overwriteExisting = false + + /** + * Display verbose logs for package installation + */ + verboseInstallOutput = false + + constructor(app: Application, cliLogger: UIPrimitives['logger']) { + super() + this.#app = app + this.#cliLogger = cliLogger + } + + /** + * - Lazily import the code transformer + * - Return a fresh or reused instance of the code transformer + */ + async #getCodeTransformer() { + try { + if (!this.#codeTransformer) { + const { CodeTransformer } = await import('@adonisjs/assembler/code_transformer') + this.#codeTransformer = new CodeTransformer(this.#app.appRoot) + } + + return this.#codeTransformer + } catch { + return null + } + } + + /** + * Returns the installation command for different + * package managers + */ + #getInstallationCommands(packages: string[], packageManager: string, isDev: boolean) { + if (!packages.length) { + return '' + } + + const colors = this.#cliLogger.getColors() + const devFlag = isDev ? ' -D' : '' + + switch (packageManager) { + case 'yarn': + case 'yarn@berry': + return `${colors.yellow(`yarn add${devFlag}`)} ${packages.join(' ')}` + case 'pnpm': + return `${colors.yellow(`pnpm add${devFlag}`)} ${packages.join(' ')}` + case 'npm': + default: + return `${colors.yellow(`npm i${devFlag}`)} ${packages.join(' ')}` + } + } + + /** + * Define one or more environment variables + */ + async defineEnvVariables>( + environmentVariables: T, + options?: { omitFromExample?: Array } + ) { + const editor = new EnvEditor(this.#app.appRoot) + await editor.load() + + Object.keys(environmentVariables).forEach((key) => { + const value = environmentVariables[key] + editor.add(key, value, options?.omitFromExample?.includes(key)) + }) + + await editor.save() + this.#cliLogger.action('update .env file').succeeded() + } + + /** + * Returns the TsMorph project instance + * See https://ts-morph.com/ + */ + async getTsMorphProject(): Promise< + | InstanceType['project'] + | undefined + > { + const transformer = await this.#getCodeTransformer() + if (!transformer) { + this.#cliLogger.warning( + 'Cannot create CodeTransformer. Install "@adonisjs/assembler" to modify source files' + ) + return + } + + return transformer.project + } + + /** + * Define validations for the environment variables + */ + async defineEnvValidations(validations: EnvValidationNode) { + const transformer = await this.#getCodeTransformer() + if (!transformer) { + this.#cliLogger.warning( + 'Cannot update "start/env.ts" file. Install "@adonisjs/assembler" to modify source files' + ) + return + } + + const action = this.#cliLogger.action('update start/env.ts file') + try { + await transformer.defineEnvValidations(validations) + action.succeeded() + } catch (error) { + this.emit('error', error) + action.failed(error.message) + } + } + + /** + * Define validations for the environment variables + */ + async registerMiddleware(stack: 'server' | 'router' | 'named', middleware: MiddlewareNode[]) { + const transformer = await this.#getCodeTransformer() + if (!transformer) { + this.#cliLogger.warning( + 'Cannot update "start/kernel.ts" file. Install "@adonisjs/assembler" to modify source files' + ) + return + } + + const action = this.#cliLogger.action('update start/kernel.ts file') + try { + await transformer.addMiddlewareToStack(stack, middleware) + action.succeeded() + } catch (error) { + this.emit('error', error) + action.failed(error.message) + } + } + + /** + * Register bouncer policies to the list of policies + * collection exported from the "app/policies/main.ts" + * file. + */ + async registerPolicies(policies: BouncerPolicyNode[]) { + const transformer = await this.#getCodeTransformer() + if (!transformer) { + this.#cliLogger.warning( + 'Cannot update "app/policies/main.ts" file. Install "@adonisjs/assembler" to modify source files' + ) + return + } + + const action = this.#cliLogger.action('update app/policies/main.ts file') + try { + await transformer.addPolicies(policies) + action.succeeded() + } catch (error) { + this.emit('error', error) + action.failed(error.message) + } + } + + /** + * Update RCFile + */ + async updateRcFile(...params: Parameters) { + const transformer = await this.#getCodeTransformer() + if (!transformer) { + this.#cliLogger.warning( + 'Cannot update "adonisrc.ts" file. Install "@adonisjs/assembler" to modify source files' + ) + return + } + + const action = this.#cliLogger.action('update adonisrc.ts file') + try { + await transformer.updateRcFile(...params) + action.succeeded() + } catch (error) { + this.emit('error', error) + action.failed(error.message) + } + } + + /** + * Register a new Vite plugin in the `vite.config.ts` file + */ + async registerVitePlugin(...params: Parameters) { + const transformer = await this.#getCodeTransformer() + if (!transformer) { + this.#cliLogger.warning( + 'Cannot update "vite.config.ts" file. Install "@adonisjs/assembler" to modify source files' + ) + return + } + + const action = this.#cliLogger.action('update vite.config.ts file') + try { + await transformer.addVitePlugin(...params) + action.succeeded() + } catch (error) { + this.emit('error', error) + action.failed(error.message) + } + } + + /** + * Register a new Japa plugin in the `tests/bootstrap.ts` file + */ + async registerJapaPlugin(...params: Parameters) { + const transformer = await this.#getCodeTransformer() + if (!transformer) { + this.#cliLogger.warning( + 'Cannot update "tests/bootstrap.ts" file. Install "@adonisjs/assembler" to modify source files' + ) + return + } + + const action = this.#cliLogger.action('update tests/bootstrap.ts file') + try { + await transformer.addJapaPlugin(...params) + action.succeeded() + } catch (error) { + this.emit('error', error) + action.failed(error.message) + } + } + + /** + * Generate the stub + */ + async makeUsingStub(stubsRoot: string, stubPath: string, stubState: Record) { + const stubs = await this.#app.stubs.create() + const stub = await stubs.build(stubPath, { source: stubsRoot }) + const output = await stub.generate({ force: this.overwriteExisting, ...stubState }) + + const entityFileName = slash(this.#app.relativePath(output.destination)) + const result = { ...output, relativeFileName: entityFileName } + + if (output.status === 'skipped') { + this.#cliLogger.action(`create ${entityFileName}`).skipped(output.skipReason) + return result + } + + this.#cliLogger.action(`create ${entityFileName}`).succeeded() + return result + } + + /** + * Install packages using the correct package manager + * You can specify version of each package by setting it in the + * name like : + * + * ``` + * this.installPackages([{ name: '@adonisjs/lucid@next', isDevDependency: false }]) + * ``` + */ + async installPackages(packages: { name: string; isDevDependency: boolean }[]) { + const transformer = await this.#getCodeTransformer() + const appPath = this.#app.makePath() + const colors = this.#cliLogger.getColors() + const devDependencies = packages.filter((pkg) => pkg.isDevDependency).map(({ name }) => name) + const dependencies = packages.filter((pkg) => !pkg.isDevDependency).map(({ name }) => name) + + if (!transformer) { + this.#cliLogger.warning( + 'Cannot install packages. Install "@adonisjs/assembler" or manually install following packages' + ) + this.#cliLogger.log(`devDependencies: ${devDependencies.join(',')}`) + this.#cliLogger.log(`dependencies: ${dependencies.join(',')}`) + return + } + + const packageManager = await transformer.detectPackageManager(appPath) + + const spinner = this.#cliLogger.await( + `installing dependencies using ${packageManager || 'npm'} ` + ) + + const silentLogs = !this.verboseInstallOutput + if (silentLogs) { + spinner.start() + } + + try { + await transformer.installPackage(dependencies, { + cwd: appPath, + silent: silentLogs, + }) + await transformer.installPackage(devDependencies, { + dev: true, + cwd: appPath, + silent: silentLogs, + }) + + if (silentLogs) { + spinner.stop() + } + + this.#cliLogger.success('Packages installed') + this.#cliLogger.log( + devDependencies.map((dependency) => ` ${colors.dim('dev')} ${dependency} `).join('\n') + ) + this.#cliLogger.log( + dependencies.map((dependency) => ` ${colors.dim('prod')} ${dependency} `).join('\n') + ) + } catch (error) { + if (silentLogs) { + spinner.update('unable to install dependencies') + spinner.stop() + } + this.#cliLogger.fatal(error) + this.emit('error', error) + } + } + + /** + * List the packages one should install before using the packages + */ + async listPackagesToInstall(packages: { name: string; isDevDependency: boolean }[]) { + const appPath = this.#app.makePath() + const devDependencies = packages.filter((pkg) => pkg.isDevDependency).map(({ name }) => name) + const dependencies = packages.filter((pkg) => !pkg.isDevDependency).map(({ name }) => name) + + let packageManager: string | null = null + const transformer = await this.#getCodeTransformer() + if (transformer) packageManager = await transformer.detectPackageManager(appPath) + + this.#cliLogger.log('Please install following packages') + this.#cliLogger.log( + this.#getInstallationCommands(devDependencies, packageManager || 'npm', true) + ) + this.#cliLogger.log(this.#getInstallationCommands(dependencies, packageManager || 'npm', false)) + } +} diff --git a/modules/ace/commands.ts b/modules/ace/commands.ts new file mode 100644 index 00000000..aeaa3b46 --- /dev/null +++ b/modules/ace/commands.ts @@ -0,0 +1,173 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BaseCommand as AceBaseCommand, ListCommand as AceListCommand } from '@adonisjs/ace' + +import { Kernel } from './kernel.js' +import type { ApplicationService } from '../../src/types.js' +import type { CommandOptions, ParsedOutput, UIPrimitives } from '../../types/ace.js' + +/** + * The base command to create custom ace commands. The AdonisJS base commands + * receives the application instance + */ +export class BaseCommand extends AceBaseCommand { + static options: CommandOptions = {} + + get staysAlive() { + return (this.constructor as typeof BaseCommand).options.staysAlive + } + + get startApp() { + return (this.constructor as typeof BaseCommand).options.startApp + } + + constructor( + public app: ApplicationService, + public kernel: Kernel, + parsed: ParsedOutput, + ui: UIPrimitives, + prompt: Kernel['prompt'] + ) { + super(kernel, parsed, ui, prompt) + } + + /** + * Creates the codemods module to modify source files + */ + async createCodemods() { + const { Codemods } = await import('./codemods.js') + const codemods = new Codemods(this.app, this.logger) + codemods.on('error', () => { + this.exitCode = 1 + }) + + return codemods + } + + /** + * The prepare template method is used to prepare the + * state for the command. This is the first method + * executed on a given command instance. + */ + prepare?(..._: any[]): any + + /** + * The interact template method is used to display the prompts + * to the user. The method is called after the prepare + * method. + */ + interact?(..._: any[]): any + + /** + * The completed method is the method invoked after the command + * finishes or results in an error. + * + * You can access the command error using the `this.error` property. + * Returning `true` from completed method supresses the error + * reporting to the kernel layer. + */ + completed?(..._: any[]): any + + /** + * Executes the command + */ + async exec() { + this.hydrate() + + try { + /** + * Executing the template methods + */ + this.prepare && (await this.app.container.call(this, 'prepare')) + this.interact && (await this.app.container.call(this, 'interact')) + const result = await this.app.container.call(this, 'run') + + /** + * Set exit code + */ + this.result = this.result === undefined ? result : this.result + this.exitCode = this.exitCode ?? 0 + } catch (error) { + this.error = error + this.exitCode = this.exitCode ?? 1 + } + + /** + * Run the completed method (if exists) and check if has handled + * the error + */ + let errorHandled = this.completed + ? await this.app.container.call(this, 'completed') + : false + + if (this.error && !errorHandled) { + await this.kernel.errorHandler.render(this.error, this.kernel) + } + + return this.result + } + + /** + * Terminate the app. A command should prefer calling this method + * over the "app.terminate", because this method only triggers + * app termination when the current command is in the charge + * of the process. + */ + async terminate() { + if (this.kernel.getMainCommand() === this) { + await this.app.terminate() + } + } +} + +/** + * The List command is used to display a list of commands + */ +export class ListCommand extends AceListCommand implements BaseCommand { + static options: CommandOptions = {} + + get staysAlive() { + return (this.constructor as typeof BaseCommand).options.staysAlive + } + + get startApp() { + return (this.constructor as typeof BaseCommand).options.startApp + } + + constructor( + public app: ApplicationService, + public kernel: Kernel, + parsed: ParsedOutput, + ui: UIPrimitives, + prompt: Kernel['prompt'] + ) { + super(kernel, parsed, ui, prompt) + } + + /** + * Creates the codemods module to modify source files + */ + async createCodemods() { + const { Codemods } = await import('./codemods.js') + return new Codemods(this.app, this.logger) + } + + /** + * Terminate the app. A command should prefer calling this method + * over the "app.terminate", because this method only triggers + * app termination when the current command is in the charge + * of the process. + */ + async terminate() { + if (this.kernel.getMainCommand() === this) { + await this.app.terminate() + } + } +} diff --git a/modules/ace/create_kernel.ts b/modules/ace/create_kernel.ts new file mode 100644 index 00000000..233866ac --- /dev/null +++ b/modules/ace/create_kernel.ts @@ -0,0 +1,93 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Kernel } from './main.js' +import type { ApplicationService } from '../../src/types.js' +import { FsLoader, HelpCommand, type BaseCommand } from '../../modules/ace/main.js' + +/** + * We abstract the logic for creating the ace kernel in this + * file. So that both the "console" environment and rest + * of the environments can configure and use ace. + * + * - In console environment, ace manages the lifecycle of the process + * - In other environments, ace can be pulled from the container to + * run commands + */ +export function createAceKernel(app: ApplicationService, commandName?: string) { + const kernel = new Kernel(app) + kernel.info.set('binary', 'node ace') + + /** + * Lazy import commands mentioned in the "commands" array + * of rcFile + */ + app.rcFile.commands.forEach((commandModule) => { + kernel.addLoader(() => + typeof commandModule === 'function' ? commandModule() : app.import(commandModule) + ) + }) + + /** + * When we know the command we are running ahead of time, then we + * defer loading the application commands if the command has + * already been registered by other loaders. + */ + const fsLoader = new FsLoader(app.commandsPath()) + kernel.addLoader({ + async getMetaData() { + if (!commandName || !kernel.getCommand(commandName)) { + return fsLoader.getMetaData() + } + return [] + }, + getCommand(command) { + return fsLoader.getCommand(command) + }, + }) + + /** + * Custom global flags + */ + kernel.defineFlag('ansi', { + type: 'boolean', + showNegatedVariantInHelp: true, + description: 'Force enable or disable colorful output', + }) + + kernel.defineFlag('help', { + type: 'boolean', + description: HelpCommand.description, + }) + + /** + * Flag listener to turn colors on/off + */ + kernel.on('ansi', (_, $kernel, parsed) => { + if (parsed.flags.ansi === false) { + $kernel.ui.switchMode('silent') + } + + if (parsed.flags.ansi === true) { + $kernel.ui.switchMode('normal') + } + }) + + /** + * Flag listener to display the help + */ + kernel.on('help', async (command, $kernel, parsed) => { + parsed.args.unshift(command.commandName) + const help = new HelpCommand($kernel, parsed, kernel.ui, kernel.prompt) + await help.exec() + return $kernel.shortcircuit() + }) + + return kernel +} diff --git a/modules/ace/kernel.ts b/modules/ace/kernel.ts new file mode 100644 index 00000000..328fb8f6 --- /dev/null +++ b/modules/ace/kernel.ts @@ -0,0 +1,28 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Kernel as AceKernel } from '@adonisjs/ace' +import { BaseCommand, ListCommand } from './commands.js' +import type { ApplicationService } from '../../src/types.js' + +/** + * The base command to create custom ace commands. The AdonisJS base commands + * receives the application instance + */ +export class Kernel extends AceKernel { + constructor(public app: ApplicationService) { + super(ListCommand, { + create: async (command, parsedOutput, $kernel) => { + return app.container.make(command, [app, $kernel, parsedOutput, $kernel.ui, $kernel.prompt]) + }, + + run: (command) => command.exec(), + }) + } +} diff --git a/modules/ace/main.ts b/modules/ace/main.ts new file mode 100644 index 00000000..e5f7b5a2 --- /dev/null +++ b/modules/ace/main.ts @@ -0,0 +1,22 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { Kernel } from './kernel.js' +export { BaseCommand, ListCommand } from './commands.js' +export { + args, + flags, + errors, + Parser, + FsLoader, + ListLoader, + cliHelpers, + HelpCommand, + IndexGenerator, +} from '@adonisjs/ace' diff --git a/modules/app.ts b/modules/app.ts new file mode 100644 index 00000000..ff51c28d --- /dev/null +++ b/modules/app.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/application' diff --git a/modules/bodyparser/bodyparser_middleware.ts b/modules/bodyparser/bodyparser_middleware.ts new file mode 100644 index 00000000..fd83d3cd --- /dev/null +++ b/modules/bodyparser/bodyparser_middleware.ts @@ -0,0 +1,16 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { BodyParserMiddleware } from '@adonisjs/bodyparser/bodyparser_middleware' + +/** + * Default export allows lazy importing middleware with + * destructuring the named exports + */ +export default BodyParserMiddleware diff --git a/modules/bodyparser/main.ts b/modules/bodyparser/main.ts new file mode 100644 index 00000000..ef082b44 --- /dev/null +++ b/modules/bodyparser/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/bodyparser' diff --git a/modules/config.ts b/modules/config.ts new file mode 100644 index 00000000..15688996 --- /dev/null +++ b/modules/config.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/config' diff --git a/modules/container.ts b/modules/container.ts new file mode 100644 index 00000000..25f78cb2 --- /dev/null +++ b/modules/container.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/fold' diff --git a/modules/dumper/define_config.ts b/modules/dumper/define_config.ts new file mode 100644 index 00000000..737345f4 --- /dev/null +++ b/modules/dumper/define_config.ts @@ -0,0 +1,21 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ConsoleDumpConfig } from '@poppinss/dumper/console/types' +import { HTMLDumpConfig } from '@poppinss/dumper/html/types' + +/** + * Define config for the dumper service exported by + * the "@adonisjs/core/services/dumper" module + */ +export function defineConfig( + dumperConfig: Partial<{ html: HTMLDumpConfig; console: ConsoleDumpConfig }> +) { + return dumperConfig +} diff --git a/modules/dumper/dumper.ts b/modules/dumper/dumper.ts new file mode 100644 index 00000000..b486c5ce --- /dev/null +++ b/modules/dumper/dumper.ts @@ -0,0 +1,239 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import useColors from '@poppinss/colors' +import { dump as consoleDump } from '@poppinss/dumper/console' +import type { HTMLDumpConfig } from '@poppinss/dumper/html/types' +import type { ConsoleDumpConfig } from '@poppinss/dumper/console/types' +import { createScript, createStyleSheet, dump } from '@poppinss/dumper/html' + +import type { Application } from '../app.js' +import { E_DUMP_DIE_EXCEPTION } from './errors.js' + +const colors = useColors.ansi() + +const DUMP_TITLE_STYLES = ` +.adonisjs-dump-header { + font-family: JetBrains Mono, monaspace argon, Menlo, Monaco, Consolas, monospace; + background: #ff1639; + border-radius: 4px; + color: #fff; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + padding: 0.4rem 1.2rem; + font-size: 1em; + display: flex; + justify-content: space-between; +} +.adonisjs-dump-header .adonisjs-dump-header-title { + font-weight: bold; + text-transform: uppercase; +} +.adonisjs-dump-header .adonisjs-dump-header-source { + font-weight: bold; + color: inherit; + text-decoration: underline; +} +.dumper-dump pre { + border-radius: 4px; + border-top-left-radius: 0; + border-top-right-radius: 0; +}` + +const IDE = process.env.ADONIS_IDE ?? process.env.EDITOR ?? '' + +/** + * Dumper exposes the API to dump or die/dump values in your + * AdonisJS application. An singleton instance of the Dumper + * is shared as a service and may use it follows. + * + * ```ts + * const dumper = container.make('dumper') + * + * dumper.configureHtmlOutput({ + * // parser + html formatter config + * }) + * + * dumper.configureAnsiOutput({ + * // parser + console formatter config + * }) + * + * const html = dumper.dumpToHtml(value) + * const ansi = dumper.dumpToAnsi(value) + * + * // Returns style and script tags that must be + * // injeted to the head of the HTML document + * + * const head = dumper.getHeadElements() + * ``` + */ +export class Dumper { + #app: Application + + /** + * Configuration for the HTML formatter + */ + #htmlConfig: HTMLDumpConfig = {} + + /** + * Configuration for the Console formatter + */ + #consoleConfig: ConsoleDumpConfig = { + collapse: ['DateTime', 'Date'], + } + + /** + * A collections of known editors to create URLs to open + * them + */ + #editors: Record = { + textmate: 'txmt://open?url=file://%f&line=%l', + macvim: 'mvim://open?url=file://%f&line=%l', + emacs: 'emacs://open?url=file://%f&line=%l', + sublime: 'subl://open?url=file://%f&line=%l', + phpstorm: 'phpstorm://open?file=%f&line=%l', + atom: 'atom://core/open/file?filename=%f&line=%l', + vscode: 'vscode://file/%f:%l', + } + + constructor(app: Application) { + this.#app = app + } + + /** + * Returns the link to open the file using dd inside one + * of the known code editors + */ + #getEditorLink(source?: { + location: string + line: number + }): { href: string; text: string } | undefined { + const editorURL = this.#editors[IDE] || IDE + if (!editorURL || !source) { + return + } + + return { + href: editorURL.replace('%f', source.location).replace('%l', String(source.line)), + text: `${this.#app.relativePath(source.location)}:${source.line}`, + } + } + + /** + * Configure the HTML formatter output + */ + configureHtmlOutput(config: HTMLDumpConfig): this { + this.#htmlConfig = config + return this + } + + /** + * Configure the ANSI formatter output + */ + configureAnsiOutput(config: ConsoleDumpConfig): this { + this.#consoleConfig = config + return this + } + + /** + * Returns the style and the script elements for the + * HTML document + */ + getHeadElements(cspNonce?: string): string { + return ( + `' + + `' + ) + } + + /** + * Dump value to HTML ouput + */ + dumpToHtml( + value: unknown, + options: { + cspNonce?: string + title?: string + source?: { + location: string + line: number + } + } = {} + ) { + const link = this.#getEditorLink(options.source) ?? null + const title = options.title || 'DUMP' + + return ( + '
' + + `${title}` + + (link ? `${link.text}` : '') + + '
' + + dump(value, { cspNonce: options.cspNonce, ...this.#htmlConfig }) + ) + } + + /** + * Dump value to ANSI output + */ + dumpToAnsi( + value: unknown, + options: { + title?: string + source?: { + location: string + line: number + } + } = {} + ) { + const columns = process.stdout.columns + + /** + * Link to the source file + */ + const link = `${this.#getEditorLink(options.source)?.text ?? ''} ` + + /** + * Dump title + */ + const title = ` ${options.title || 'DUMP'}` + + /** + * Whitespace between the title and the link to align them + * on each side of x axis + */ + const whiteSpaceLength = columns ? columns - link.length - title.length - 4 : 2 + const whiteSpace = new Array(whiteSpaceLength <= 0 ? 2 : whiteSpaceLength).join(' ') + + /** + * Styled heading with background color and bold text + */ + const heading = colors.bgRed().bold(`${title}${whiteSpace}${link}`) + + return `${heading}\n${consoleDump(value, this.#consoleConfig)}` + } + + /** + * Dump values and die. The formatter will be picked + * based upon where your app is running. + * + * - During an HTTP request, the HTML output will be + * sent to the server. + * - Otherwise the value will be logged in the console + */ + dd(value: unknown, traceSourceIndex: number = 1) { + const error = new E_DUMP_DIE_EXCEPTION(value, this) + error.setTraceSourceIndex(traceSourceIndex) + throw error + } +} diff --git a/modules/dumper/errors.ts b/modules/dumper/errors.ts new file mode 100644 index 00000000..870e206f --- /dev/null +++ b/modules/dumper/errors.ts @@ -0,0 +1,122 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { inspect } from 'node:util' +import { parse } from 'error-stack-parser-es' +import type { Kernel } from '@adonisjs/core/ace' +import { Exception } from '@poppinss/utils/exception' +import type { HttpContext } from '@adonisjs/core/http' + +import type { Dumper } from './dumper.js' + +/** + * DumpDie exception is raised by the "dd" function. It will + * result in dumping the value in response to an HTTP + * request or printing the value to the console + */ +class DumpDieException extends Exception { + static status: number = 500 + static code: string = 'E_DUMP_DIE_EXCEPTION' + + declare fileName: string + declare lineNumber: number + + #dumper: Dumper + #traceSourceIndex: number = 1 + value: unknown + + constructor(value: unknown, dumper: Dumper) { + super('Dump and Die exception') + this.#dumper = dumper + this.value = value + } + + /** + * Returns the source file and line number location for the error + */ + #getErrorSource(): { location: string; line: number } | undefined { + if (this.fileName && this.lineNumber) { + return { + location: this.fileName, + line: this.lineNumber, + } + } + + const source = parse(this)[this.#traceSourceIndex] + if (!source.fileName || !source.lineNumber) { + return + } + + return { + location: source.fileName, + line: source.lineNumber, + } + } + + /** + * Set the index for the trace source. This is helpful when + * you build nested helpers on top of Die/Dump + */ + setTraceSourceIndex(index: number) { + this.#traceSourceIndex = index + return this + } + + /** + * Preventing itself from getting reported by the + * AdonisJS exception reporter + */ + report() {} + + /** + * Handler called by the AdonisJS HTTP exception handler + */ + async handle(error: DumpDieException, ctx: HttpContext) { + const source = this.#getErrorSource() + + /** + * Comes from the shield package + */ + const cspNonce = 'nonce' in ctx.response ? (ctx.response.nonce as string) : undefined + + ctx.response + .status(500) + .send( + '' + + '' + + '' + + '' + + '' + + `${this.#dumper.getHeadElements(cspNonce)}` + + '' + + '' + + `${this.#dumper.dumpToHtml(error.value, { cspNonce, source, title: 'DUMP DIE' })}` + + '' + + '' + ) + } + + /** + * Handler called by the AdonisJS Ace kernel + */ + async render(error: DumpDieException, kernel: Kernel) { + const source = this.#getErrorSource() + kernel.ui.logger.log(this.#dumper.dumpToAnsi(error.value, { source, title: 'DUMP DIE' })) + } + + /** + * Custom output for the Node.js util inspect + */ + [inspect.custom]() { + const source = this.#getErrorSource() + return this.#dumper.dumpToAnsi(this.value, { source, title: 'DUMP DIE' }) + } +} + +export const E_DUMP_DIE_EXCEPTION = DumpDieException diff --git a/modules/dumper/main.ts b/modules/dumper/main.ts new file mode 100644 index 00000000..0fd44a67 --- /dev/null +++ b/modules/dumper/main.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * as errors from './errors.js' +export { Dumper } from './dumper.js' +export { defineConfig } from './define_config.js' diff --git a/modules/dumper/plugins/edge.ts b/modules/dumper/plugins/edge.ts new file mode 100644 index 00000000..73dd5eb1 --- /dev/null +++ b/modules/dumper/plugins/edge.ts @@ -0,0 +1,88 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { type Edge, Template } from 'edge.js' +import { type Dumper } from '../dumper.js' + +/** + * Returns an edge plugin that integrates with a given + * dumper instance + */ +export function pluginEdgeDumper(dumper: Dumper) { + Template.macro('dumper' as any, dumper) + + return (edge: Edge) => { + edge.registerTag({ + tagName: 'dump', + block: false, + seekable: true, + noNewLine: true, + compile(parser, buffer, token) { + const parsed = parser.utils.transformAst( + parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), + token.filename, + parser + ) + + buffer.writeExpression( + `template.stacks.pushOnceTo('dumper', 'dumper_globals', template.dumper.getHeadElements(state.cspNonce))`, + token.filename, + token.loc.start.line + ) + + buffer.outputExpression( + `template.dumper.dumpToHtml(${parser.utils.stringify(parsed)}, { cspNonce: state.cspNonce, source: { location: $filename, line: $lineNumber } })`, + token.filename, + token.loc.start.line, + true + ) + }, + }) + + edge.registerTag({ + tagName: 'dd', + block: false, + seekable: true, + noNewLine: true, + compile(parser, buffer, token) { + const parsed = parser.utils.transformAst( + parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), + token.filename, + parser + ) + + /** + * Dump/Die statement to catch error and convert it into + * an Edge error + */ + const ddStatement = [ + 'try {', + ` template.dumper.dd(${parser.utils.stringify(parsed)})`, + '} catch (error) {', + ` if (error.code === 'E_DUMP_DIE_EXCEPTION') {`, + ' const edgeError = template.createError(error.message, $filename, $lineNumber)', + ' error.fileName = $filename', + ' error.lineNumber = $lineNumber', + ' edgeError.handle = function (_, ctx) {', + ' return error.handle(error, ctx)', + ' }', + ' edgeError.report = function () {', + ' return error.report(error)', + ' }', + ' throw edgeError', + ' }', + ' throw error', + '}', + ].join('\n') + + buffer.writeStatement(ddStatement, token.filename, token.loc.start.line) + }, + }) + } +} diff --git a/modules/encryption.ts b/modules/encryption.ts new file mode 100644 index 00000000..ea224410 --- /dev/null +++ b/modules/encryption.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/encryption' diff --git a/modules/env/editor.ts b/modules/env/editor.ts new file mode 100644 index 00000000..2f8ab187 --- /dev/null +++ b/modules/env/editor.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/env/editor' diff --git a/modules/env/main.ts b/modules/env/main.ts new file mode 100644 index 00000000..f3d0630b --- /dev/null +++ b/modules/env/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/env' diff --git a/modules/events.ts b/modules/events.ts new file mode 100644 index 00000000..d67fef5d --- /dev/null +++ b/modules/events.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/events' diff --git a/modules/hash/define_config.ts b/modules/hash/define_config.ts new file mode 100644 index 00000000..72aacee0 --- /dev/null +++ b/modules/hash/define_config.ts @@ -0,0 +1,127 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { InvalidArgumentsException } from '@poppinss/utils' + +import debug from '../../src/debug.js' +import type { Argon } from './drivers/argon.js' +import type { Scrypt } from './drivers/scrypt.js' +import type { Bcrypt } from './drivers/bcrypt.js' +import type { ConfigProvider } from '../../src/types.js' +import { configProvider } from '../../src/config_provider.js' +import type { + ArgonConfig, + BcryptConfig, + ScryptConfig, + ManagerDriverFactory, +} from '../../types/hash.js' + +/** + * Resolved config from the config provider will be + * the config accepted by the hash manager + */ +type ResolvedConfig< + KnownHashers extends Record>, +> = { + default?: keyof KnownHashers + list: { + [K in keyof KnownHashers]: KnownHashers[K] extends ConfigProvider ? A : KnownHashers[K] + } +} + +/** + * Define config for the hash service. + */ +export function defineConfig< + KnownHashers extends Record>, +>(config: { + default?: keyof KnownHashers + list: KnownHashers +}): ConfigProvider> { + /** + * Hashers list should always be provided + */ + if (!config.list) { + throw new InvalidArgumentsException('Missing "list" property in hash config') + } + + /** + * The default hasher should be mentioned in the list + */ + if (config.default && !config.list[config.default]) { + throw new InvalidArgumentsException( + `Missing "list.${String( + config.default + )}" in hash config. It is referenced by the "default" property` + ) + } + + /** + * Config provider to lazily import drivers as they are used inside + * the user application + */ + return configProvider.create>(async (app) => { + debug('resolving hash config') + + const hashersList = Object.keys(config.list) + const hashers = {} as Record< + string, + ManagerDriverFactory | ConfigProvider + > + + for (let hasherName of hashersList) { + const hasher = config.list[hasherName] + if (typeof hasher === 'function') { + hashers[hasherName] = hasher + } else { + hashers[hasherName] = await hasher.resolver(app) + } + } + + return { + default: config.default, + list: hashers as ResolvedConfig['list'], + } + }) +} + +/** + * Helpers to configure drivers inside the config file. The + * drivers will be imported and constructed lazily. + * + * - Import happens when you first use the hash module + * - Construction of drivers happens when you first use a driver + */ +export const drivers: { + argon2: (config: ArgonConfig) => ConfigProvider<() => Argon> + bcrypt: (config: BcryptConfig) => ConfigProvider<() => Bcrypt> + scrypt: (config: ScryptConfig) => ConfigProvider<() => Scrypt> +} = { + argon2: (config) => { + return configProvider.create(async () => { + const { Argon } = await import('./drivers/argon.js') + debug('configuring argon driver') + return () => new Argon(config) + }) + }, + bcrypt: (config) => { + return configProvider.create(async () => { + const { Bcrypt } = await import('./drivers/bcrypt.js') + debug('configuring bcrypt driver') + return () => new Bcrypt(config) + }) + }, + scrypt: (config) => { + return configProvider.create(async () => { + const { Scrypt } = await import('./drivers/scrypt.js') + debug('configuring scrypt driver') + return () => new Scrypt(config) + }) + }, +} diff --git a/modules/hash/drivers/argon.ts b/modules/hash/drivers/argon.ts new file mode 100644 index 00000000..e2bd490e --- /dev/null +++ b/modules/hash/drivers/argon.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/hash/drivers/argon' diff --git a/modules/hash/drivers/bcrypt.ts b/modules/hash/drivers/bcrypt.ts new file mode 100644 index 00000000..df876617 --- /dev/null +++ b/modules/hash/drivers/bcrypt.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/hash/drivers/bcrypt' diff --git a/modules/hash/drivers/scrypt.ts b/modules/hash/drivers/scrypt.ts new file mode 100644 index 00000000..46885128 --- /dev/null +++ b/modules/hash/drivers/scrypt.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/hash/drivers/scrypt' diff --git a/modules/hash/main.ts b/modules/hash/main.ts new file mode 100644 index 00000000..264673a4 --- /dev/null +++ b/modules/hash/main.ts @@ -0,0 +1,11 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/hash' +export { defineConfig, drivers } from './define_config.js' diff --git a/modules/hash/phc_formatter.ts b/modules/hash/phc_formatter.ts new file mode 100644 index 00000000..c8233de6 --- /dev/null +++ b/modules/hash/phc_formatter.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/hash/phc_formatter' diff --git a/modules/health.ts b/modules/health.ts new file mode 100644 index 00000000..86a7ccf7 --- /dev/null +++ b/modules/health.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/health' diff --git a/modules/http/main.ts b/modules/http/main.ts new file mode 100644 index 00000000..649f3e58 --- /dev/null +++ b/modules/http/main.ts @@ -0,0 +1,16 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Bodyparser import is needed to merge types of Request + * class augmented by the bodyparser package + */ +import '@adonisjs/bodyparser' +export * from '@adonisjs/http-server' +export { RequestValidator } from './request_validator.js' diff --git a/modules/http/request_validator.ts b/modules/http/request_validator.ts new file mode 100644 index 00000000..2b004040 --- /dev/null +++ b/modules/http/request_validator.ts @@ -0,0 +1,101 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { VineValidator } from '@vinejs/vine' +import type { + Infer, + SchemaTypes, + ErrorReporterContract, + MessagesProviderContact, +} from '@vinejs/vine/types' + +import type { HttpContext } from './main.js' +import type { FeatureFlags } from '../app.js' +import type { ExperimentalFlagsList } from '../../types/app.js' +import type { RequestValidationOptions } from '../../types/http.js' + +/** + * Request validator is used validate HTTP request data using + * VineJS validators. You may validate the request body, + * files, cookies, and headers. + */ +export class RequestValidator { + #ctx: HttpContext + #experimentalFlags?: FeatureFlags + + constructor(ctx: HttpContext, experimentalFlags?: FeatureFlags) { + this.#ctx = ctx + this.#experimentalFlags = experimentalFlags + } + + /** + * The error reporter method returns the error reporter + * to use for reporting errors. + * + * You can use this function to pick a different error reporter + * for each HTTP request + */ + static errorReporter?: (_: HttpContext) => ErrorReporterContract + + /** + * The messages provider method returns the messages provider to use + * finding custom error messages + * + * You can use this function to pick a different messages provider for + * each HTTP request + */ + static messagesProvider?: (_: HttpContext) => MessagesProviderContact + + /** + * The validate method can be used to validate the request + * data for the current request using VineJS validators + */ + validateUsing>( + validator: VineValidator, + ...[options]: [undefined] extends MetaData + ? [options?: RequestValidationOptions | undefined] + : [options: RequestValidationOptions] + ): Promise> { + const validatorOptions: RequestValidationOptions = options || {} + + /** + * Assign request specific error reporter + */ + if (RequestValidator.errorReporter && !validatorOptions.errorReporter) { + const errorReporter = RequestValidator.errorReporter(this.#ctx) + validatorOptions.errorReporter = () => errorReporter + } + + /** + * Assign request specific messages provider + */ + if (RequestValidator.messagesProvider && !validatorOptions.messagesProvider) { + validatorOptions.messagesProvider = RequestValidator.messagesProvider(this.#ctx) + } + + const requestBody = this.#experimentalFlags?.enabled('mergeMultipartFieldsAndFiles') + ? this.#ctx.request.all() + : { + ...this.#ctx.request.all(), + ...this.#ctx.request.allFiles(), + } + + /** + * Data to validate + */ + const data = validatorOptions.data || { + ...requestBody, + params: this.#ctx.request.params(), + headers: this.#ctx.request.headers(), + cookies: this.#ctx.request.cookiesList(), + } + + return validator.validate(data, validatorOptions as any) + } +} diff --git a/modules/logger.ts b/modules/logger.ts new file mode 100644 index 00000000..a6fd99d1 --- /dev/null +++ b/modules/logger.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/logger' diff --git a/modules/repl.ts b/modules/repl.ts new file mode 100644 index 00000000..6ab3fcd1 --- /dev/null +++ b/modules/repl.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from '@adonisjs/repl' diff --git a/package.json b/package.json index a40cf668..9960d8d6 100644 --- a/package.json +++ b/package.json @@ -1,89 +1,241 @@ { - "name": "adonis-framework", - "version": "3.0.1", - "description": "Adonis framework makes it easy for you to write webapps with less code", - "main": "index.js", - "scripts": { - "test": "npm run lint && node --harmony_proxies ./node_modules/.bin/istanbul cover _mocha --report lcovonly -- -R spec test/unit test/acceptance && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", - "coverage": "npm run lint && node --harmony_proxies ./node_modules/.bin/istanbul cover _mocha test/unit test/acceptance --bail", - "lint": "standard src/**/*.js src/**/**/*.js test/**/*.js providers/*.js" - }, - "standard": { - "global": [ - "it", - "describe", - "context", - "before", - "after", - "beforeEach", - "afterEach" - ] + "name": "@adonisjs/core", + "description": "Core of AdonisJS", + "version": "6.19.0", + "engines": { + "node": ">=20.6.0" }, - "keywords": [ - "adonis-framework", - "mvc", - "mvc-framework" + "main": "build/index.js", + "type": "module", + "files": [ + "build/modules", + "build/commands", + "build/providers", + "build/services", + "build/factories", + "build/toolkit", + "build/types", + "build/src", + "build/stubs", + "build/index.d.ts", + "build/index.js" ], - "author": "adonisjs", - "license": "MIT", - "devDependencies": { - "adonis-fold": "^3.0.2", - "adonis-redis": "^1.0.0", - "chai": "^3.3.0", - "cheerio": "^0.22.0", - "co-mocha": "^1.1.3", - "co-supertest": "0.0.10", - "coveralls": "^2.11.7", - "cz-conventional-changelog": "^1.2.0", - "formidable": "^1.0.17", - "istanbul": "^0.4.0", - "mocha": "^3.0.1", - "mocha-lcov-reporter": "^1.1.0", - "pem": "^1.8.1", - "sinon": "^1.17.5", - "standard": "^7.1.2", - "supertest": "^1.1.0", - "test-console": "^1.0.0", - "zombie": "^4.2.1" + "bin": { + "adonis-kit": "./build/toolkit/main.js" }, - "peerDependencies": { - "adonis-fold": "^3.0.2" + "exports": { + ".": "./build/index.js", + "./commands": "./build/commands/main.js", + "./commands/*": "./build/commands/*.js", + "./factories": "./build/factories/core/main.js", + "./factories/*": "./build/factories/*.js", + "./types": "./build/src/types.js", + "./types/*": "./build/types/*.js", + "./services/*": "./build/services/*.js", + "./providers/*": "./build/providers/*.js", + "./helpers": "./build/src/helpers/main.js", + "./helpers/*": "./build/src/helpers/*.js", + "./ace": "./build/modules/ace/main.js", + "./ace/codemods": "./build/modules/ace/codemods.js", + "./bodyparser": "./build/modules/bodyparser/main.js", + "./bodyparser_middleware": "./build/modules/bodyparser/bodyparser_middleware.js", + "./hash": "./build/modules/hash/main.js", + "./hash/phc_formatter": "./build/modules/hash/phc_formatter.js", + "./hash/drivers/argon": "./build/modules/hash/drivers/argon.js", + "./hash/drivers/bcrypt": "./build/modules/hash/drivers/bcrypt.js", + "./hash/drivers/scrypt": "./build/modules/hash/drivers/scrypt.js", + "./app": "./build/modules/app.js", + "./config": "./build/modules/config.js", + "./container": "./build/modules/container.js", + "./encryption": "./build/modules/encryption.js", + "./env": "./build/modules/env/main.js", + "./dumper": "./build/modules/dumper/main.js", + "./dumper/plugin_edge": "./build/modules/dumper/plugins/edge.js", + "./env/editor": "./build/modules/env/editor.js", + "./events": "./build/modules/events.js", + "./http": "./build/modules/http/main.js", + "./logger": "./build/modules/logger.js", + "./repl": "./build/modules/repl.js", + "./package.json": "./package.json", + "./exceptions": "./build/src/exceptions.js", + "./test_utils": "./build/src/test_utils/main.js", + "./health": "./build/modules/health.js", + "./vine": "./build/src/vine.js" + }, + "scripts": { + "pretest": "npm run lint", + "test": "cross-env NODE_DEBUG=adonisjs:core c8 npm run quick:test", + "clean": "del-cli build", + "copy:templates": "copyfiles \"stubs/**/**/*.stub\" build", + "precompile": "npm run lint", + "compile": "npm run clean && tsc", + "postcompile": "npm run copy:templates && npm run index:commands", + "build": "npm run compile", + "release": "npx release-it", + "version": "npm run build", + "prepublishOnly": "npm run build", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "format": "prettier --write .", + "quick:test": "node --import=ts-node-maintained/register/esm --enable-source-maps --experimental-import-meta-resolve bin/test.ts --force-exit", + "citgm": "cross-env FORCE_COLOR=0 node --import=ts-node-maintained/register/esm --experimental-import-meta-resolve bin/test.ts --force-exit", + "index:commands": "node --import=ts-node-maintained/register/esm toolkit/main.js index build/commands" + }, + "devDependencies": { + "@adonisjs/assembler": "^7.8.2", + "@adonisjs/eslint-config": "^2.1.0", + "@adonisjs/prettier-config": "^1.4.5", + "@adonisjs/tsconfig": "^1.4.1", + "@japa/assert": "^4.0.1", + "@japa/expect-type": "^2.0.3", + "@japa/file-system": "^2.3.2", + "@japa/runner": "^4.2.0", + "@japa/snapshot": "^2.0.8", + "@release-it/conventional-changelog": "^10.0.1", + "@swc/core": "1.10.7", + "@types/node": "^24.0.3", + "@types/pretty-hrtime": "^1.0.3", + "@types/sinon": "^17.0.4", + "@types/supertest": "^6.0.3", + "@types/test-console": "^2.0.3", + "@vinejs/vine": "^3.0.1", + "argon2": "^0.43.0", + "bcrypt": "^6.0.0", + "c8": "^10.1.3", + "copyfiles": "^2.4.1", + "cross-env": "^7.0.3", + "del-cli": "^6.0.0", + "edge.js": "^6.2.1", + "eslint": "^9.29.0", + "execa": "^9.6.0", + "get-port": "^7.1.0", + "prettier": "^3.5.3", + "release-it": "^19.0.3", + "sinon": "^21.0.0", + "supertest": "^7.1.1", + "test-console": "^2.0.0", + "timekeeper": "^2.3.1", + "ts-node-maintained": "^10.9.5", + "typescript": "^5.8.3" }, "dependencies": { - "adonis-binding-resolver": "^1.0.1", - "bcryptjs": "^2.3.0", - "bytes": "^2.4.0", - "cat-log": "^1.0.2", - "co": "^4.6.0", - "co-fs-extra": "^1.2.1", - "dotenv": "^2.0.0", - "eventemitter2": "^2.1.0", - "lodash": "^4.14.1", - "node-cookie": "^1.0.2", - "node-exceptions": "^1.0.3", - "node-req": "^1.0.3", - "node-res": "^3.0.0", - "node-uuid": "^1.4.7", - "nunjucks": "^2.3.0", - "path-to-regexp": "^1.3.0", - "require-all": "^2.0.0", - "serve-static": "^1.10.2", - "type-of-is": "^3.4.0" + "@adonisjs/ace": "^13.3.0", + "@adonisjs/application": "^8.4.1", + "@adonisjs/bodyparser": "^10.1.0", + "@adonisjs/config": "^5.0.3", + "@adonisjs/encryption": "^6.0.2", + "@adonisjs/env": "^6.2.0", + "@adonisjs/events": "^9.0.2", + "@adonisjs/fold": "^10.2.0", + "@adonisjs/hash": "^9.1.1", + "@adonisjs/health": "^2.0.0", + "@adonisjs/http-server": "^7.7.0", + "@adonisjs/logger": "^6.0.6", + "@adonisjs/repl": "^4.1.0", + "@antfu/install-pkg": "^1.1.0", + "@paralleldrive/cuid2": "^2.2.2", + "@poppinss/colors": "^4.1.4", + "@poppinss/dumper": "^0.6.3", + "@poppinss/macroable": "^1.0.4", + "@poppinss/utils": "^6.10.0", + "@sindresorhus/is": "^7.0.2", + "@types/he": "^1.2.3", + "error-stack-parser-es": "^1.0.5", + "he": "^1.2.0", + "parse-imports": "^2.2.1", + "pretty-hrtime": "^1.0.3", + "string-width": "^7.2.0", + "youch": "^3.3.4", + "youch-terminal": "^2.2.3" }, - "config": { - "commitizen": { - "path": "./node_modules/cz-conventional-changelog" - } + "peerDependencies": { + "@adonisjs/assembler": "^7.8.0", + "@vinejs/vine": "^2.1.0 || ^3.0.0", + "argon2": "^0.31.2 || ^0.41.0 || ^0.43.0", + "bcrypt": "^5.1.1 || ^6.0.0", + "edge.js": "^6.2.0" }, - "directories": { - "test": "test" + "peerDependenciesMeta": { + "argon2": { + "optional": true + }, + "bcrypt": { + "optional": true + }, + "@adonisjs/assembler": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "edge.js": { + "optional": true + } }, + "homepage": "https://github.com/adonisjs/core#readme", "repository": { "type": "git", - "url": "git+https://github.com/adonisjs/adonis-framework.git" + "url": "git+https://github.com/adonisjs/core.git" }, "bugs": { - "url": "https://github.com/adonisjs/adonis-framework/issues" + "url": "https://github.com/adonisjs/core/issues" + }, + "keywords": [ + "adonisjs", + "framework", + "mvc" + ], + "author": "Harminder Virk ", + "contributors": [ + "Romain Lanz ", + "Julien Ripouteau ", + "Michaël Zasso" + ], + "license": "MIT", + "publishConfig": { + "provenance": true, + "access": "public" + }, + "c8": { + "reporter": [ + "text", + "html" + ], + "exclude": [ + "tests/**", + "build/**", + "factories/**", + ".yalc/**" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "requireCleanWorkingDir": true, + "requireUpstream": true, + "commitMessage": "chore(release): ${version}", + "tagAnnotation": "v${version}", + "push": true, + "tagName": "v${version}" + }, + "github": { + "release": true + }, + "npm": { + "publish": true, + "skipChecks": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "angular" + } + } + } }, - "homepage": "https://github.com/adonisjs/adonis-framework#readme" + "prettier": "@adonisjs/prettier-config" } diff --git a/providers/ConfigProvider.js b/providers/ConfigProvider.js deleted file mode 100644 index 04eadb40..00000000 --- a/providers/ConfigProvider.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class ConfigProvider extends ServiceProvider { - - * register () { - this.app.singleton('Adonis/Src/Config', function (app) { - const Config = require('../src/Config') - const Helpers = app.use('Adonis/Src/Helpers') - return new Config(Helpers) - }) - } -} - -module.exports = ConfigProvider diff --git a/providers/EncryptionProvider.js b/providers/EncryptionProvider.js deleted file mode 100644 index 39ab2a7d..00000000 --- a/providers/EncryptionProvider.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class EncryptionProvider extends ServiceProvider { - - * register () { - this.app.singleton('Adonis/Src/Encryption', function (app) { - const Encryption = require('../src/Encryption') - const Config = app.use('Adonis/Src/Config') - return new Encryption(Config) - }) - } -} - -module.exports = EncryptionProvider diff --git a/providers/EnvProvider.js b/providers/EnvProvider.js deleted file mode 100644 index b6f54f2f..00000000 --- a/providers/EnvProvider.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class EnvProvider extends ServiceProvider { - - * register () { - this.app.singleton('Adonis/Src/Env', function (app) { - const Env = require('../src/Env') - const Helpers = app.use('Adonis/Src/Helpers') - return new Env(Helpers) - }) - } -} - -module.exports = EnvProvider diff --git a/providers/EventProvider.js b/providers/EventProvider.js deleted file mode 100644 index 0cf519da..00000000 --- a/providers/EventProvider.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class EventProvider extends ServiceProvider { - - * register () { - this.app.singleton('Adonis/Src/Event', function (app) { - const Event = require('../src/Event') - const Config = app.use('Adonis/Src/Config') - const Helpers = app.use('Adonis/Src/Helpers') - return new Event(Config, Helpers) - }) - } -} - -module.exports = EventProvider diff --git a/providers/HashProvider.js b/providers/HashProvider.js deleted file mode 100644 index 3cd11839..00000000 --- a/providers/HashProvider.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class HashProvider extends ServiceProvider { - - * register () { - this.app.bind('Adonis/Src/Hash', function () { - return require('../src/Hash') - }) - } -} - -module.exports = HashProvider diff --git a/providers/HelpersProvider.js b/providers/HelpersProvider.js deleted file mode 100644 index 9b7adeaa..00000000 --- a/providers/HelpersProvider.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class HelpersProvider extends ServiceProvider { - - * register () { - this.app.bind('Adonis/Src/Helpers', function () { - return require('../src/Helpers') - }) - } -} - -module.exports = HelpersProvider diff --git a/providers/MiddlewareProvider.js b/providers/MiddlewareProvider.js deleted file mode 100644 index d02886cc..00000000 --- a/providers/MiddlewareProvider.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class MiddlewareProvider extends ServiceProvider { - - * register () { - this.app.bind('Adonis/Src/Middleware', function () { - return require('../src/Middleware') - }) - } -} - -module.exports = MiddlewareProvider diff --git a/providers/RequestProvider.js b/providers/RequestProvider.js deleted file mode 100644 index 84f7b5aa..00000000 --- a/providers/RequestProvider.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class RequestProvider extends ServiceProvider { - - * register () { - this.app.singleton('Adonis/Src/Request', function () { - return require('../src/Request') - }) - } -} - -module.exports = RequestProvider diff --git a/providers/ResponseProvider.js b/providers/ResponseProvider.js deleted file mode 100644 index 8bcdad13..00000000 --- a/providers/ResponseProvider.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class ResponseProvider extends ServiceProvider { - - * register () { - this.app.singleton('Adonis/Src/Response', function (app) { - const View = app.use('Adonis/Src/View') - const Route = app.use('Adonis/Src/Route') - const Config = app.use('Adonis/Src/Config') - const Response = require('../src/Response') - return new Response(View, Route, Config) - }) - } -} - -module.exports = ResponseProvider diff --git a/providers/RouteProvider.js b/providers/RouteProvider.js deleted file mode 100644 index 26838e42..00000000 --- a/providers/RouteProvider.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class RouteProvider extends ServiceProvider { - - * register () { - this.app.bind('Adonis/Src/Route', function () { - return require('../src/Route') - }) - } -} - -module.exports = RouteProvider diff --git a/providers/ServerProvider.js b/providers/ServerProvider.js deleted file mode 100644 index 99451819..00000000 --- a/providers/ServerProvider.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class ServerProvider extends ServiceProvider { - - * register () { - this.app.bind('Adonis/Src/Server', function (app) { - const Request = app.use('Adonis/Src/Request') - const Response = app.use('Adonis/Src/Response') - const Route = app.use('Adonis/Src/Route') - const Helpers = app.use('Adonis/Src/Helpers') - const Middleware = app.use('Adonis/Src/Middleware') - const Static = app.use('Adonis/Src/Static') - const Session = app.use('Adonis/Src/Session') - const Config = app.use('Adonis/Src/Config') - const Event = app.use('Adonis/Src/Event') - const Server = require('../src/Server') - return new Server(Request, Response, Route, Helpers, Middleware, Static, Session, Config, Event) - }) - } -} - -module.exports = ServerProvider diff --git a/providers/SessionProvider.js b/providers/SessionProvider.js deleted file mode 100644 index 641d6dfe..00000000 --- a/providers/SessionProvider.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class SessionProvider extends ServiceProvider { - - * register () { - const SessionManager = require('../src/Session/SessionManager') - this.app.singleton('Adonis/Src/Session', function (app) { - const Config = app.use('Adonis/Src/Config') - return new SessionManager(Config) - }) - - this.app.manager('Adonis/Src/Session', SessionManager) - } -} - -module.exports = SessionProvider diff --git a/providers/StaticProvider.js b/providers/StaticProvider.js deleted file mode 100644 index a5987c55..00000000 --- a/providers/StaticProvider.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class StaticProvider extends ServiceProvider { - - * register () { - this.app.bind('Adonis/Src/Static', function (app) { - const Helpers = app.use('Adonis/Src/Helpers') - const Config = app.use('Adonis/Src/Config') - const Static = require('../src/Static') - return new Static(Helpers, Config) - }) - } -} - -module.exports = StaticProvider diff --git a/providers/ViewProvider.js b/providers/ViewProvider.js deleted file mode 100644 index 39d1a032..00000000 --- a/providers/ViewProvider.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const ServiceProvider = require('adonis-fold').ServiceProvider - -class ViewProvider extends ServiceProvider { - - * register () { - this.app.singleton('Adonis/Src/View', function (app) { - const Helpers = app.use('Adonis/Src/Helpers') - const Config = app.use('Adonis/Src/Config') - const Route = app.use('Adonis/Src/Route') - const View = require('../src/View') - return new View(Helpers, Config, Route) - }) - } -} - -module.exports = ViewProvider diff --git a/providers/app_provider.ts b/providers/app_provider.ts new file mode 100644 index 00000000..656085cb --- /dev/null +++ b/providers/app_provider.ts @@ -0,0 +1,185 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Config } from '../modules/config.js' +import { Logger } from '../modules/logger.js' +import { Application } from '../modules/app.js' +import { Dumper } from '../modules/dumper/dumper.js' +import { Encryption } from '../modules/encryption.js' +import { Router, Server } from '../modules/http/main.js' +import { BaseEvent, Emitter } from '../modules/events.js' +import type { ApplicationService, LoggerService } from '../src/types.js' +import BodyParserMiddleware from '../modules/bodyparser/bodyparser_middleware.js' + +/** + * The Application Service provider registers all the baseline + * features required to run the framework. + */ +export default class AppServiceProvider { + constructor(protected app: ApplicationService) {} + + /** + * Registers test utils with the container + */ + protected registerTestUtils() { + this.app.container.singleton('testUtils', async () => { + const { TestUtils } = await import('../src/test_utils/main.js') + return new TestUtils(this.app) + }) + } + + /** + * Registers ace with the container + */ + protected registerAce() { + this.app.container.singleton('ace', async () => { + const { createAceKernel } = await import('../modules/ace/create_kernel.js') + return createAceKernel(this.app) + }) + } + + /** + * Registers the application to the container + */ + protected registerApp() { + this.app.container.singleton(Application, () => this.app) + this.app.container.alias('app', Application) + } + + /** + * Registers the logger class to resolve the default logger + */ + protected registerLogger() { + this.app.container.singleton(Logger, async (resolver) => { + const loggerManager = await resolver.make('logger') + return loggerManager.use() + }) + } + + /** + * Registers the logger manager to the container + */ + protected registerLoggerManager() { + this.app.container.singleton('logger', async () => { + const { LoggerManager } = await import('../modules/logger.js') + const config = this.app.config.get('logger') + return new LoggerManager(config) as LoggerService + }) + } + + /** + * Registers the config to the container + */ + protected registerConfig() { + this.app.container.singleton(Config, () => this.app.config) + this.app.container.alias('config', Config) + } + + /** + * Registers emitter service to the container + */ + protected registerEmitter() { + this.app.container.singleton(Emitter, async () => { + return new Emitter(this.app) as Emitter + }) + this.app.container.alias('emitter', Emitter) + } + + /** + * Register the encryption service to the container + */ + protected registerEncryption() { + this.app.container.singleton(Encryption, () => { + const appKey = this.app.config.get('app.appKey') + return new Encryption({ secret: appKey }) + }) + this.app.container.alias('encryption', Encryption) + } + + /** + * Registers the HTTP server with the container as a singleton + */ + protected registerServer() { + this.app.container.singleton(Server, async (resolver) => { + const encryption = await resolver.make('encryption') + const emitter = await resolver.make('emitter') + const logger = await resolver.make('logger') + const config = this.app.config.get('app.http') + return new Server(this.app, encryption, emitter, logger, config) + }) + + this.app.container.alias('server', Server) + } + + /** + * Registers router with the container as a singleton + */ + protected registerRouter() { + this.app.container.singleton(Router, async (resolver) => { + const server = await resolver.make('server') + return server.getRouter() + }) + this.app.container.alias('router', Router) + } + + /** + * Self construct bodyparser middleware class, since it needs + * config that cannot be resolved by the container + */ + protected registerBodyParserMiddleware() { + this.app.container.bind(BodyParserMiddleware, () => { + const config = this.app.config.get('bodyparser') + return new BodyParserMiddleware(config, this.app.experimentalFlags) + }) + } + + /** + * Registeres singleton instance of the "Dumper" module configured + * via the "config/app.ts" file. + */ + protected registerDumper() { + this.app.container.singleton(Dumper, async () => { + const config = this.app.config.get('app.dumper', {}) + const dumper = new Dumper(this.app) + + if (config.html) { + dumper.configureHtmlOutput(config.html) + } + if (config.console) { + dumper.configureAnsiOutput(config.console) + } + + return dumper + }) + + this.app.container.alias('dumper', Dumper) + } + + /** + * Registers bindings + */ + register() { + this.registerApp() + this.registerAce() + this.registerDumper() + this.registerLoggerManager() + this.registerLogger() + this.registerConfig() + this.registerEmitter() + this.registerEncryption() + this.registerTestUtils() + this.registerServer() + this.registerRouter() + this.registerBodyParserMiddleware() + } + + async boot() { + BaseEvent.useEmitter(await this.app.container.make('emitter')) + } +} diff --git a/providers/edge_provider.ts b/providers/edge_provider.ts new file mode 100644 index 00000000..c3330d85 --- /dev/null +++ b/providers/edge_provider.ts @@ -0,0 +1,106 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import edge, { type Edge } from 'edge.js' +import type { ApplicationService } from '../src/types.js' +import { pluginEdgeDumper } from '../modules/dumper/plugins/edge.js' +import { BriskRoute, HttpContext, type Route, type Router } from '../modules/http/main.js' + +declare module '@adonisjs/core/http' { + interface HttpContext { + /** + * Reference to the edge renderer to render templates + * during an HTTP request + */ + view: ReturnType + } + + interface BriskRoute { + /** + * Render an edge template without defining an + * explicit route handler + */ + render(template: string, data?: Record): Route + } +} + +/** + * The Edge service provider configures Edge to work within + * an AdonisJS application environment + */ +export default class EdgeServiceProvider { + constructor(protected app: ApplicationService) { + this.app.usingEdgeJS = true + } + + /** + * Bridge AdonisJS and Edge + */ + async boot() { + const app = this.app + const router = await this.app.container.make('router') + const dumper = await this.app.container.make('dumper') + + function edgeConfigResolver(key: string, defaultValue?: any) { + return app.config.get(key, defaultValue) + } + edgeConfigResolver.has = function (key: string) { + return app.config.has(key) + } + + /** + * Mount the default disk + */ + edge.mount(app.viewsPath()) + + /** + * Cache templates in production + */ + edge.configure({ cache: app.inProduction }) + + /** + * Define Edge global helpers + */ + edge.global('route', function (...args: Parameters) { + return router.makeUrl(...args) + }) + edge.global('signedRoute', function (...args: Parameters) { + return router.makeSignedUrl(...args) + }) + edge.global('app', app) + edge.global('config', edgeConfigResolver) + + /** + * Creating a isolated instance of edge renderer + */ + HttpContext.getter( + 'view', + function (this: HttpContext) { + return edge.createRenderer().share({ + request: this.request, + }) + }, + true + ) + + /** + * Adding brisk route to render templates without an + * explicit handler + */ + BriskRoute.macro('render', function (this: BriskRoute, template, data) { + function rendersTemplate({ view }: HttpContext) { + return view.render(template, data) + } + Object.defineProperty(rendersTemplate, 'listArgs', { value: template, writable: false }) + return this.setHandler(rendersTemplate) + }) + + edge.use(pluginEdgeDumper(dumper)) + } +} diff --git a/providers/hash_provider.ts b/providers/hash_provider.ts new file mode 100644 index 00000000..d00ae726 --- /dev/null +++ b/providers/hash_provider.ts @@ -0,0 +1,62 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { RuntimeException } from '@poppinss/utils' + +import { Hash } from '../modules/hash/main.js' +import { configProvider } from '../src/config_provider.js' +import type { ApplicationService } from '../src/types.js' + +/** + * Registers the passwords hasher with the container + */ +export default class HashServiceProvider { + constructor(protected app: ApplicationService) {} + + /** + * Registering the hash class to resolve an instance with the + * default hasher. + */ + protected registerHash() { + this.app.container.singleton(Hash, async (resolver) => { + const hashManager = await resolver.make('hash') + return hashManager.use() + }) + } + + /** + * Registers the hash manager with the container + */ + protected registerHashManager() { + this.app.container.singleton('hash', async () => { + const hashConfigProvider = this.app.config.get('hash') + + /** + * Resolve config from the provider + */ + const config = await configProvider.resolve(this.app, hashConfigProvider) + if (!config) { + throw new RuntimeException( + 'Invalid "config/hash.ts" file. Make sure you are using the "defineConfig" method' + ) + } + + const { HashManager } = await import('../modules/hash/main.js') + return new HashManager(config) + }) + } + + /** + * Registers bindings + */ + register() { + this.registerHashManager() + this.registerHash() + } +} diff --git a/providers/repl_provider.ts b/providers/repl_provider.ts new file mode 100644 index 00000000..3c07c17b --- /dev/null +++ b/providers/repl_provider.ts @@ -0,0 +1,170 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { homedir } from 'node:os' +import { fsImportAll } from '@poppinss/utils' + +import { Repl } from '../modules/repl.js' +import type { ApplicationService, ContainerBindings } from '../src/types.js' + +/** + * Resolves a container binding and sets it on the REPL + * context + */ +async function resolveBindingForRepl( + app: ApplicationService, + repl: Repl, + binding: keyof ContainerBindings +) { + repl.server!.context[binding] = await app.container.make(binding) + repl.notify( + `Loaded "${binding}" service. You can access it using the "${repl.colors.underline( + binding + )}" variable` + ) +} + +export default class ReplServiceProvider { + constructor(protected app: ApplicationService) {} + + /** + * Registers the REPL binding + */ + register() { + this.app.container.singleton(Repl, async () => { + return new Repl({ + historyFilePath: join(homedir(), '.adonisjs_v6_repl_history'), + }) + }) + this.app.container.alias('repl', Repl) + } + + /** + * Registering REPL bindings during provider boot + */ + async boot() { + this.app.container.resolving('repl', (repl) => { + repl.addMethod( + 'importDefault', + (_, modulePath: string) => { + return this.app.importDefault(modulePath) + }, + { + description: 'Returns the default export for a module', + } + ) + + repl.addMethod( + 'importAll', + (_, dirPath: string) => { + return fsImportAll(this.app.makeURL(dirPath), { + ignoreMissingRoot: false, + }) + }, + { + description: 'Import all files from a directory and assign them to a variable', + } + ) + + repl.addMethod( + 'make', + (_, service: any, runtimeValues?: any[]) => { + return this.app.container.make(service, runtimeValues) + }, + { + description: 'Make class instance using "container.make" method', + } + ) + + repl.addMethod( + 'loadApp', + () => { + return resolveBindingForRepl(this.app, repl, 'app') + }, + { + description: 'Load "app" service in the REPL context', + } + ) + + repl.addMethod( + 'loadEncryption', + () => { + return resolveBindingForRepl(this.app, repl, 'encryption') + }, + { + description: 'Load "encryption" service in the REPL context', + } + ) + + repl.addMethod( + 'loadHash', + () => { + return resolveBindingForRepl(this.app, repl, 'hash') + }, + { + description: 'Load "hash" service in the REPL context', + } + ) + + repl.addMethod( + 'loadRouter', + () => { + return resolveBindingForRepl(this.app, repl, 'router') + }, + { + description: 'Load "router" service in the REPL context', + } + ) + + repl.addMethod( + 'loadConfig', + () => { + return resolveBindingForRepl(this.app, repl, 'config') + }, + { + description: 'Load "config" service in the REPL context', + } + ) + + repl.addMethod( + 'loadTestUtils', + () => { + return resolveBindingForRepl(this.app, repl, 'testUtils') + }, + { + description: 'Load "testUtils" service in the REPL context', + } + ) + + repl.addMethod( + 'loadHelpers', + async () => { + const { default: isModule } = await import('../src/helpers/is.js') + const { default: stringModule } = await import('../src/helpers/string.js') + const helpers = await import('../src/helpers/main.js') + repl.server!.context.helpers = { + string: stringModule, + is: isModule, + ...helpers, + } + + repl.notify( + `Loaded "helpers" module. You can access it using the "${repl.colors.underline( + 'helpers' + )}" variable` + ) + }, + { + description: 'Load "helpers" module in the REPL context', + } + ) + }) + } +} diff --git a/providers/vinejs_provider.ts b/providers/vinejs_provider.ts new file mode 100644 index 00000000..b1cdb68a --- /dev/null +++ b/providers/vinejs_provider.ts @@ -0,0 +1,59 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Vine } from '@vinejs/vine' +import type { ApplicationService } from '../src/types.js' +import { Request, RequestValidator } from '../modules/http/main.js' +import { FileRuleValidationOptions, VineMultipartFile } from '../src/vine.js' + +/** + * Extend VineJS + */ +declare module '@vinejs/vine' { + interface Vine { + file(options?: FileRuleValidationOptions): VineMultipartFile + } +} + +/** + * Extend HTTP request class + */ +declare module '@adonisjs/core/http' { + interface Request extends RequestValidator {} +} + +/** + * The Edge service provider configures Edge to work within + * an AdonisJS application environment + */ +export default class VineJSServiceProvider { + constructor(protected app: ApplicationService) { + this.app.usingVineJS = true + } + + boot() { + const experimentalFlags = this.app.experimentalFlags + + /** + * The file method is used to validate a field to be a valid + * multipart file. + */ + Vine.macro('file', function (this: Vine, options) { + return new VineMultipartFile(options) + }) + + /** + * The validate method can be used to validate the request + * data for the current request using VineJS validators + */ + Request.macro('validateUsing', function (this: Request, ...args) { + return new RequestValidator(this.ctx!, experimentalFlags).validateUsing(...args) + }) + } +} diff --git a/services/ace.ts b/services/ace.ts new file mode 100644 index 00000000..34964d9b --- /dev/null +++ b/services/ace.ts @@ -0,0 +1,26 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { Kernel } from '../modules/ace/main.js' + +let ace: Kernel + +/** + * Returns a singleton instance of the ace kernel + * from the container. + * + * ace service is an instance of the "Kernel" class stored inside + * the "modules/ace/kernel.ts" file + */ +await app.booted(async () => { + ace = await app.container.make('ace') +}) + +export { ace as default } diff --git a/services/app.ts b/services/app.ts new file mode 100644 index 00000000..9b4ed281 --- /dev/null +++ b/services/app.ts @@ -0,0 +1,26 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { ApplicationService } from '../src/types.js' + +let app: ApplicationService + +/** + * Set the application instance the app service should + * be using. Other services relies on the same app + * instance as well. + * + * app service is an instance of the "Application" exported from + * the "modules/app.ts" file. + */ +export function setApp(appService: ApplicationService) { + app = appService +} + +export { app as default } diff --git a/services/config.ts b/services/config.ts new file mode 100644 index 00000000..3d6279f8 --- /dev/null +++ b/services/config.ts @@ -0,0 +1,22 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { ApplicationService } from '../src/types.js' + +let config: ApplicationService['config'] + +/** + * The config service uses the config instance from the app service + */ +await app.booted(() => { + config = app.config +}) + +export { config as default } diff --git a/services/dumper.ts b/services/dumper.ts new file mode 100644 index 00000000..2a1ac888 --- /dev/null +++ b/services/dumper.ts @@ -0,0 +1,30 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { Dumper } from '../modules/dumper/dumper.js' + +let dumper: Dumper + +/** + * dumper service is an instance of the "Dumper" class stored inside + * the "modules/dumper/dumper.ts" file + */ +await app.booted(async () => { + dumper = await app.container.make('dumper') +}) + +/** + * Dump a value and die. The dumped value will be displayed + * using the HTML printer during an HTTP request or within + * the console otherwise. + */ +export const dd = (value: unknown) => { + dumper.dd(value, 2) +} diff --git a/services/emitter.ts b/services/emitter.ts new file mode 100644 index 00000000..0c58ad0f --- /dev/null +++ b/services/emitter.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { EmitterService } from '../src/types.js' + +let emitter: EmitterService + +/** + * Returns a singleton instance of the emitter class + * from the container + */ +await app.booted(async () => { + emitter = await app.container.make('emitter') +}) + +export { emitter as default } diff --git a/services/encryption.ts b/services/encryption.ts new file mode 100644 index 00000000..f7e69b65 --- /dev/null +++ b/services/encryption.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { EncryptionService } from '../src/types.js' + +let encryption: EncryptionService + +/** + * Returns a singleton instance of the encryption class + * from the container + */ +await app.booted(async () => { + encryption = await app.container.make('encryption') +}) + +export { encryption as default } diff --git a/services/hash.ts b/services/hash.ts new file mode 100644 index 00000000..5ef0b6d3 --- /dev/null +++ b/services/hash.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { HashService } from '../src/types.js' + +let hash: HashService + +/** + * Returns a singleton instance of the Hash manager from the + * container + */ +await app.booted(async () => { + hash = await app.container.make('hash') +}) + +export { hash as default } diff --git a/services/logger.ts b/services/logger.ts new file mode 100644 index 00000000..8c8d7910 --- /dev/null +++ b/services/logger.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { LoggerService } from '../src/types.js' + +let logger: LoggerService + +/** + * Returns a singleton instance of the logger class + * from the container + */ +await app.booted(async () => { + logger = await app.container.make('logger') +}) + +export { logger as default } diff --git a/services/repl.ts b/services/repl.ts new file mode 100644 index 00000000..087f8843 --- /dev/null +++ b/services/repl.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { Repl } from '../modules/repl.js' + +let repl: Repl + +/** + * Returns a singleton instance of the Repl class from + * the container + */ +await app.booted(async () => { + repl = await app.container.make('repl') +}) + +export { repl as default } diff --git a/services/router.ts b/services/router.ts new file mode 100644 index 00000000..47e3ff12 --- /dev/null +++ b/services/router.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { HttpRouterService } from '../src/types.js' + +let router: HttpRouterService + +/** + * Returns a singleton instance of the router class from + * the container + */ +await app.booted(async () => { + router = await app.container.make('router') +}) + +export { router as default } diff --git a/services/server.ts b/services/server.ts new file mode 100644 index 00000000..89647638 --- /dev/null +++ b/services/server.ts @@ -0,0 +1,23 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { HttpServerService } from '../src/types.js' + +let server: HttpServerService + +/** + * Returns a singleton instance of the HTTP server + * from the container + */ +await app.booted(async () => { + server = await app.container.make('server') +}) + +export { server as default } diff --git a/services/test_utils.ts b/services/test_utils.ts new file mode 100644 index 00000000..1a55e540 --- /dev/null +++ b/services/test_utils.ts @@ -0,0 +1,26 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import app from './app.js' +import type { TestUtils } from '../src/test_utils/main.js' + +let testUtils: TestUtils + +/** + * Returns a singleton instance of the TestUtils class + * from the container. + * + * testUtils service is an instance of the "TestUtils" exported from + * the "src/test_utils/main.ts" file. + */ +await app.booted(async () => { + testUtils = await app.container.make('testUtils') +}) + +export { testUtils as default } diff --git a/src/Config/index.js b/src/Config/index.js deleted file mode 100644 index e7f38ff8..00000000 --- a/src/Config/index.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const requireAll = require('require-all') -const _ = require('lodash') -const util = require('../../lib/util') - -/** - * Manage configuration for an application by - * reading all .js files from config directory. - */ -class Config { - - constructor (Helpers) { - const configPath = Helpers.configPath() - /** - * @type {Object} - */ - this.config = requireAll({ - dirname: configPath, - filters: /(.*)\.js$/ - }) - } - - /** - * get value for a given key from config store. - * - * @param {String} key - Configuration key to return value for - * @param {Mixed} [defaultValue] - Default value to return when actual value - * is null or undefined - * @return {Mixed} - * - * @example - * Config.get('database.connection') - * Config.get('database.mysql.host') - * - * @public - */ - get (key, defaultValue) { - defaultValue = util.existy(defaultValue) ? defaultValue : null - const returnValue = _.get(this.config, key) - return util.existy(returnValue) ? returnValue : defaultValue - } - - /** - * set/update value for a given key inside - * config store. - * - * @param {String} key - Key to set value for - * @param {Mixed} value - Value to be saved next to defined key - * - * @example - * Config.set('database.connection', 'mysql') - * Config.set('database.mysql.host', 'localhost') - * - * @public - */ - set (key, value) { - _.set(this.config, key, value) - } -} - -module.exports = Config diff --git a/src/Encryption/index.js b/src/Encryption/index.js deleted file mode 100644 index 1e9baf36..00000000 --- a/src/Encryption/index.js +++ /dev/null @@ -1,233 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const crypto = require('crypto') -const CE = require('../Exceptions') - -/** - * Encrypt and decrypt values using nodeJs crypto, make - * sure to set APP_KEY inside .env file. - * - * Not compatible with Laravel because they use serialize()/unserialize() - * @class - */ -class Encryption { - - constructor (Config) { - this.appKey = Config.get('app.appKey') - this.algorithm = Config.get('app.encryption.algorithm', 'aes-256-cbc') - - if (!this.appKey) { - throw CE.RuntimeException.missingAppKey('App key needs to be specified in order to make use of Encryption') - } - - if (!this.supported(this.appKey, this.algorithm)) { - throw CE.RuntimeException.invalidEncryptionCipher() - } - } - - /** - * Determine if the given key and cipher combination is valid. - * - * @param {String} key - * @param {String} cipher - * @return {Boolean} - */ - supported (key, cipher) { - key = key || '' - cipher = cipher || '' - return (cipher.toLowerCase() === 'aes-128-cbc' && key.length === 16) || (cipher.toLowerCase() === 'aes-256-cbc' && key.length === 32) - } - - /** - * encrypts a given value - * - * @param {Mixed} value - value to be encrypted - * @param {String} [encoding=utf8] encoding to be used for input value - * @return {String} - * - * @example - * Encryption.encrypt('somevalue') - * - * @public - */ - encrypt (value, encoding) { - if (!value) { - throw CE.InvalidArgumentException.missingParameter('Could not encrypt the data') - } - - encoding = encoding || 'utf8' - let iv = crypto.randomBytes(this.getIvSize()) - - const cipher = crypto.createCipheriv(this.algorithm, this.appKey, iv) - value = cipher.update(value, encoding, 'base64') - value += cipher.final('base64') - - // Once we have the encrypted value we will go ahead base64_encode the input - // vector and create the MAC for the encrypted value so we can verify its - // authenticity. Then, we'll JSON encode the data in a "payload" array. - const mac = this.hash(iv = this.base64Encode(iv), value) - const json = JSON.stringify({iv: iv, value: value, mac: mac}) - return this.base64Encode(json) - } - - /** - * decrypts encrypted value - * - * @param {String} value - value to decrypt - * @param {String} [encoding=utf8] encoding to be used for output value - * @return {Mixed} - * - * @example - * Encryption.decrypt('somevalue') - * - * @public - */ - decrypt (payload, encoding) { - encoding = encoding || 'utf8' - payload = this.getJsonPayload(payload) - - const iv = this.base64Decode(payload.iv, true) - - const decipher = crypto.createDecipheriv(this.algorithm, this.appKey, iv) - let decrypted = decipher.update(payload.value, 'base64', encoding) - decrypted += decipher.final(encoding) - - if (!decrypted) { - throw CE.RuntimeException.decryptFailed() - } - return decrypted - } - - /** - * get the JSON object from the given payload - * - * @param {String} payload - * @return {Mixed} - * - * @public - */ - getJsonPayload (payload) { - const json = this.base64Decode(payload) - try { - payload = JSON.parse(json) - } catch (e) { - throw CE.RuntimeException.malformedJSON() - } - - // If the payload is not valid JSON or does not have the proper keys set we will - // assume it is invalid and bail out of the routine since we will not be able - // to decrypt the given value. We'll also check the MAC for this encryption. - if (!payload || this.invalidPayload(payload)) { - throw CE.RuntimeException.invalidEncryptionPayload() - } - - if (!this.validMac(payload)) { - throw CE.RuntimeException.invalidEncryptionMac() - } - return payload - } - - /** - * Create a MAC for the given value - * - * @param {String} iv - * @param {String} value - * @return {String} - * - * @public - */ - hash (iv, value) { - return this.hashHmac('sha256', iv + value, this.appKey) - } - - /** - * Generate a keyed hash value using the HMAC method - * - * @param {String} algo - * @param {String} data - * @param {String} key - * @return {String} - * - * @public - */ - hashHmac (algo, data, key) { - return crypto.createHmac(algo, key).update(data).digest('hex') - } - - /** - * returns encoded base64 string - * - * @param {String} unencoded - * @return {String} - * - * @public - */ - base64Encode (unencoded) { - return new Buffer(unencoded || '').toString('base64') - } - - /** - * returns decoded base64 string/buffer - * - * @param {String} encoded - * @param {Boolean} raw - * @return {Mixed} - * - * @public - */ - base64Decode (encoded, raw) { - if (raw) { - return new Buffer(encoded || '', 'base64') - } - return new Buffer(encoded || '', 'base64').toString('utf8') - } - - /** - * Verify that the encryption payload is valid. - * - * @param {Mixed} data - * @return {Boolean} - * - * @public - */ - invalidPayload (data) { - return typeof data !== 'object' || !data.hasOwnProperty('iv') || !data.hasOwnProperty('value') || !data.hasOwnProperty('mac') - } - - /** - * Determine if the MAC for the given payload is valid - * - * @param object payload - * @return {Boolean} - * - * @public - */ - validMac (payload) { - const bytes = crypto.randomBytes(this.getIvSize()) - const calcMac = this.hashHmac('sha256', this.hash(payload.iv, payload.value), bytes) - return this.hashHmac('sha256', payload.mac, bytes) === calcMac - } - - /** - * Get the IV size for the cipher - * - * @return {Integer} - * - * @public - */ - getIvSize () { - return 16 - } - -} - -module.exports = Encryption diff --git a/src/Env/index.js b/src/Env/index.js deleted file mode 100644 index db251ae6..00000000 --- a/src/Env/index.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ -const path = require('path') -const dotenv = require('dotenv') -const util = require('../../lib/util') - -/** - * Manage environment variables by reading .env file - * inside the project root. - * - * @class - */ -class Env { - - constructor (Helpers) { - const envLocation = this.envPath() - const options = { - path: path.isAbsolute(envLocation) ? envLocation : path.join(Helpers.basePath(), envLocation), - silent: process.env.ENV_SILENT || false, - encoding: process.env.ENV_ENCODING || 'utf8' - } - dotenv.load(options) - } - - /** - * returns envPath by checking the environment variables - * - * @method envPath - * - * @return {String} - * - * @public - */ - envPath () { - if (!process.env.ENV_PATH || process.env.ENV_PATH.length === 0) { - return '.env' - } - return process.env.ENV_PATH - } - - /** - * get value of an existing key from - * env file. - * - * @param {String} key - key to read value for - * @param {Mixed} [defaultValue] - default value to be used when actual value - * is undefined or null. - * @return {Mixed} - * - * @example - * Env.get('APP_PORT') - * Env.get('CACHE_VIEWS', false) - * - * @public - */ - get (key, defaultValue) { - defaultValue = util.existy(defaultValue) ? defaultValue : null - let returnValue = process.env[key] || defaultValue - if (returnValue === 'true' || returnValue === '1') { - return true - } - if (returnValue === 'false' || returnValue === '0') { - return false - } - return returnValue - } - - /** - * set/update value for a given key - * - * @param {String} key - Key to set value for - * @param {Mixed} value - value to save next to defined key - * - * @example - * Env.set('CACHE_VIEWS', true) - * - * @public - */ - set (key, value) { - process.env[key] = value - } - -} - -module.exports = Env diff --git a/src/Event/index.js b/src/Event/index.js deleted file mode 100644 index a5b36b85..00000000 --- a/src/Event/index.js +++ /dev/null @@ -1,299 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const EventEmitter2 = require('eventemitter2').EventEmitter2 -const Ioc = require('adonis-fold').Ioc -const Resolver = require('adonis-binding-resolver') -const resolver = new Resolver(Ioc) -const _ = require('lodash') -const util = require('../../lib/util') -const co = require('co') -const CE = require('../Exceptions') - -class Event { - - constructor (Config, Helpers) { - const options = Config.get('event') - this.listenersPath = 'Listeners' - this.helpers = Helpers - this.namedListeners = {} - this.listenerLimit = null - this.emitter = new EventEmitter2(options) - } - - /** - * here we resolve the handler from the IoC container - * or the actual callback if a closure was passed. - * - * @param {String|Function} handler - * @return {Object|Function} - * - * @private - */ - _resolveHandler (handler) { - const formattedHandler = typeof (handler) === 'string' ? this.helpers.makeNameSpace(this.listenersPath, handler) : handler - return resolver.resolveBinding(formattedHandler) - } - - /** - * here we bind the instance to the method, only - * if it exists. - * - * @param {Object} instance - * @param {Function} method - * @return {Function} - * - * @private - */ - _bindInstance (instance, method) { - return function () { - instance.emitter = this - instance.emitter.eventName = instance.emitter.event instanceof Array - ? instance.emitter.event.join(instance.emitter.delimiter) - : instance.emitter.event - method.apply(instance, arguments) - } - } - - /** - * here we wrap the generator method using co.wrap - * and make sure to pass the instance to the - * method. - * - * @param {Object} instance - * @param {Function} method - * @return {Function} - * - * @private - */ - _wrapGenerator (instance, method) { - return co.wrap(function * () { - instance.emitter = this - yield method.apply(instance, arguments) - }) - } - - /** - * here we make the handler method with correct context and - * execution type. It makes it possible to use generator - * methods and other methods as event handler. - * - * @param {Object|Function} handler - * @return {Function} - * - * @private - */ - _makeHandler (handler) { - let parentContext = {} - /** - * if handler is resolved out of IoC container, it will - * be an object with the parent context and the method. - */ - if (typeof (handler) !== 'function' && handler.instance) { - parentContext = handler.instance - handler = handler.instance[handler.method] - } - - /** - * if handler to the event is a generator, then we need to - * wrap it inside co with correct context - */ - if (util.isGenerator(handler)) { - return this._wrapGenerator(parentContext, handler) - } - - /** - * otherwise we bind the parentContext to the method - */ - return this._bindInstance(parentContext, handler) - } - - /** - * returns an array of listeners for a specific event - * - * @param {String} event - * @return {Array} - * - * @public - */ - getListeners (event) { - return this.emitter.listeners(event) - } - - /** - * it should tell whether there are any listeners - * for a given event or not. - * - * @param {String} event - * @return {Boolean} - * - * @public - */ - hasListeners (event) { - return !!this.getListeners(event).length - } - - /** - * returns the status for wildcard - * - * - * @return {Boolean} - * - * @public - */ - wildcard () { - return this.emitter.wildcard - } - - /** - * removes a named handler for a given event from the - * emitter - * - * @param {String} event - * @param {String} name - * - * @public - */ - removeListener (event, name) { - const handler = this.namedListeners[name] - if (!handler) { - throw CE.InvalidArgumentException.missingEvent(event, name) - } - this.emitter.removeListener(event, handler) - } - - /** - * removes all listeners for a given or all events - * - * @param {String} [event] - * - * @public - */ - removeListeners (event) { - event ? this.emitter.removeAllListeners(event) : this.emitter.removeAllListeners() - } - - /** - * emits a given event and passes all the data - * to the handler - * - * @param {...Spread} data - * - * @public - */ - emit () { - const args = _.toArray(arguments) - this.emitter.emit.apply(this.emitter, args) - } - - /** - * @alias emit - */ - fire () { - this.emit.apply(this, arguments) - } - - /** - * binds an handler to any event - * - * @param {Function|Sring} handler - * - * @public - */ - any (handler) { - resolver.validateBinding(handler) - handler = this._resolveHandler(handler) - handler = this._makeHandler(handler) - this.emitter.onAny(handler) - } - - /** - * defines a limit for a listener to be executed - * - * @param {Number} limit - * @return {Object} - * - * @public - */ - times (limit) { - this.listenerLimit = limit - return this - } - - /** - * adds a new event listen for a specific event - * - * @param {String} event - * @param {Function|String} handler - * - * @public - */ - on (event, name, handler) { - if (!handler) { - handler = name - name = null - } - resolver.validateBinding(handler) - handler = this._resolveHandler(handler) - handler = this._makeHandler(handler) - if (name) { - this.namedListeners[name] = handler - } - - /** - * if there is a limit define, go with the many - * method on the emitter - */ - if (this.listenerLimit) { - this.emitter.many(event, this.listenerLimit, handler) - this.listenerLimit = null - return - } - - /** - * otherwise register normally - */ - this.emitter.on(event, handler) - } - - /** - * @alias on - */ - when () { - this.on.apply(this, arguments) - } - - /** - * @alias on - */ - listen () { - this.on.apply(this, arguments) - } - - /** - * adds a new event listen for a specific event - * to be ran only for one time. - * - * @param {String} event - * @param {Function|String} handler - * - * @public - */ - once (event, handler) { - resolver.validateBinding(handler) - handler = this._resolveHandler(handler) - handler = this._makeHandler(handler) - this.emitter.once(event, handler) - } - -} - -module.exports = Event diff --git a/src/Exceptions/index.js b/src/Exceptions/index.js deleted file mode 100644 index e32160bc..00000000 --- a/src/Exceptions/index.js +++ /dev/null @@ -1,198 +0,0 @@ -'use strict' - -const NE = require('node-exceptions') - -class RuntimeException extends NE.RuntimeException { - - /** - * default error code to be used for raising - * exceptions - * - * @return {Number} - */ - static get defaultErrorCode () { - return 500 - } - - /** - * this exception is thrown when a route action is referenced - * inside a view but not registered within the routes file. - * - * @param {String} action - * @param {Number} [code=500] - * - * @return {Object} - */ - static missingRouteAction (action, code) { - return new this(`The action ${action} has not been found`, code || this.defaultErrorCode, 'E_MISSING_ROUTE_ACTION') - } - - /** - * this exception is thrown when a route is referenced inside - * a view but not registered within the routes file. - * - * @param {String} route - * @param {Number} [code=500] - * - * @return {Object} - */ - static missingRoute (route, code) { - return new this(`The route ${route} has not been found`, code || this.defaultErrorCode, 'E_MISSING_ROUTE') - } - - /** - * this exceptions is raised when mac is invalid when - * trying to encrypt data - * - * @param {Number} [code=500] - * - * @return {Object} - */ - static invalidEncryptionMac (code) { - return new this('The MAC is invalid', code || this.defaultErrorCode, 'E_INVALID_ENCRYPTION_MAC') - } - - /** - * this exception is raised when encryption payload is not valid - * - * @param {Number} [code=500] - * - * @return {Object} - */ - static invalidEncryptionPayload (code) { - return new this('The payload is invalid', code || this.defaultErrorCode, 'E_INVALID_ENCRYPTION_PAYLOAD') - } - - /** - * this exception is raised when expected value is - * not a valid json object. - * - * @param {Number} [code=500] - * - * @return {Object} - */ - static malformedJSON (code) { - return new this('The payload is not a json object', code || this.defaultErrorCode, 'E_MALFORMED_JSON') - } - - /** - * this exception is raised when encryption class is not - * able to decrypt a given piece of data - * - * @param {Number} [code=500] - * - * @return {Object} - */ - static decryptFailed (code) { - return new this('Could not decrypt the data', code || this.defaultErrorCode, 'E_ENCRYPTION_DECRYPT_FAILED') - } - - /** - * this exception is raised when the encryption cipher is - * not supported or app key length is not in-sync with - * given cipher - * - * @param {Number} [code=500] - * - * @return {Object} - */ - static invalidEncryptionCipher (code) { - return new this('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths', code || this.defaultErrorCode, 'E_INVALID_ENCRPYTION_CIPHER') - } - - /** - * this exception is raised when app key is missing - * inside config/app.js file. - * - * @param {String} message - * @param {Number} [code=500] - * - * @return {Object} - */ - static missingAppKey (message, code) { - return new this(message, code || this.defaultErrorCode, 'E_MISSING_APPKEY') - } - - /** - * this exception is raised when an uknown - * session driver is used - * - * @param {String} driver - * @param {Number} [code=500] - * - * @return {Object} - */ - static invalidSessionDriver (driver, code) { - return new this(`Unable to locate ${driver} session driver`, code || this.defaultErrorCode, 'E_INVALID_SESSION_DRIVER') - } - - /** - * this exception is raised when a named middleware is used - * but not registered - * - * @param {String} name - * @param {Number} [code=500] - * - * @return {Object} - */ - static missingNamedMiddleware (name, code) { - return new this(`${name} is not registered as a named middleware`, code || this.defaultErrorCode, 'E_MISSING_NAMED_MIDDLEWARE') - } - -} - -class InvalidArgumentException extends NE.InvalidArgumentException { - - /** - * default error code to be used for raising - * exceptions - * - * @return {Number} - */ - static get defaultErrorCode () { - return 500 - } - - /** - * this exception is raised when a method parameter is - * missing but expected to exist. - * - * @param {String} message - * @param {Number} [code=500] - * - * @return {Object} - */ - static missingParameter (message, code) { - return new this(message, code || this.defaultErrorCode, 'E_MISSING_PARAMETER') - } - - /** - * this exception is raised when a method parameter value - * is invalid. - * - * @param {String} message - * @param {Number} [code=500] - * - * @return {Object} - */ - static invalidParameter (message, code) { - return new this(message, code || this.defaultErrorCode, 'E_INVALID_PARAMETER') - } - - /** - * this exception is raised when unable to find - * an event with a given name - * - * @param {String} event - * @param {String} name - * @param {Number} [code=500] - * - * @return {Object} - */ - static missingEvent (event, name, code) { - return new this(`Cannot find an event with ${name} name for ${event} event`, code || this.defaultErrorCode, 'E_MISSING_NAMED_EVENT') - } - -} - -module.exports = {RuntimeException, InvalidArgumentException, HttpException: NE.HttpException} diff --git a/src/File/index.js b/src/File/index.js deleted file mode 100644 index e9bb88ae..00000000 --- a/src/File/index.js +++ /dev/null @@ -1,286 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const path = require('path') -const fs = require('fs') -const bytes = require('bytes') - -/** - * Used by request object internally to manage file uploads. - * - * @class - * - * @alias Request.file - */ -class File { - - constructor (formidableObject, options) { - options = options || {} - this.file = formidableObject - this.file.error = null - this.file.fileName = '' - this.file.maxSize = options.maxSize ? bytes(options.maxSize) : null - this.file.allowedExtensions = options.allowedExtensions || [] - this.file.filePath = '' - } - - /** - * sets error on the file instance and clears - * the file name and path - * - * @param {String} error - * - * @private - */ - _setError (error) { - this.file.error = error - this.file.fileName = '' - this.file.filePath = '' - } - - /** - * sets filePath and name after the move - * and clears the error. - * - * @param {String} fileName - * @param {String} filePath - * - * @private - */ - _setUploadedFile (fileName, filePath) { - this.file.error = null - this.file.fileName = fileName - this.file.filePath = filePath - } - - /** - * sets file size exceeds error - * - * @private - */ - _setFileSizeExceedsError () { - this._setError(`Uploaded file size ${bytes(this.clientSize())} exceeds the limit of ${bytes(this.file.maxSize)}`) - } - - /** - * sets file size extension error - * - * @private - */ - _setFileExtensionError () { - this._setError(`Uploaded file extension ${this.extension()} is not valid`) - } - - /** - * validates the file size - * - * @return {Boolean} - * - * @private - */ - _underAllowedSize () { - return !this.file.maxSize || (this.clientSize() <= this.file.maxSize) - } - - /** - * returns whether file has one of the defined extension - * or not. - * - * @return {Boolean} [description] - * - * @private - */ - _hasValidExtension () { - return !this.file.allowedExtensions.length || this.file.allowedExtensions.indexOf(this.extension()) > -1 - } - - /** - * a method to validate a given file. - * - * @return {Boolean} - */ - validate () { - if (!this._hasValidExtension()) { - this._setFileExtensionError() - return false - } else if (!this._underAllowedSize()) { - this._setFileSizeExceedsError() - return false - } - return true - } - - /** - * validates the file size and move it to the destination - * - * @param {String} fileName - * @param {String} completePath - * - * @return {Promise} - * - * @private - */ - _validateAndMove (fileName, completePath) { - return new Promise((resolve) => { - if (!this.validate()) { - resolve() - return - } - fs.rename(this.tmpPath(), completePath, (error) => { - error ? this._setError(error) : this._setUploadedFile(fileName, completePath) - resolve() - }) - }) - } - - /** - * moves uploaded file from tmpPath to a given location. This is - * an async function. - * - * @param {String} toPath - * @param {String} name - * - * @example - * yield file.move() - * - * @public - */ - move (toPath, name) { - name = name || this.clientName() - const uploadingFileName = `${toPath}/${name}` - return this._validateAndMove(name, uploadingFileName) - } - - /** - * returns name of the uploaded file inside tmpPath. - * - * @return {String} - * - * @public - */ - clientName () { - return this.file.name - } - - /** - * returns file mime type detected from original uploaded file. - * - * @return {String} - * - * @public - */ - mimeType () { - return this.file.type - } - - /** - * returns file extension from original uploaded file. - * - * @return {String} - * - * @public - */ - extension () { - return path.extname(this.clientName()).replace('.', '') - } - - /** - * returns file size of original uploaded file. - * - * @return {String} - * - * @public - */ - clientSize () { - return this.file.size - } - - /** - * returns temporary path of file. - * - * @return {String} - * - * @public - */ - tmpPath () { - return this.file.path - } - - /** - * returns file name after moving file - * - * @return {String} - * - * @public - */ - uploadName () { - return this.file.fileName - } - - /** - * returns complete uploadPath after moving file - * - * @return {String} - * - * @public - */ - uploadPath () { - return this.file.filePath - } - - /** - * tells whether file exists on temporary path or not - * - * @return {Boolean} - * - * @public - */ - exists () { - return !!this.tmpPath() - } - - /** - * tells whether move operation was successful or not - * - * @return {Boolean} - * - * @public - */ - moved () { - return !this.errors() - } - - /** - * returns errors caused while moving file - * - * @return {Object} - * - * @public - */ - errors () { - return this.file.error - } - - /** - * returns the JSON representation of the - * file instance. - * - * @return {Object} - * - * @public - */ - toJSON () { - return this.file - } - -} - -module.exports = File diff --git a/src/Hash/index.js b/src/Hash/index.js deleted file mode 100644 index 34799a1c..00000000 --- a/src/Hash/index.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const bcrypt = require('bcryptjs') - -/** - * Create and verify hash values using Bcrypt as underlaying - * algorithm. - * @module Hash - */ -let Hash = exports = module.exports = {} - -/** - * hash a value with given number of rounds - * - * @method make - * @param {String} value - value to hash - * @param {Number} [rounds=10] - number of rounds to be used for creating hash - * - * @return {Promise} - * - * @public - * - * @example - * yield Hash.make('somepassword') - * yield Hash.make('somepassword', 5) - */ -Hash.make = function (value, rounds) { - rounds = rounds || 10 - return new Promise(function (resolve, reject) { - bcrypt.hash(value, rounds, function (error, hash) { - if (error) { - return reject(error) - } - resolve(hash) - }) - }) -} - -/** - * verifies a given value against hash value - * - * @method verify - * @param {String} value - Plain value - * @param {String} hash - Previously hashed value - * - * @return {Promise} - * - * @public - * - * @example - * yield Hash.verify('plainpassword', 'hashpassword') - */ -Hash.verify = function (value, hash) { - return new Promise(function (resolve, reject) { - bcrypt.compare(value, hash, function (error, response) { - if (error) { - return reject(error) - } - resolve(response) - }) - }) -} diff --git a/src/Helpers/index.js b/src/Helpers/index.js deleted file mode 100644 index 74ee23b2..00000000 --- a/src/Helpers/index.js +++ /dev/null @@ -1,362 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const path = require('path') -const _ = require('lodash') -const CatLog = require('cat-log') -const log = new CatLog('adonis:framework') -const NE = require('node-exceptions') - -let rootPath = '' // application root path -let autoloadNameSpace = '' // autoloading namespace | required for backword compatibility - -/** - * path to base directories with relative - * paths from the project root. - * - * @type {Object} - */ -let originalProjectDirectories = { - public: 'public', - storage: 'storage', - database: 'database', - resources: 'resources', - config: 'config', - app: 'app' -} - -/** - * cloning over original app directories so that orignal - * reset method should be able to restore it. - * - * @type {Object} - */ -let projectDirectories = _.clone(originalProjectDirectories) - -/** - * Manage commonly required methods to be used anywhere inside - * the application - * @module Helpers - */ -let Helpers = exports = module.exports = {} - -/** - * loads package.json file from application and set required paths - * and namespace based on same. - * - * @method load - * - * @param {String} packagePath - * @param {Object} Ioc - * - * @throws {DomainException} If autoload is not defined in package.json file - * - * @public - */ -Helpers.load = function (packagePath, Ioc) { - Helpers.reset() // reset itself before start - log.verbose('reading autoload settings from %s', packagePath) - - rootPath = path.dirname(packagePath) - - const packageFile = require(packagePath) - if (!packageFile.autoload) { - throw new NE.DomainException('autoload must be enable inside package.json file') - } - - const autoloadSettings = Object.keys(packageFile.autoload) - if (!autoloadSettings.length) { - throw new NE.DomainException('autoload must be enable inside package.json file') - } - - autoloadNameSpace = autoloadSettings[0] - Helpers.setProjectDirectory('app', packageFile.autoload[autoloadNameSpace]) - if (Ioc && Ioc.autoload) { - Ioc.autoload(autoloadNameSpace, path.join(rootPath, projectDirectories.app)) - } -} - -/** - * the load method to be shipped with 3.1 - * - * @param {String} appRoot - * - * @public - */ -Helpers.loadInFuture = function (appRoot) { - rootPath = appRoot -} - -/** - * reset helpers state back to original - * - * @public - */ -Helpers.reset = function () { - projectDirectories = _.clone(originalProjectDirectories) - rootPath = null - autoloadNameSpace = null -} - -/** - * returns the current mapping of directories - * - * @return {Object} - * - * @public - */ -Helpers.getProjectDirectories = function () { - return projectDirectories -} - -/** - * overrides the current mapping of directories - * - * @param {Object} directories - * - * @public - */ -Helpers.setProjectDirectories = function (directories) { - projectDirectories = directories -} - -/** - * overrides a give mapping of directories. - * - * @param {String} name - * @param {String} toPath - * - * @public - */ -Helpers.setProjectDirectory = function (name, toPath) { - projectDirectories[name] = toPath -} - -/** - * Returns absolute path to application root - * - * @method basePath - * - * @return {String} - */ -Helpers.basePath = function () { - return rootPath -} - -/** - * Returns absolute path to application folder which is - * defined under a given namespace. - * - * @method appPath - * - * @return {String} - */ -Helpers.appPath = function () { - const toDir = projectDirectories.app - return Helpers._makePath(rootPath, toDir) -} - -/** - * Returns absolute path to application public folder or path to a - * given file inside public folder. - * - * @method publicPath - * - * @param {String} [toFile] - filename to return path for - * @return {String} - */ -Helpers.publicPath = function (toFile) { - const toDir = projectDirectories.public - return Helpers._makePath(rootPath, toDir, toFile) -} - -/** - * Returns application namespace , under which - * app directory is registered. - * - * @method appNameSpace - * - * @return {String} - */ -Helpers.appNameSpace = function () { - return autoloadNameSpace -} - -/** - * makes complete namespace for a given path and base - * namespace - * - * @method makeNameSpace - * - * @param {String} baseNameSpace - * @param {String} toPath - * @return {String} - * - * @public - */ -Helpers.makeNameSpace = function (baseNameSpace, toPath) { - const appNameSpace = Helpers.appNameSpace() - if (toPath.startsWith(appNameSpace)) { - return toPath - } - return path.normalize(`${appNameSpace}/${baseNameSpace}/${toPath}`) -} - -/** - * returns absolute path to config directory or a file inside - * config directory - * - * @method configPath - * - * @param {String} [toFile] - filename to return path for - * @return {String} - */ -Helpers.configPath = function (toFile) { - const toDir = projectDirectories.config - return Helpers._makePath(rootPath, toDir, toFile) -} - -/** - * returns absolute path to storage path of application or an - * file inside the storage path. - * - * @method storagePath - * - * @param {String} [toFile] - filename to return path for - * @return {String} - * - * @public - */ -Helpers.storagePath = function (toFile) { - const toDir = projectDirectories.storage - return Helpers._makePath(rootPath, toDir, toFile) -} - -/** - * returns absolute path to resources directory or a file inside - * resources directory - * - * @method resourcesPath - * - * @param {String} [toFile] - filename to return path for - * @return {String} - * - * @public - */ -Helpers.resourcesPath = function (toFile) { - const toDir = projectDirectories.resources - return Helpers._makePath(rootPath, toDir, toFile) -} - -/** - * returns absolute path to database/migrations directory. - * - * @method migrationsPath - * - * @param {String} [toFile] - filename to return path for - * @return {String} - * - * @public - */ -Helpers.migrationsPath = function (toFile) { - const toDir = toFile ? `./migrations/${toFile}` : './migrations' - return Helpers.databasePath(toDir) -} - -/** - * returns absolute path to database/seeds directory. - * - * @method seedsPath - * - * @param {String} [toFile] - filename to return path for - * @return {String} - * - * @public - */ -Helpers.seedsPath = function (toFile) { - const toDir = toFile ? `./seeds/${toFile}` : './seeds' - return Helpers.databasePath(toDir) -} - -/** - * returns absolute path to database/factories directory. - * - * @method factoriesPath - * - * @param {String} [toFile] - filename to return path for - * @return {String} - * - * @public - */ -Helpers.factoriesPath = function (toFile) { - const toDir = toFile ? `./factories/${toFile}` : './factories' - return Helpers.databasePath(toDir) -} - -/** - * returns path to the database directory. - * - * @method databasePath - * - * @param {String} toFile - * @return {String} - * - * @public - */ -Helpers.databasePath = function (toFile) { - const toDir = projectDirectories.database - return Helpers._makePath(rootPath, toDir, toFile) -} - -/** - * returns whether the process belongs to ace command - * or not. - * - * @method isAceCommand - * - * @return {Boolean} - * - * @public - */ -Helpers.isAceCommand = function () { - const processFile = process.mainModule.filename - return processFile.endsWith('ace') -} - -/** - * returns absolute path to views directory - * - * @method viewsPath - * - * @return {String} - * - * @public - */ -Helpers.viewsPath = function () { - return Helpers.resourcesPath('views') -} - -/** - * makes path by joining two endpoints - * - * @method _makePath - * - * @param {String} base - * @param {String} toDir - * @param {String} toFile - * @return {String} - * - * @private - */ -Helpers._makePath = function (base, toDir, toFile) { - toDir = path.isAbsolute(toDir) ? toDir : path.join(base, toDir) - return toFile ? path.join(toDir, toFile) : toDir -} diff --git a/src/Middleware/index.js b/src/Middleware/index.js deleted file mode 100644 index 1945b396..00000000 --- a/src/Middleware/index.js +++ /dev/null @@ -1,250 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const _ = require('lodash') -const Ioc = require('adonis-fold').Ioc -const CE = require('../Exceptions') - -let globalMiddleware = [] -let namedMiddleware = {} - -/** - * composes a closure to an object for consistent behaviour - * - * @method _composeFunction - * - * @param {Function} middleware - * - * @return {Object} - * - * @private - */ -const _composeFunction = function (middleware) { - return {instance: null, method: middleware, parameters: []} -} - -/** - * composes a consistent object from the actual - * middleware object - * - * @method _composeObject - * - * @param {Object} middleware - * - * @return {Object} - * - * @private - */ -const _composeObject = function (middleware) { - const instance = middleware.instance || null - const method = instance ? instance[middleware.method] : middleware.method - return {instance, method, parameters: middleware.parameters} -} - -/** - * Http middleware layer to register and resolve middleware - * for a given HTTP request. - * @module Middleware - */ -let Middleware = exports = module.exports = {} - -/** - * clears off all global and named middleware - * - * @method new - * - * @public - */ -Middleware.new = function () { - globalMiddleware = [] - namedMiddleware = {} -} - -/** - * registers a new global or named middleware. If second - * parameter is empty, middleware will be considered - * global. - * - * @method register - * - * @param {String} [key] - unqiue key for named middleware - * @param {String} namespace - Reference to the binding of Ioc container - * - * @example - * Middleware.register('App/Http/Middleware/Auth') - * Middleware.register('app', 'App/Http/Middleware/Auth') - * - * @public - */ -Middleware.register = function (key, namespace) { - if (!namespace) { - globalMiddleware.push(key) - return - } - namedMiddleware[key] = namespace -} - -/** - * concats a array of middleware inside global list. - * - * @method global - * - * @param {Array} arrayOfMiddleware - * - * @example - * Middleware.global(['App/Http/Middleware/Auth', '...']) - * - * @public - */ -Middleware.global = function (arrayOfMiddleware) { - globalMiddleware = globalMiddleware.concat(_.uniq(arrayOfMiddleware)) -} - -/** - * adds an object of middleware to named list. - * - * @method named - * - * @param {Object} namedMiddleware - * - * @example - * Middleware.named({'auth': 'App/Http/Middleware/Auth'}, {...}) - * - * @public - */ -Middleware.named = function (namedMiddleware) { - _.each(namedMiddleware, (namespace, key) => Middleware.register(key, namespace)) -} - -/** - * returns list of global middleware - * - * @method getGlobal - * - * @return {Array} - * - * @public - */ -Middleware.getGlobal = function () { - return globalMiddleware -} - -/** - * returns list of named middleware - * - * @method getNamed - * - * @return {Object} - * - * @public - */ -Middleware.getNamed = function () { - return namedMiddleware -} - -/** - * fetch params defined next to named middleware while - * consuming them. - * - * @method fetchParams - * - * @param {String|Undefined} params - * @return {Array} - * - * @public - */ -Middleware.fetchParams = function (params) { - return params ? params.split(',') : [] -} - -/** - * returning an object of named middleware by - * parsing them. - * - * @method formatNamedMiddleware - * - * @param {Array} keys - * @return {Object} - * - * @example - * Middleware.formatNamedMiddleware(['auth:basic,jwt']) - * returns - * {'Adonis/Middleware/Auth': ['basic', 'jwt']} - * - * @throws {RunTimeException} If named middleware for a given - * key is not registered. - * @public - */ -Middleware.formatNamedMiddleware = function (keys) { - return _.reduce(keys, (structured, key) => { - const tokens = key.split(':') - const middlewareNamespace = namedMiddleware[tokens[0]] - if (!middlewareNamespace) { - throw CE.RuntimeException.missingNamedMiddleware(tokens[0]) - } - structured[middlewareNamespace] = Middleware.fetchParams(tokens[1]) - return structured - }, {}) -} - -/** - * resolves an array of middleware namespaces from - * ioc container - * - * @method resolve - * - * @param {Object} namedMiddlewareHash - * @param {Boolean} [includeGlobal=false] - * - * @return {Array} - * - * @example - * Middleware.resolve({}, true) // all global - * Middleware.resolve(Middleware.formatNamedMiddleware(['auth:basic', 'acl:user'])) - * - * @public - */ -Middleware.resolve = function (namedMiddlewareHash, includeGlobal) { - const finalSet = includeGlobal ? Middleware.getGlobal().concat(_.keys(namedMiddlewareHash)) : _.keys(namedMiddlewareHash) - return _.map(finalSet, (item) => { - const func = Ioc.makeFunc(`${item}.handle`) - func.parameters = namedMiddlewareHash[item] || [] - return func - }) -} - -/** - * composes middleware and calls them in sequence something similar - * to koa-compose. - * - * @method compose - * - * @param {Array} Middleware - Array of middleware resolved from Ioc container - * @param {Object} request - Http request object - * @param {Object} response - Http response object - * - * @public - */ -Middleware.compose = function (middlewareList, request, response) { - function * noop () {} - return function * (next) { - next = next || noop() - _(middlewareList) - .map((middleware) => { - return typeof (middleware) === 'function' ? _composeFunction(middleware) : _composeObject(middleware) - }) - .forEachRight((middleware) => { - const values = [request, response, next].concat(middleware.parameters) - next = middleware.method.apply(middleware.instance, values) - }) - return yield * next - } -} diff --git a/src/Request/index.js b/src/Request/index.js deleted file mode 100644 index 2a0df3f0..00000000 --- a/src/Request/index.js +++ /dev/null @@ -1,587 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const nodeReq = require('node-req') -const nodeCookie = require('node-cookie') -const File = require('../File') -const pathToRegexp = require('path-to-regexp') -const _ = require('lodash') -const util = require('../../lib/util') - -/** - * Glued http request object to read values for - * a given request. Instance of this class - * is generated automatically on every - * new request. - * @class - */ -class Request { - - constructor (request, response, Config) { - this.request = request - this.response = response - this.config = Config - this._body = {} - this._files = [] - - /** - * secret to parse and decrypt cookies - * @type {String} - */ - this.secret = this.config.get('app.appKey') - - /** - * holding references to cookies once they - * have been parsed. It is required to - * optimize performance as decrypting - * is an expensive operation - * @type {Object} - */ - this.cookiesObject = {} - - /** - * flag to find whether cookies have been - * parsed once or not - * @type {Boolean} - */ - this.parsedCookies = false - } - - /** - * returns input value for a given key from post - * and get values. - * - * @param {String} key - Key to return value for - * @param {Mixed} defaultValue - default value to return when actual - * value is empty - * @return {Mixed} - * - * @example - * request.input('name') - * request.input('profile.name') - * - * @public - */ - input (key, defaultValue) { - defaultValue = util.existy(defaultValue) ? defaultValue : null - const input = this.all() - const value = _.get(input, key) - return util.existy(value) ? value : defaultValue - } - - /** - * returns merged values from get and post methods. - * - * @return {Object} - * - * @public - */ - all () { - return _.merge(this.get(), this.post()) - } - - /** - * returns all input values except defined keys - * - * @param {Mixed} keys an array of keys or multiple keys to omit values for - * @return {Object} - * - * @example - * request.except('password', 'credit_card') - * request.except(['password', 'credit_card']) - * - * @public - */ - except () { - const args = _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments) - return _.omit(this.all(), args) - } - - /** - * returns all input values for defined keys only - * - * @param {Mixed} keys an array of keys or multiple keys to pick values for - * @return {Object} - * - * @example - * request.only('name', 'email') - * request.only(['name', 'name']) - * - * @public - */ - only () { - const args = _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments) - return _.pick(this.all(), args) - } - - /** - * returns a group of objects with defined keys and values - * corresponding to them. It is helpful when accepting - * an array of values via form submission. - * - * @param {Mixed} keys an array of keys or multiple keys to pick values for - * @return {Array} - * - * @example - * request.collect('name', 'email') - * request.collect(['name', 'email']) - * - * @public - */ - collect () { - const args = _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments) - const selectedValues = this.only(args) - - /** - * need to make sure the values array is in balance to the expected - * array. Otherwise map method will pickup values for wrong keys. - */ - if (_.size(args) > _.size(selectedValues)) { - args.forEach((key) => { selectedValues[key] = selectedValues[key] || [] }) - } - - const keys = _.keys(selectedValues) - const values = _.zip.apply(_, _.values(selectedValues)) - return _.map(values, (item, index) => { - const group = {} - _.each(args, (k, i) => { group[keys[i]] = item[i] || null }) - return group - }) - } - - /** - * returns query parameters from request querystring - * - * @return {Object} - * - * @public - */ - get () { - return nodeReq.get(this.request) - } - - /** - * returns post body from request, BodyParser - * middleware needs to be enabled for this to work - * - * @return {Object} - * - * @public - */ - post () { - return this._body || {} - } - - /** - * returns header value for a given key - * - * @param {String} key - * @param {Mixed} defaultValue - default value to return when actual - * value is undefined or null - * @return {Mixed} - * - * @example - * request.header('Authorization') - * - * @public - */ - header (key, defaultValue) { - defaultValue = util.existy(defaultValue) ? defaultValue : null - const headerValue = nodeReq.header(this.request, key) - return util.existy(headerValue) ? headerValue : defaultValue - } - - /** - * returns all request headers from a given request - * - * @return {Object} - * - * @public - */ - headers () { - return nodeReq.headers(this.request) - } - - /** - * tells whether request is fresh or not by - * checking Etag and expires header - * - * @return {Boolean} - * - * @public - */ - fresh () { - return nodeReq.fresh(this.request, this.response) - } - - /** - * opposite of fresh - * - * @see fresh - * - * @return {Boolean} - * - * @public - */ - stale () { - return nodeReq.stale(this.request, this.response) - } - - /** - * returns most trusted ip address for a given request. Proxy - * headers are trusted only when app.http.trustProxy is - * enabled inside config file. - * - * @uses app.http.subdomainOffset - * - * @return {String} - * - * @public - */ - ip () { - return nodeReq.ip(this.request, this.config.get('app.http.trustProxy')) - } - - /** - * returns an array of ip addresses sorted from most to - * least trusted. Proxy headers are trusted only when - * app.http.trustProxy is enabled inside config file. - * - * @uses app.http.subdomainOffset - * - * @return {Array} - * - * @public - */ - ips () { - return nodeReq.ips(this.request, this.config.get('app.http.trustProxy')) - } - - /** - * tells whether request is on https or not - * - * @return {Boolean} - * - * @public - */ - secure () { - return nodeReq.secure(this.request) - } - - /** - * returns an array of subdomains from url. Proxy headers - * are trusted only when app.http.trustProxy is enabled - * inside config file. - * - * @uses app.http.subdomainOffset - * @uses app.http.trustProxy - * - * @return {Array} - * - * @public - */ - subdomains () { - return nodeReq.subdomains(this.request, this.config.get('app.http.trustProxy'), this.config.get('app.http.subdomainOffset')) - } - - /** - * tells whether request is an ajax request or not - * - * @return {Boolean} - * - * @public - */ - ajax () { - return nodeReq.ajax(this.request) - } - - /** - * tells whether request is pjax or - * not based on X-PJAX header - * - * @return {Boolean} - * - * @public - */ - pjax () { - return nodeReq.pjax(this.request) - } - - /** - * returns request hostname - * - * @uses app.http.subdomainOffset - * - * @return {String} - * - * @public - */ - hostname () { - return nodeReq.hostname(this.request, this.config.get('app.http.trustProxy')) - } - - /** - * returns request url without query string - * - * @return {String} - * - * @public - */ - url () { - return nodeReq.url(http://github.com/this.request) - } - - /** - * returns request original Url with query string - * - * @return {String} - * - * @public - */ - originalUrl () { - return nodeReq.originalUrl(this.request) - } - - /** - * tells whether request is of certain type - * based upon Content-type header - * - * @return {Boolean} - * - * @example - * request.is('text/html', 'text/plain') - * request.is(['text/html', 'text/plain']) - * - * @public - */ - is () { - const args = _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments) - return nodeReq.is(this.request, args) - } - - /** - * returns the best response type to be accepted using Accepts header - * - * @return {String} - * - * @example - * request.accepts('text/html', 'application/json') - * request.accepts(['text/html', 'application/json']) - * - * @public - */ - accepts () { - const args = _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments) - return nodeReq.accepts(this.request, args) - } - - /** - * returns request method or verb in HTTP terms - * - * @return {String} - * - * @public - */ - method () { - return nodeReq.method(this.request) - } - - /** - * returns cookie value for a given key - * - * @param {String} key - Key for which value should be returnd - * @param {Mixed} defaultValue - default value to return when actual - * value is undefined or null - * @return {Mixed} - * - * @public - */ - cookie (key, defaultValue) { - defaultValue = util.existy(defaultValue) ? defaultValue : null - const cookies = this.cookies() - return util.existy(cookies[key]) ? cookies[key] : defaultValue - } - - /** - * returns all cookies associated to a given request - * - * @return {Object} - * - * @public - */ - cookies () { - const secret = this.secret || null - const decrypt = !!this.secret - - /** - * avoiding re-parsing of cookies if done once - */ - if (!this.parsedCookies) { - this.cookiesObject = nodeCookie.parse(this.request, secret, decrypt) - this.parsedCookies = true - } - - return this.cookiesObject - } - - /** - * return route param value for a given key - * - * @param {String} key - key for which the value should be return - * @param {Mixed} defaultValue - default value to be returned with actual - * is null or undefined - * @return {Mixed} - * - * @public - */ - param (key, defaultValue) { - defaultValue = util.existy(defaultValue) ? defaultValue : null - return util.existy(this.params()[key]) ? this.params()[key] : defaultValue - } - - /** - * returns all route params - * - * @return {Object} - * - * @public - */ - params () { - return this._params || {} - } - - /** - * converts a file object to file instance - * if already is not an instance - * - * @param {Object} file - * @param {Object} [options] - * @return {Object} - * @private - */ - _toFileInstance (file, options) { - if (!(file instanceof File)) { - file = new File(file, options) - } - return file - } - - /** - * returns uploaded file instance for a given key - * @instance Request.file - * - * @param {String} key - * @param {Objecr} [options] - * @return {Object} - * - * @example - * request.file('avatar') - * @public - */ - file (key, options) { - /** - * if requested file was not uploaded return an - * empty instance of file object. - */ - if (!this._files[key]) { - return null - } - - /** - * grabbing file from uploaded files and - * converting them to file instance - */ - const fileToReturn = this._files[key] - - /** - * if multiple file upload , convert them to - * file instances - */ - if (_.isArray(fileToReturn)) { - return _.map(fileToReturn, (file) => this._toFileInstance(file.toJSON(), options)) - } - return this._toFileInstance(fileToReturn.toJSON(), options) - } - - /** - * returns all uploded files by converting - * them to file instances - * - * @return {Array} - * - * @public - */ - files () { - return _.map(this._files, (file, index) => { - return this.file(index) - }) - } - - /** - * tells whether a given pattern matches the current url or not - * - * @param {String} pattern - * @return {Boolean} - * - * @example - * request.match('/user/:id', 'user/(+.)') - * request.match(['/user/:id', 'user/(+.)']) - * - * @public - */ - match () { - const args = _.isArray(arguments[0]) ? arguments[0] : _.toArray(arguments) - const url = this.url() - const pattern = pathToRegexp(args, []) - return pattern.test(url) - } - - /** - * returns request format enabled by using - * .formats on routes - * - * @return {String} - * - * @example - * request.format() - * - * @public - */ - format () { - return this.param('format') ? this.param('format').replace('.', '') : null - } - - /** - * tells whether or not request has body. It can be - * used by bodyParsers to decide whether or not to parse body - * - * @return {Boolean} - * - * @public - */ - hasBody () { - return nodeReq.hasBody(this.request) - } - - /** - * adds a new method to the request prototype - * - * @param {String} name - * @param {Function} callback - * - * @public - */ - static macro (name, callback) { - this.prototype[name] = callback - } -} - -module.exports = Request diff --git a/src/Response/index.js b/src/Response/index.js deleted file mode 100644 index bac8cb24..00000000 --- a/src/Response/index.js +++ /dev/null @@ -1,354 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const nodeRes = require('node-res') -const nodeCookie = require('node-cookie') -let viewInstance = null -let routeInstance = null -let configInstance = null - -/** - * Glued http response to end requests by sending - * proper formatted response. - * @class - */ -class Response { - - constructor (request, response) { - this.request = request - this.response = response - if (configInstance.get('app.http.setPoweredBy', true)) { - nodeRes.header(this.response, 'X-Powered-By', 'AdonisJs') - } - nodeRes.descriptiveMethods.forEach((method) => { - this[method] = (body) => { - nodeRes[method](this.request.request, this.response, body) - } - }) - } - - /** - * returns whether request has been - * finished or not - * - * @method finished - * - * @return {Boolean} - */ - get finished () { - return this.response.finished - } - - /** - * returns whether request headers - * have been sent or not - * - * @method headersSent - * - * @return {Boolean} - */ - get headersSent () { - return this.response.headersSent - } - - /** - * returns whether a request is pending - * or not - * - * @method isPending - * - * @return {Boolean} - */ - get isPending () { - return (!this.headersSent && !this.finished) - } - - /** - * sets key/value pair on response header - * - * @param {String} key - key to set value for - * @param {Mixed} value - key to save corresponding to given key - * @return {Object} - Reference to class instance for chaining methods - * - * @example - * response.header('Content-type', 'application/json') - * - * @public - */ - header (key, value) { - nodeRes.header(this.response, key, value) - return this - } - - /** - * creates a new view using View class - * @async - * - * @param {String} template - * @param {Object} options - * @returns {Html} - compiled html template - * - * @example - * yield response.view('index') - * yield response.view('profile', {name: 'doe'}) - * - * @public - */ - * view (template, options) { - return viewInstance.make(template, options) - } - - /** - * creates a new view using View class and ends the - * response by sending compilied html template - * back as response content. - * - * @param {String} template - * @param {Object} options - * - * @example - * yield response.sendView('index') - * yield response.sendView('profile', {name: 'doe'}) - * @public - */ - * sendView (template, options) { - const view = yield this.view(template, options) - this.send(view) - } - - /** - * removes previously added header. - * - * @param {String} key - * @return {Object} - reference to class instance for chaining - * - * @example - * response.removeHeader('Accept') - * - * @public - */ - removeHeader (key) { - nodeRes.removeHeader(this.response, key) - return this - } - - /** - * set's response status, make it adhers to RFC specifications - * - * @see {@link https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} - * - * @param {Number} statusCode - * @return {Object} - reference to class instance for chaining - * - * @example - * response.status(200) - * @public - */ - status (statusCode) { - nodeRes.status(this.response, statusCode) - return this - } - - /** - * ends response, should not be used with - * send method - * - * @public - */ - end () { - nodeRes.end(this.response) - } - - /** - * writes content to response and sends it back as - * response body - * - * @param {Mixed} body - * - * @public - */ - send (body) { - nodeRes.send(this.request.request, this.response, body) - } - - /** - * writes json response using send method and sets content-type - * to application/json - * - * @param {Object} body - * - * @public - */ - json (body) { - nodeRes.json(this.request.request, this.response, body) - } - - /** - * writes jsonp response using send method and sets content-type - * to text/javascript - * - * @uses app.http.jsonpCallback - * @param {Object} body - * - * @public - */ - jsonp (body) { - const callback = this.request.input('callback') || configInstance.get('app.http.jsonpCallback') - nodeRes.jsonp(this.request.request, this.response, body, callback) - } - - /** - * streams file content to response and ends request - * once done. - * - * @param {String} filePath - path to file from where to read contents - * - * @example - * response.download('absolute/path/to/file') - * - * @public - */ - download (filePath) { - nodeRes.download(this.request, this.response, filePath) - } - - /** - * force download input file by setting content-disposition - * - * @param {String} filePath - path to file from where to read contents - * @param {String} name - downloaded file name - * @param {String} [disposition=attachment] - content disposition - * - * @example - * response.attach('absolute/path/to/file', 'name') - * - * @public - */ - attachment (filePath, name, disposition) { - nodeRes.attachment(this.request, this.response, filePath, name, disposition) - } - - /** - * sets location header on response - * - * @param {String} toUrl - * @return {Object} - reference to class instance for chaining - * - * @public - */ - location (toUrl) { - if (toUrl === 'back') { - toUrl = this.request.header('Referrer') || '/' - } - nodeRes.location(this.response, toUrl) - return this - } - - /** - * redirect request to a given url and ends the request - * - * @param {String} toUrl - url to redirect to - * @param {Number} [status=302] - http status code - * - */ - redirect (toUrl, status) { - if (toUrl === 'back') { - toUrl = this.request.header('Referrer') || '/' - } - nodeRes.redirect(this.request.request, this.response, toUrl, status) - } - - /** - * sets vary header on response - * - * @param {String} field - * @return {Object} - reference to class instance for chaining - */ - vary (field) { - nodeRes.vary(this.response, field) - return this - } - - /** - * redirects to a registered route from routes.js files - * - * @param {String} route - name of the route - * @param {Object} data - route params - * @param {Number} [status=302] - http status code - * - * @example - * response.route('/profile/:id', {id: 1}) - * response.route('user.profile', {id: 1}) - * @public - */ - route (route, data, status) { - const toUrl = routeInstance.url(http://github.com/route,%20data) - this.redirect(toUrl, status) - } - - /** - * adds new cookie to response cookies - * - * @param {String} key - cookie name - * @param {Mixed} value - value to be saved next for cookie name - * @param {Object} options - options to define cookie path,host age etc. - * @return {Object} - reference to class instance for chaining - * - * @example - * response.cookie('cart', values) - * response.cookie('cart', values, { - * maxAge: 1440, - * httpOnly: false - * }) - * - * @public - */ - cookie (key, value, options) { - const secret = configInstance.get('app.appKey') - const encrypt = !!secret - nodeCookie.create(this.request.request, this.response, key, value, options, secret, encrypt) - return this - } - - /** - * clears existing cookie from response header - * - * @param {String} key - * @param {Object} options - * - * @return {Object} - reference to class instance for chaining - */ - clearCookie (key, options) { - nodeCookie.clear(this.request.request, this.response, key, options) - return this - } - - /** - * adds a new method to the response prototype - * - * @param {String} name - * @param {Function} callback - * - * @public - */ - static macro (name, callback) { - this.prototype[name] = callback - } - -} - -class ResponseBuilder { - constructor (View, Route, Config) { - viewInstance = View - routeInstance = Route - configInstance = Config - return Response - } -} - -module.exports = ResponseBuilder diff --git a/src/Route/ResourceCollection.js b/src/Route/ResourceCollection.js deleted file mode 100644 index 1e1a2f14..00000000 --- a/src/Route/ResourceCollection.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const helpers = require('./helpers') -const util = require('../../lib/util') -const CatLog = require('cat-log') -const logger = new CatLog('adonis:framework') - -class ResourceCollection { - - constructor (route) { - this.route = route - } - - /** - * binds action to the route, it will override - * the old action - * - * @param {String|Function} action - * - * @return {Object} - * - * @public - */ - bindAction (action) { - this.route.handler = action - return this - } - - /** - * @see this.middleware - */ - middlewares () { - logger.warn('collection@middlewares: consider using method middleware, instead of middlewares') - return this.middleware.apply(this, arguments) - } - - /** - * appends middlewares to the route - * - * @return {Object} - * - * @public - */ - middleware () { - helpers.appendMiddleware( - this.route, - util.spread.apply(this, arguments) - ) - return this - } - - /** - * assign name to the route - * - * @param {String} name - * - * @return {Object} - * - * @public - */ - as (name) { - this.route.name = name - return this - } - - /** - * return json representation of the route - * - * @return {Object} - * - * @public - */ - toJSON () { - return this.route - } - -} - -module.exports = ResourceCollection diff --git a/src/Route/ResourceMember.js b/src/Route/ResourceMember.js deleted file mode 100644 index f2499e2b..00000000 --- a/src/Route/ResourceMember.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const helpers = require('./helpers') -const util = require('../../lib/util') -const CatLog = require('cat-log') -const logger = new CatLog('adonis:framework') - -class ResourceMember { - - constructor (route) { - this.route = route - } - - /** - * binds action to the route, it will override - * the old action - * - * @param {String|Function} action - * - * @return {Object} - * - * @public - */ - bindAction (action) { - this.route.handler = action - return this - } - - /** - * @see this.middleware - */ - middlewares () { - logger.warn('member@middlewares: consider using method middleware, instead of middlewares') - return this.middleware.apply(this, arguments) - } - - /** - * appends middlewares to the route - * - * @return {Object} - * - * @public - */ - middleware () { - helpers.appendMiddleware( - this.route, - util.spread.apply(this, arguments) - ) - return this - } - - /** - * assign name to the route - * - * @param {String} name - * - * @return {Object} - * - * @public - */ - as (name) { - this.route.name = name - return this - } - - /** - * return json representation of the route - * - * @return {Object} - * - * @public - */ - toJSON () { - return this.route - } - -} - -module.exports = ResourceMember diff --git a/src/Route/domains.js b/src/Route/domains.js deleted file mode 100644 index f4fab5d5..00000000 --- a/src/Route/domains.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -let registeredDomains = [] - -let domains = exports = module.exports = {} - -/** - * pushes a new domain to registeredDomains - * - * @param {String} domain - * - * @private - */ -domains.add = function (domain) { - registeredDomains.push(domain) -} - -/** - * returns domains matching to a given - * host - * - * @param {String} host - * @return {Boolean} - * - * @private - */ -domains.match = function (host) { - let isDomain = false - registeredDomains.forEach(function (domain) { - if (domain.test(host)) { - isDomain = true - } - }) - return isDomain -} diff --git a/src/Route/group.js b/src/Route/group.js deleted file mode 100644 index 24bb8a17..00000000 --- a/src/Route/group.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const helpers = require('./helpers') -const domains = require('./domains') -const util = require('../../lib/util') - -/** - * Route groups to keep configuration DRY for bunch of - * routes. - * @class - * @alias Route.Group - */ -class Group { - - constructor (routes) { - this.routes = routes - } - - /** - * @see module:Route~middlewares - */ - middlewares () { - helpers.appendMiddleware( - this.routes, - util.spread.apply(this, arguments) - ) - return this - } - - /** - * @see module:Route~middleware - */ - middleware () { - return this.middlewares.apply(this, arguments) - } - - /** - * prefix group of routes with a given pattern - * - * @param {String} pattern - * - * @return {Object} - reference to this for chaining - * - * @example - * Route.group('...').prefix('/v1') - * - * @public - */ - prefix (pattern) { - helpers.prefixRoute(this.routes, pattern) - return this - } - - /** - * add domain to group of routes. All routes inside the group - * will be matched on define domain - * - * @param {String} domain - * @return {Object} - reference to this for chaining - * - * @example - * Route.group('...').domain(':user.example.com') - * - * @public - */ - domain (domain) { - domains.add(helpers.makeRoutePattern(domain)) - helpers.addDomain(this.routes, domain) - } - - /** - * @see module:Route~formats - */ - formats (formats, strict) { - helpers.addFormats(this.routes, formats, strict) - return this - } - -} - -module.exports = Group diff --git a/src/Route/helpers.js b/src/Route/helpers.js deleted file mode 100644 index e3ab4264..00000000 --- a/src/Route/helpers.js +++ /dev/null @@ -1,177 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const pathToRegexp = require('path-to-regexp') -const _ = require('lodash') - -let RouterHelper = exports = module.exports = {} - -/** - * construct a new route using path-to-regexp - * - * @param {String} route - * @param {String} verb - * @param {Any} handler - * - * @return {Object} - * @private - */ -RouterHelper.construct = function (route, verb, handler, group) { - route = route.startsWith('/') ? route : `/${route}` - const pattern = RouterHelper.makeRoutePattern(route) - const middlewares = [] - const domain = null - const name = route - - verb = _.isArray(verb) ? verb : [verb] // a route can register for multiple verbs - return {route, verb, handler, pattern, middlewares, name, group, domain} -} - -/** - * make regex pattern for a given route - * - * @param {String} route - * @return {Regex} - * - * @private - */ -RouterHelper.makeRoutePattern = function (route) { - return pathToRegexp(route, []) -} - -/** - * resolve route from routes store based upon current url - * - * @param {Object} routes - * @param {String} url - * @param {String} verb - * @return {Object} - * - * @private - */ -RouterHelper.returnMatchingRouteToUrl = function (routes, urlPath, verb) { - let maps = _.filter(routes, function (route) { - if (route.domain) { - route.pattern = RouterHelper.makeRoutePattern(route.domain + route.route) - } - return (route.pattern.test(urlPath) && _.includes(route.verb, verb)) - }) - maps = maps[0] || {} - if (maps.verb) { - maps.matchedVerb = verb - } // define which verb has been matched while resolving route - return maps -} - -/** - * return params passed to a given resolved route - * - * @param {Object} route - * @param {String} urlPath - * @return {Object} - * - * @private - */ -RouterHelper.returnRouteArguments = function (route, urlPath) { - const routeShallowCopy = _.clone(route) - let extracted = routeShallowCopy.pattern.exec(urlPath) - routeShallowCopy.params = {} - - _.map(routeShallowCopy.pattern.keys, function (key, index) { - routeShallowCopy.params[key.name] = extracted[index + 1] - }) - return routeShallowCopy -} - -/** - * return compiled url based on input route - * - * @param {String} route - * @param {Object} values - * @return {String} - * - * @private - */ -RouterHelper.compileRouteToUrl = function (route, values) { - return pathToRegexp.compile(route)(values) -} - -/** - * general purpose method to append new middlewares to - * a route or group of routes - * @method appendMiddleware - * @param {Array|Object} routes - * @param {Array} middlewares - * @return {void} - * @private - */ -RouterHelper.appendMiddleware = function (routes, middlewares) { - if (_.isArray(routes)) { - _.each(routes, function (route) { - route.middlewares = route.middlewares.concat(middlewares) - }) - } else { - routes.middlewares = routes.middlewares.concat(middlewares) - } -} - -/** - * adds formats to routes or an array of routes - * - * @param {Array|Object} routes - * @param {Boolean} strict - * @param {void} format - * @private - */ -RouterHelper.addFormats = function (routes, formats, strict) { - const flag = strict ? '' : '?' - const formatsPattern = `:format(.${formats.join('|.')})${flag}` - if (_.isArray(routes)) { - _.each(routes, function (route) { - route.route = `${route.route}${formatsPattern}` - route.pattern = RouterHelper.makeRoutePattern(route.route) - }) - } else { - routes.route = `${routes.route}${formatsPattern}` - routes.pattern = RouterHelper.makeRoutePattern(routes.route) - } -} - -/** - * general purpose method to prefix group of routes - * - * @param {Array} routes - * @param {String} prefix - * @return {void} - * - * @private - */ -RouterHelper.prefixRoute = function (routes, prefix) { - _.each(routes, function (route) { - route.route = route.route === '/' ? prefix : prefix + route.route - route.pattern = RouterHelper.makeRoutePattern(route.route) - return route - }) -} - -/** - * adds domain to group of routes. - * - * @param {Array} routes - * @param {String} domain - * - * @private - */ -RouterHelper.addDomain = function (routes, domain) { - _.each(routes, function (route) { - route.domain = domain - }) -} diff --git a/src/Route/index.js b/src/Route/index.js deleted file mode 100644 index f7abbd30..00000000 --- a/src/Route/index.js +++ /dev/null @@ -1,502 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const helpers = require('./helpers') -const Group = require('./group') -const Resource = require('./resource') -const domains = require('./domains') -const util = require('../../lib/util') -const _ = require('lodash') -const CatLog = require('cat-log') -const logger = new CatLog('adonis:framework') - -/** - * holding reference to registered routes - * @type {Array} - * @private - */ -let routes = [] - -/** - * holding reference to active Group - * @type {String} - * @private - */ -let activeGroup = null - -/** - * Create and register routes using regular expressions - * @module Route - */ -let Route = exports = module.exports = {} - -/** - * return all registered routes - * - * @method routes - * @return {Object} - * - * @public - */ -Route.routes = function () { - return routes -} - -/** - * clear registered routes and other local variables - * - * @method new - * - * @public - */ -Route.new = function () { - activeGroup = null - routes = [] -} - -/** - * a low level method to register route with path,verb - * and handler - * - * @method route - * - * @param {string} route - route expression - * @param {string} verb - http verb/method - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.route('/welcome', 'GET', function * () { - * - * }) - * - * @public - */ -Route.route = function (route, verb, handler) { - let constructedRoute = helpers.construct(route, verb, handler, activeGroup) - routes.push(constructedRoute) - return this -} - -/** - * register route with GET verb - * - * @method get - * - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.get('/user', function * () { - * - * }) - * - * @public - */ -Route.get = function (route, handler) { - this.route(route, ['GET', 'HEAD'], handler) - return this -} - -/** - * registers a get route with null handler - * which later can be used with render - * method to render a view. - * - * @method on - * - * @param {String} route - * @return {Object} - * - * @public - */ -Route.on = function (route) { - Route.get(route, null) - return this -} - -/** - * Replaces the route handler method with a custom - * closure, to send a given view. - * - * @method render - * - * @param {String} view - * @return {Object} - * - * @public - */ -Route.render = function (view) { - const route = Route._lastRoute() - route.handler = function * (request, response) { - yield response.sendView(view, {request}) - } - return this -} - -/** - * register route with POST verb - * - * @method post - * - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.post('/user', function * () { - * - * }) - * - * @public - */ -Route.post = function (route, handler) { - this.route(route, 'POST', handler) - return this -} - -/** - * register route with PUT verb - * - * @method put - * - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.put('/user/:id', function * () { - * - * }) - * - * @public - */ -Route.put = function (route, handler) { - this.route(route, 'PUT', handler) - return this -} - -/** - * register route with PATCH verb - * - * @method patch - * - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.patch('/user/:id', function * () { - * - * }) - * - * @public - */ -Route.patch = function (route, handler) { - this.route(route, 'PATCH', handler) - return this -} - -/** - * register route with DELETE verb - * - * @method delete - * - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.delete('/user/:id', function * () { - * - * }) - * - * @public - */ -Route.delete = function (route, handler) { - this.route(route, 'DELETE', handler) - return this -} - -/** - * register route with OPTIONS verb - * - * @method options - * - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.put('/user/:id', function * () { - * - * }) - * - * @public - */ -Route.options = function (route, handler) { - this.route(route, 'OPTIONS', handler) - return this -} - -/** - * registers a route with multiple HTTP verbs - * - * @method match - * - * @param {Array} verbs - an array of verbs - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.match(['GET', 'POST'], '/user', function * () { - * - * }) - * - * @public - */ -Route.match = function (verbs, route, handler) { - verbs = _.map(verbs, function (verb) { return verb.toUpperCase() }) - this.route(route, verbs, handler) - return this -} - -/** - * registers route for all http verbs - * - * @method any - * - * @param {String} route - route expression - * @param {any} handler - handler to respond to a given request - * - * @example - * Route.any('/user', function * () { - * - * }) - * - * @public - */ -Route.any = function (route, handler) { - const verbs = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] - this.route(route, verbs, handler) - return this -} - -/** - * giving unique name to a registered route - * - * @method as - * - * @param {String} name - name for recently registered route - * - * @example - * Route.get('/user/:id', '...').as('getUser') - * - * @public - */ -Route.as = function (name) { - let lastRoute = Route._lastRoute() - lastRoute.name = name - return this -} - -/** - * returns last route registered inside the route store - * - * @method lastRoute - * - * @return {Object} - * - * @private - */ -Route._lastRoute = function () { - return _.last(routes) -} - -/** - * assign array of named middlewares to route - * - * @method middleware - * @synonym middleware - * - * @param {Mixed} keys - an array of middleware or multiple parameters - * @return {Object} - reference to this for chaining - * - * @example - * Route.get('...').middleware('auth', 'csrf') - * Route.get('...').middleware(['auth', 'csrf']) - * - * @public - */ -Route.middleware = function () { - helpers.appendMiddleware( - Route._lastRoute(), - util.spread.apply(this, arguments) - ) - return this -} - -/** - * @see module:Route~middleware - */ -Route.middlewares = function () { - logger.warn('route@middlewares: consider using method middleware, instead of middlewares') - Route.middleware.apply(Route, arguments) -} - -/** - * create a new group of routes to apply rules on a group - * instead of applying them on every route. - * - * @method group - * - * @param {String} name - unqiue name for group - * @param {Function} cb - Callback to isolate group - * @returns {Route.Group} - Instance of route group - * - * @example - * Route.group('v1', function () { - * - * }).prefix('/v1').middleware('auth') - * @public - */ -Route.group = function (name, cb) { - activeGroup = name - cb() - const groupRoutes = _.filter(routes, function (route) { - return route.group === activeGroup - }) - activeGroup = null - return new Group(groupRoutes) -} - -/** - * resolves route for a given url and HTTP verb/method - * - * @method resolve - * - * @param {String} urlPath - Path to url - * @param {String} verb - Http verb - * @param {String} host - Current host - * - * @return {Object} - * - * @example - * Route.resolve('/user/1', 'GET', 'localhost') - * - * @public - */ -Route.resolve = function (urlPath, verb, host) { - if (domains.match(host)) { - urlPath = `${host}${urlPath}` - } - let resolvedRoute = helpers.returnMatchingRouteToUrl(routes, urlPath, verb) - if (_.size(resolvedRoute) === 0) { - return {} - } - return helpers.returnRouteArguments(resolvedRoute, urlPath, host) -} - -/** - * creates a resource of routes based out of conventions - * - * @method resource - * @alias resources - * - * @param {String} name - Resource name - * @param {String} controller - Controller to handle resource requests - * @returns {Route.resources} - Instance of Resources class - * - * @example - * Route.resource('user', 'UserController') - * Route.resource('post.comments', 'CommentsController') - * - * @public - */ -Route.resource = function (name, controller) { - return new Resource(Route, name, controller) -} -Route.resources = Route.resource - -/** - * creates a valid url based on route pattern and parameters and params - * - * @method url - * - * @param {String} pattern - * @param {Object} params - * @return {String} - * - * @example - * Route.url('http://github.com/user/:id',%20%7Bid:%201%7D) - * - * @public - */ -Route.url = function (pattern, params) { - const namedRoute = _.filter(routes, function (route) { - return route.name === pattern - })[0] - - /** - * if found pattern as a named route, make it using - * route properties - */ - if (namedRoute) { - const resolveRoute = namedRoute.domain ? `${namedRoute.domain}${namedRoute.route}` : namedRoute.route - return helpers.compileRouteToUrl(resolveRoute, params) - } - return helpers.compileRouteToUrl(pattern, params) -} - -/** - * returns a route with it's property - * - * @method getRoute - * @param {Object} property - * - * @example - * Route.getRoute({name: 'user.show'}) - * Route.getRoute({handler: 'UserController.show'}) - * - * @return {Object} - */ -Route.getRoute = function (property) { - const index = _.findIndex(routes, property) - return routes[index] -} - -/** - * removes a route from routes mapping using it's name - * - * @method remove - * - * @param {String} name - * - * @example - * Route.remove('user.create') - * - * @public - */ -Route.remove = function (name) { - const index = _.findIndex(routes, {name}) - routes.splice(index, 1) -} - -/** - * add formats paramters to route defination which makes - * url to have optional extensions at the end of them. - * - * @method formats - * - * @param {Array} formats - array of supported supports - * @param {Boolean} [strict=false] - Using strict mode will not register - * a plain route without any extension - * - * @example - * Route.get('/user', '...').formats(['json', 'xml']) - * - * @public - */ -Route.formats = function (formats, strict) { - const lastRoute = Route._lastRoute() - helpers.addFormats(lastRoute, formats, strict) -} diff --git a/src/Route/resource.js b/src/Route/resource.js deleted file mode 100644 index 0cb43fa9..00000000 --- a/src/Route/resource.js +++ /dev/null @@ -1,332 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2015 Harminder Virk - * MIT Licensed -*/ - -const _ = require('lodash') -const helpers = require('./helpers') -const CatLog = require('cat-log') -const logger = new CatLog('adonis:framework') -const util = require('../../lib/util') -const ResourceMember = require('./ResourceMember') -const ResourceCollection = require('./ResourceCollection') -const CE = require('../Exceptions') - -/** - * Resource management for Http routes. - * @class - * @alias Route.Resource - */ -class Resource { - - constructor (RouteHelper, pattern, handler) { - if (typeof (handler) !== 'string') { - throw CE.InvalidArgumentException.invalidParameter('You can only bind controllers to resources') - } - if (pattern === '/') { - logger.warn('You are registering a resource for / path, which is not a good practice') - } - this.RouteHelper = RouteHelper - this.pattern = this._makePattern(pattern) - this.handler = handler - this.routes = [] - this.basename = pattern.replace('/', '') - this._buildRoutes() - return this - } - - /** - * register a route to the routes store - * and pushes it to local array to reference it - * later - * - * @param {String} verb - * @param {String} route - * @param {String} handler - * @param {String} name - * - * @return {Object} - * - * @private - */ - _registerRoute (verb, route, handler, name) { - const resourceName = (this.basename === '/' || !this.basename) ? name : `${this.basename}.${name}` - this.RouteHelper.route(route, verb, `${handler}.${name}`).as(resourceName) - const registeredRoute = this.RouteHelper._lastRoute() - this.routes.push(registeredRoute) - return registeredRoute - } - - /** - * creates pattern for a given resource by removing - * {.} with nested route resources. - * - * @param {String} pattern [description] - * @return {String} [description] - * - * @example - * user.post.comment will return - * user/user_id/post/post_id/comment - * - * @private - */ - _makePattern (pattern) { - return pattern.replace(/(\w+)\./g, function (index, group) { - return `${group}/:${group}_id/` - }).replace(/\/$/, '') - } - - /** - * builds all routes for a given pattern - * - * @method _buildRoutes - * - * @return {void} - * - * @private - */ - _buildRoutes () { - this._registerRoute(['GET', 'HEAD'], this.pattern, this.handler, 'index') - this._registerRoute(['GET', 'HEAD'], `${this.pattern}/create`, this.handler, 'create') - this._registerRoute('POST', `${this.pattern}`, this.handler, 'store') - this._registerRoute(['GET', 'HEAD'], `${this.pattern}/:id`, this.handler, 'show') - this._registerRoute(['GET', 'HEAD'], `${this.pattern}/:id/edit`, this.handler, 'edit') - this._registerRoute(['PUT', 'PATCH'], `${this.pattern}/:id`, this.handler, 'update') - this._registerRoute('DELETE', `${this.pattern}/:id`, this.handler, 'destroy') - } - - /** - * transform methods keys to resource route names - * - * @method _transformKeys - * - * @param {Array} pairKeys - * @return {Array} - * - * @throws {Error} If pairKeys are not defines as array - * - * @private - */ - _transformKeys (pairKeys) { - if (!_.isArray(pairKeys)) { - throw CE.InvalidArgumentException.invalidParameter('Resource route methods must be defined as an array') - } - return pairKeys.map((item) => { - return `${this.basename}.${item}` - }) - } - - /** - * registers an expression of middleware to the specified - * actions - * - * @param {Object} expression - * - * @private - */ - _registerMiddlewareViaExpression (expression) { - _(expression) - .map((methods, middleware) => { - const routes = _.filter(this.routes, (route) => this._transformKeys(methods).indexOf(route.name) > -1) - return {routes, middleware} - }) - .each((item) => this._addMiddleware(item.routes, item.middleware)) - } - - /** - * adds an array of middleware to the given routes - * - * @param {Array} routes - * @param {Array} middleware - * - * @private - */ - _addMiddleware (routes, middleware) { - _.each(routes, (route) => { - helpers.appendMiddleware(route, middleware) - }) - } - - /** - * {@link module:Route~as} - */ - as (pairs) { - const pairKeys = _.keys(pairs) - const pairTransformedKeys = this._transformKeys(pairKeys) - _.each(this.routes, function (route) { - const pairIndex = pairTransformedKeys.indexOf(route.name) - if (pairIndex > -1) { - route.name = pairs[pairKeys[pairIndex]] - } - }) - return this - } - - /** - * removes all other actions from routes resources - * except the given array - * - * @param {Mixed} methods - An array of methods or multiple parameters defining - * methods - * @return {Object} - reference to resource instance for chaining - * - * @example - * Route.resource('...').only('create', 'store') - * Route.resource('...').only(['create', 'store']) - * - * @public - */ - only () { - const methods = util.spread.apply(this, arguments) - const transformedMethods = this._transformKeys(methods) - this.routes = _.filter(this.routes, (route) => { - if (transformedMethods.indexOf(route.name) <= -1) { - this.RouteHelper.remove(route.name) - } else { - return true - } - }) - return this - } - - /** - * filters resource by removing routes for defined actions - * - * @param {Mixed} methods - An array of methods or multiple parameters defining - * methods - * @return {Object} - reference to resource instance for chaining - * - * @example - * Route.resource('...').except('create', 'store') - * Route.resource('...').except(['create', 'store']) - * - * @public - */ - except () { - const methods = util.spread.apply(this, arguments) - const transformedMethods = this._transformKeys(methods) - this.routes = _.filter(this.routes, (route) => { - if (transformedMethods.indexOf(route.name) > -1) { - this.RouteHelper.remove(route.name) - } else { - return true - } - }) - return this - } - - /** - * See {@link module:Route~formats} - */ - formats (formats, strict) { - helpers.addFormats(this.routes, formats, strict) - return this - } - - /** - * add a member route to the resource - * - * @param {String} route - Route and action to be added to the resource - * @param {Mixed} [verbs=['GET', 'HEAD']] - An array of verbs - * @param {Function} [callback] - * - * @return {Object} - reference to resource instance for chaining - * - * @example - * Route.resource('...').addMember('completed') - * - * @public - */ - addMember (route, verbs, callback) { - if (_.isEmpty(route)) { - throw CE.InvalidArgumentException.invalidParameter('Resource.addMember expects a route') - } - - verbs = verbs || ['GET', 'HEAD'] - verbs = _.isArray(verbs) ? verbs : [verbs] - const registeredRoute = this._registerRoute(verbs, `${this.pattern}/:id/${route}`, this.handler, route) - if (typeof (callback) === 'function') { - callback(new ResourceMember(registeredRoute)) - } - return this - } - - /** - * add a collection route to the resource - * - * @param {String} route - Route and action to be added to the resource - * @param {Mixed} [verbs=['GET', 'HEAD']] - An array of verbs - * @param {Function} [callback] - * - * @return {Object} - reference to resource instance for chaining - * - * @example - * Route.resource('...').addCollection('completed') - * - * @public - */ - addCollection (route, verbs, callback) { - if (_.isEmpty(route)) { - throw CE.InvalidArgumentException.invalidParameter('Resource.addCollection expects a route') - } - - verbs = verbs || ['GET', 'HEAD'] - verbs = _.isArray(verbs) ? verbs : [verbs] - const registeredRoute = this._registerRoute(verbs, `${this.pattern}/${route}`, this.handler, route) - if (typeof (callback) === 'function') { - callback(new ResourceCollection(registeredRoute)) - } - return this - } - - /** - * @see this.middleware - */ - middlewares () { - logger.warn('resource@middlewares: consider using method middleware, instead of middlewares') - return this.middleware.apply(this, arguments) - } - - /** - * adds middleware to the resource - * - * @param {Mixed} middlewareExpression - * - * @return {Object} - * - * @example - * Route.resource(...).middleware('auth') - * Route.resource(...).middleware({ - * auth: ['store', 'update', 'delete'], - * web: ['index'] - * }) - * - * @public - */ - middleware (middlewareExpression) { - if (_.isObject(middlewareExpression) && !_.isArray(middlewareExpression)) { - this._registerMiddlewareViaExpression(middlewareExpression) - return this - } - this._addMiddleware(this.routes, util.spread.apply(this, arguments)) - return this - } - - /** - * returns routes JSON representation, helpful for - * inspection - * - * @return {Array} - * - * @public - */ - toJSON () { - return this.routes - } - -} - -module.exports = Resource diff --git a/src/Server/helpers.js b/src/Server/helpers.js deleted file mode 100644 index 61b3162d..00000000 --- a/src/Server/helpers.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -let helpers = exports = module.exports = {} - -/** - * returns an array of middleware concatenated with - * the actual route handler. - * - * @param {Object} resolvedRoute - * @param {Object} middleware - * @param {String} appNamespace - * @return {Array} - * - * @private - */ - -helpers.makeMiddlewareChain = function (middleware, finalHandler, isGlobal, resolvedRoute) { - if (isGlobal) { - return middleware.resolve([], true).concat([{instance: null, method: finalHandler}]) - } - const routeMiddleware = middleware.resolve(middleware.formatNamedMiddleware(resolvedRoute.middlewares), false) - return routeMiddleware.concat([finalHandler]) -} diff --git a/src/Server/index.js b/src/Server/index.js deleted file mode 100644 index 5cb9a83b..00000000 --- a/src/Server/index.js +++ /dev/null @@ -1,269 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const CatLog = require('cat-log') -const helpers = require('./helpers') -const http = require('http') -const co = require('co') -const Resolver = require('adonis-binding-resolver') -const Ioc = require('adonis-fold').Ioc -const resolver = new Resolver(Ioc) -const CE = require('../Exceptions') - -/** - * Http server for adonis framework - * @class - */ -class Server { - - constructor (Request, Response, Route, Helpers, Middleware, Static, Session, Config, Event) { - this.Request = Request - this.controllersPath = 'Http/Controllers' - this.Response = Response - this.Session = Session - this.route = Route - this.middleware = Middleware - this.static = Static - this.helpers = Helpers - this.config = Config - this.event = Event - this.log = new CatLog('adonis:framework') - this.httpInstance = null - } - - /** - * responds to a given http request by calling all global - * middleware and finally executing the route action. - * - * @param {Object} request - * @param {Object} response - * @param {Function} finalHandler - * - * @private - */ - _respond (request, response, finalHandler) { - try { - const chain = helpers.makeMiddlewareChain(this.middleware, finalHandler, true) - return this._executeChain(chain, request, response) - } catch (e) { - this._handleError(e, request, response) - } - } - - /** - * responds to request by finding registered - * route or throwing 404 error - * - * @param {Object} request - * @param {Object} response - * @throws {HttpException} If there is not registered route action - * - * @private - */ - _callRouteAction (resolvedRoute, request, response) { - if (!resolvedRoute.handler) { - throw new CE.HttpException(`Route not found ${request.url()}`, 404) - } - const routeAction = this._makeRouteAction(resolvedRoute.handler) - const chain = helpers.makeMiddlewareChain(this.middleware, routeAction, false, resolvedRoute) - return this._executeChain(chain, request, response) - } - - /** - * makes route action based upon the type of registered handler - * - * @param {Function|String} handler - * @return {Object} - * - * @throws {InvalidArgumentException} If a valid handler type is not found - * - * @private - */ - _makeRouteAction (handler) { - const formattedHandler = typeof (handler) === 'string' ? this.helpers.makeNameSpace(this.controllersPath, handler) : handler - resolver.validateBinding(formattedHandler) - return resolver.resolveBinding(formattedHandler) - } - - /** - * handles any errors thrown with in a given request - * and emit them using the Event provider. - * - * @param {Object} error - * @param {Object} request - * @param {Object} response - * - * @private - */ - _handleError (error, request, response) { - this._normalizeError(error) - if (this.event.wildcard() && this.event.hasListeners(['Http', 'error', '*'])) { - this.event.fire(['Http', 'error', error.status], error, request, response) - return - } - if (!this.event.wildcard() && this.event.hasListeners(['Http', 'error'])) { - this.event.fire(['Http', 'error'], error, request, response) - return - } - this.log.error(error.stack) - response.status(error.status).send(error.stack) - } - - /** - * normalize error object by setting required parameters - * if they does not exists - * - * @param {Object} error [description] - * - * @private - */ - _normalizeError (error) { - error.status = error.status || 500 - error.message = error.message || 'Internal server error' - error.stack = error.stack || error.message - } - - /** - * executes an array of actions by composing them - * using middleware provider. - * - * @param {Array} handlers - * @param {Object} request - * @param {Object} response - * - * @private - */ - _executeChain (chain, request, response) { - const middleware = this.middleware - return co(function * () { - yield middleware.compose(chain, request, response) - }).catch((e) => { - this._handleError(e, request, response) - }) - } - - /** - * serves static resource using static server - * - * @param {Object} request - * @param {Object} response - * @return {Promise} - * - * @private - */ - _staticHandler (request, response) { - return this.static.serve(request.request, request.response) - } - - /** - * returns request method by spoofing the _method only - * if allowed by the applicatin - * - * @param {Object} request - * @return {String} - * - * @private - */ - _getRequestMethod (request) { - if (!this.config.get('app.http.allowMethodSpoofing')) { - if (request.input('_method')) { - this.log.warn('You are making use of method spoofing but it\'s not enabled. Make sure to enable it inside config/app.js file.') - } - return request.method().toUpperCase() - } - return request.input('_method', request.method()).toUpperCase() - } - - /** - * request handler to respond to a given http request - * - * @param {Object} req - * @param {Object} res - * - * @example - * http - * .createServer(Server.handle.bind(Server)) - * .listen(3333) - * - * @public - */ - handle (req, res) { - const self = this - const request = new this.Request(req, res, this.config) - const response = new this.Response(request, res) - const session = new this.Session(req, res) - request.session = session - this.log.verbose('request on url %s ', request.originalUrl()) - - /** - * making request verb/method based upon _method or falling - * back to original method - * @type {String} - */ - const method = this._getRequestMethod(request) - const resolvedRoute = this.route.resolve(request.url(), method, request.hostname()) - request._params = resolvedRoute.params - - const finalHandler = function * () { - yield self._callRouteAction(resolvedRoute, request, response) - } - - /** - * do not serve static resources when request method is not - * GET or HEAD - */ - if (method !== 'GET' && method !== 'HEAD') { - this._respond(request, response, finalHandler) - return - } - - this._staticHandler(request, response) - .catch((e) => { - if (e.status === 404) { - this._respond(request, response, finalHandler) - return - } - this._handleError(e, request, response) - }) - } - - /** - * - * @returns {*} - * @public - */ - getInstance () { - if (!this.httpInstance) { - this.httpInstance = http.createServer(this.handle.bind(this)) - } - - return this.httpInstance - } - - /** - * starting a server on a given port and host - * - * @param {String} host - * @param {String} port - * - * @example - * Server.listen('localhost', 3333) - * - * @public - */ - listen (host, port) { - this.log.info('serving app on %s:%s', host, port) - this.getInstance().listen(port, host) - } - -} - -module.exports = Server diff --git a/src/Session/CookieManager.js b/src/Session/CookieManager.js deleted file mode 100644 index f1dae60b..00000000 --- a/src/Session/CookieManager.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const nodeCookie = require('node-cookie') - -/** - * Manages HTTP request cookies by using - * right options as defined in config. - * @class - */ -class CookieManager { - - /** - * @constructor - */ - constructor (Config) { - this.appSecret = Config.get('app.appKey') - this.shouldEncrypt = !!this.appSecret - this.options = { - domain: Config.get('session.domain'), - path: Config.get('session.path', '/'), - secure: Config.get('session.secure', false) - } - if (!Config.get('session.clearWithBrowser', false)) { - this.options.expires = new Date(Date.now() + (Config.get('session.age', 120) * 60 * 1000)) - } - } - - /** - * sets cookie on a given HTTP request. - * - * @param {Object} request - * @param {Object} response - * @param {String} cookieName - * @param {Mixed} value - * - * @return {Boolean} - */ - set (request, response, cookieName, value) { - return nodeCookie.create(request, response, cookieName, value, this.options, this.appSecret, this.shouldEncrypt) - } - - /** - * Returns value for a given cookie name from the - * HTTP request. - * - * @param {Object} request - * @param {String} cookieName - * - * @return {Mixed} - */ - read (request, cookieName) { - return nodeCookie.parse(request, this.appSecret, this.shouldEncrypt)[cookieName] - } - - /** - * Removes cookie from the HTTP request by setting - * the expiry date in past. - * - * @param {Object} request - * @param {Object} response - * @param {Stirng} cookieName - * - * @return {Boolean} - */ - remove (request, response, cookieName) { - return nodeCookie.clear(request, response, cookieName) - } - -} - -module.exports = CookieManager diff --git a/src/Session/Drivers/Cookie/index.js b/src/Session/Drivers/Cookie/index.js deleted file mode 100644 index 246652cf..00000000 --- a/src/Session/Drivers/Cookie/index.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const CookieManager = require('../../CookieManager') -const _ = require('lodash') - -/** - * Cookie driver for the session manager - * @class - * @alias SessionCookieDriver - */ -class Cookie { - - /** - * Injects ['Adonis/Src/Config'] - */ - static get inject () { - return ['Adonis/Src/Config'] - } - - /** - * @constructor - */ - constructor (Config) { - this.cookieName = `${Config.get('session.cookie', 'adonis-session')}-value` - this.cookieManager = new CookieManager(Config) - this.request = null - this.response = null - this.cookieJar = {} - } - - /** - * Returns values from the cookie only after validating - * the session id - * - * @param {String} sessionId - * @param {Object} sessionValues - * - * @return {Object} - * - * @private - */ - _validateAndGetValues (sessionId, sessionValues) { - if (_.get(sessionValues, 'sessionId') === sessionId && _.get(sessionValues, 'data')) { - return _.get(sessionValues, 'data') - } - return {} - } - - /** - * Returns the value cookies from the request headers - * - * @param {String} sessionId - * - * @return {Object} - * - * @private - */ - _fetchRequestCookie (sessionId) { - if (!_.size(this.cookieJar)) { - const sessionValues = this.cookieManager.read(this.request, this.cookieName) - this.cookieJar = this._validateAndGetValues(sessionId, sessionValues) - } - return this.cookieJar - } - - /** - * returns session info for a given session id - * - * @param {String} sessionId - * - * @return {Object} - */ - * read (sessionId) { - return this._fetchRequestCookie(sessionId) - } - - /** - * writes session values back to the cookie - * - * @param {String} sessionId - * @param {Object} values - * - * @return {Boolean} - */ - * write (sessionId, values) { - this.cookieJar = values - const sessionValues = {data: values, sessionId} - this.cookieManager.set(this.request, this.response, this.cookieName, sessionValues) - } - - /** - * clears the session values cookie - * - * @return {Boolean} - */ - * destroy () { - this.cookieManager.remove(this.request, this.response, this.cookieName) - } - - /** - * called by session class to pass on the request - * and response object. - * - * @param {Object} request - * @param {Object} response - */ - setRequest (request, response) { - this.request = request - this.response = response - } - -} - -module.exports = Cookie diff --git a/src/Session/Drivers/File/index.js b/src/Session/Drivers/File/index.js deleted file mode 100644 index 753905af..00000000 --- a/src/Session/Drivers/File/index.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const fs = require('co-fs-extra') - -/** - * File driver to session provider, to save sessions - * inside a file - * @class - * @alias SessionFileDriver - */ -class File { - - /** - * Injects ['Adonis/Src/Helpers', 'Adonis/Src/Config'] - */ - static get inject () { - return ['Adonis/Src/Helpers', 'Adonis/Src/Config'] - } - - /** - * @constructor - */ - constructor (Helpers, Config) { - const sessionDir = Config.get('session.file.directory') || 'sessions/' - this.sessionPath = Helpers.storagePath(sessionDir) - this.config = Config - } - - /** - * writes session data to disk - * - * @param {String} sessionId - * - * @param {String} data - * - * @example - * yield fileDriver.write(sessionId, values) - */ - * write (sessionId, data) { - const sessionFile = `${this.sessionPath}/${sessionId}` - yield fs.ensureFile(sessionFile) - yield fs.writeJson(sessionFile, data, {spaces: 2}) - } - - /** - * reads session value for a given - * sessionId - * - * @param {String} sessionId - * - * @return {Object} - * - * @example - * yield fileDriver.read(sessionId) - */ - * read (sessionId) { - try { - const sessionFile = `${this.sessionPath}/${sessionId}` - return yield fs.readJson(sessionFile) - } catch (e) { - return {} - } - } - - /** - * removes a session file - * - * @param {String} sessionId - * - * @return {void} - */ - * destroy (sessionId) { - const sessionFile = `${this.sessionPath}/${sessionId}` - return yield fs.remove(sessionFile) - } -} - -module.exports = File diff --git a/src/Session/Drivers/Redis/index.js b/src/Session/Drivers/Redis/index.js deleted file mode 100644 index 8f911020..00000000 --- a/src/Session/Drivers/Redis/index.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -/** - * Redis session driver to store sessions within - * redis. - * @class - * @alias SessionRedisDriver - */ -class Redis { - - /** - * Injects ['Adonis/Src/Helpers', 'Adonis/Src/Config'] - */ - static get inject () { - return ['Adonis/Src/Helpers', 'Adonis/Src/Config', 'Adonis/Addons/RedisFactory'] - } - - /** - * @constructor - */ - constructor (Helpers, Config, RedisFactory) { - const redisConfig = Config.get('session.redis') - this.ttl = Config.get('session.age') - this.redis = new RedisFactory(redisConfig, Helpers, false) // do not use cluster for sessions - } - - /** - * Reads values for a session id from redis. - * - * @param {String} sessionId - * - * @return {Object} - */ - * read (sessionId) { - try { - const sessionValues = yield this.redis.get(sessionId) - yield this.redis.expire(sessionId, this.ttl) // updating expiry after activity - return JSON.parse(sessionValues) - } catch (e) { - return {} - } - } - - /** - * Writes values for a session id to redis. - * - * @param {String} sessionId - * @param {Object} values - * - * @return {Boolean} - */ - * write (sessionId, values) { - const response = yield this.redis.set(sessionId, JSON.stringify(values)) - yield this.redis.expire(sessionId, this.ttl) - return !!response - } - - /** - * Destorys the record of a given sessionId - * - * @param {String} sessionId - * - * @return {Boolean} [description] - */ - * destroy (sessionId) { - const response = yield this.redis.del(sessionId) - return !!response - } - -} - -module.exports = Redis diff --git a/src/Session/Drivers/index.js b/src/Session/Drivers/index.js deleted file mode 100644 index f8655d43..00000000 --- a/src/Session/Drivers/index.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -module.exports = { - file: require('./File'), - cookie: require('./Cookie'), - redis: require('./Redis') -} diff --git a/src/Session/SessionManager.js b/src/Session/SessionManager.js deleted file mode 100644 index 306bb78a..00000000 --- a/src/Session/SessionManager.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const Drivers = require('./Drivers') -const Ioc = require('adonis-fold').Ioc -const Session = require('./index') -const CE = require('../Exceptions') - -/** - * Makes driver instance from native or extended driver. Executes - * the callback when unable to find the specified driver. - * - * @param {String} driver - * @param {Object} drivers - * @param {Object} extendedDrivers - * @param {Function} callback - * - * @return {Object} - * - * @private - */ -const _makeDriverInstance = (driver, drivers, extendedDrivers, callback) => { - const driverInstance = drivers[driver] ? Ioc.make(drivers[driver]) : extendedDrivers[driver] - if (!driverInstance) { - callback() - } - return driverInstance -} - -/** - * Session class for reading and writing sessions - * during http request - * @returns {Session} - * @class - */ -class SessionManager { - - /** - * Extend session provider by adding a new named - * driver. This method is used the IoC container, so - * feel free to use Ioc.extend syntax. - * - * @param {String} key - name of the driver - * @param {Object} value - Driver implmentation - * - * @example - * Ioc.extend('Adonis/Src/Session', 'redis', (app) => { - * return new RedisImplementation() - * }) - */ - static extend (key, value) { - this.drivers = this.drivers || {} - this.drivers[key] = value - } - - /** - * @constructor - */ - constructor (Config) { - const driver = Config.get('session.driver') - this.constructor.drivers = this.constructor.drivers || {} - - Session.driver = _makeDriverInstance(driver, Drivers, this.constructor.drivers, () => { - throw CE.RuntimeException.invalidSessionDriver(driver) - }) - Session.config = Config - return Session - } -} - -module.exports = SessionManager diff --git a/src/Session/Store.js b/src/Session/Store.js deleted file mode 100644 index 12900573..00000000 --- a/src/Session/Store.js +++ /dev/null @@ -1,145 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const Type = require('type-of-is') -const _ = require('lodash') - -/** - * Session values store to guard and unguard - * values. - * - * @exports SessionStore - */ -const Store = exports = module.exports = {} - -/** - * Guarding a key/value pair inside an object - * by storing the original data type. - * - * @param {String} key - * @param {Mixed} value - * - * @invalidTypes - Function, RegExp, Error - * - * @return {Object} - * - * @example - * Store.guardPair('name', 'Foo') => {name: {d: 'Foo', t: 'String'}} - */ -Store.guardPair = function (key, value) { - const type = Type.string(value) - switch (type) { - case 'Number': - value = String(value) - break - case 'Object': - value = JSON.stringify(value) - break - case 'Array': - value = JSON.stringify(value) - break - case 'Boolean': - value = String(value) - break - case 'Function': - value = null - break - case 'RegExp': - value = null - break - case 'Date': - value = String(value) - break - case 'Error': - value = null - break - } - - if (!value) { - return value - } - - return {d: value, t: type} -} - -/** - * Unguards a pair which was guarded earlier and - * returns the original value with correct - * data type. - * - * @param {Object} pair - * - * @return {Mixed} - * - * @example - * Store.unGuardPair({name: {d: 'Foo', t: 'String'}}) => {name: 'Foo'} - * - * @throws {InvalidArgumentException} If pair does not have d & t properties - */ -Store.unGuardPair = function (pair) { - if (!pair || !pair.d || !pair.t) { - throw new Error('Cannot unguard unrecognized pair type') - } - - /** if parsed do not re parse */ - if (typeof (pair.d) === pair.t.toLowerCase()) { - return pair.d - } - - switch (pair.t) { - case 'Number': - pair.d = Number(pair.d) - break - case 'Object': - try { pair.d = JSON.parse(pair.d) } catch (e) {} - break - case 'Array': - try { pair.d = JSON.parse(pair.d) } catch (e) {} - break - case 'Boolean': - pair.d = pair.d === 'true' || pair.d === '1' - break - } - - return pair.d -} - -/** - * Pack values from a plain object to an object to be - * saved as JSON.stringfied string - * - * @param {Object} values - * - * @return {Object} - */ -Store.packValues = function (values) { - return _.transform(values, (result, value, key) => { - const body = Store.guardPair(key, value) - if (body) { - result[key] = body - } - return result - }, {}) -} - -/** - * Unpack values from store to a normal object - * - * @param {Object} values - * - * @return {Object} - */ -Store.unPackValues = function (values) { - return _.transform(values, (result, value, index) => { - result[index] = Store.unGuardPair(value) - return result - }, {}) || {} -} diff --git a/src/Session/index.js b/src/Session/index.js deleted file mode 100644 index f5898bdf..00000000 --- a/src/Session/index.js +++ /dev/null @@ -1,297 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -/** - * @typedef {SessionDriver} - * @type {Class} - */ - -const _ = require('lodash') -const uuid = require('node-uuid') - -const Store = require('./Store') -const CookieManager = require('./CookieManager') -const CE = require('../Exceptions') -const util = require('../../lib/util') - -let sessionManagerDriver = null -let sessionManagerConfig = {} - -/** - * Session class to read/write values to the session - * store, using one of the available drivers. - * - * @class - */ -class Session { - - /** - * @constructor - */ - constructor (request, response) { - this.request = request - this.response = response - this._instantiate() - this._setDriverRequest() - } - - /** - * Returns the active driver instance. - * - * @return {SessionDriver} - */ - static get driver () { - return sessionManagerDriver - } - - /** - * Set session driver instance as a static property. - * This needs to be done before initiating the - * class so that session instance has a - * valid driver. - * - * @param {SessionDriver} driver - */ - static set driver (driver) { - sessionManagerDriver = driver - } - - /** - * Returns config provider in use. - * - * @return {Object} - */ - static get config () { - return sessionManagerConfig - } - - /** - * Sets config driver to use for reading - * session configuration. - * - * @param {Object} config - */ - static set config (config) { - sessionManagerConfig = config - } - - /** - * Sets current HTTP request and response on the active - * driver. Some drivers like Cookie driver needs current - * request refrence to read/write cookies. It can be - * left unimplemented for other driver.s - * - * @private - */ - _setDriverRequest () { - if (this.constructor.driver.setRequest) { - this.constructor.driver.setRequest(this.request, this.response) - } - } - - /** - * Instantiates the class properly by setting some - * options on the instance - * - * @private - */ - _instantiate () { - this.cookieManager = new CookieManager(this.constructor.config) - this.sessionCookieName = this.constructor.config.get('session.cookie', 'adonis-session') - this.sessionExpiryMs = this.constructor.config.get('session.age', 120) * 60 * 1000 - this.sessionId = null - this.sessionPayload = null - } - - /** - * Returns session id by reading it from the cookies. It - * session id is not set, it will create a new uid to - * be used as a session id. - * - * @return {String} - */ - getSessionId () { - if (this.sessionId) { - return this.sessionId - } - const sessionId = this.cookieManager.read(this.request, this.sessionCookieName) - this.sessionId = (!sessionId || typeof (sessionId) !== 'string') ? uuid.v1() : sessionId - return this.sessionId - } - - /** - * Writes session id on the request as a cookie. It will - * be encrypted if appKey is defined inside the config - * file. - * - * @param {String} sessionId - */ - setSessionId (sessionId) { - this.sessionId = sessionId - this.cookieManager.set(this.request, this.response, this.sessionCookieName, sessionId) - } - - /** - * Returns values for a given session id. Values returned - * from the driver are unpacked to be used to mutations. - * - * @param {String} sessionId - * - * @return {Object} - * - * @private - */ - * _getSessionValues (sessionId) { - if (this.sessionPayload) { - return this.sessionPayload - } - const sessionValues = yield this.constructor.driver.read(sessionId) - this.sessionPayload = Store.unPackValues(sessionValues) - return this.sessionPayload - } - - /** - * Writes values to the session driver by packing them - * into a safe object. - * - * @method _setSessionValues - * - * @param {String} sessionId - * @see Session.put - * - * @return {Boolean} - * - * @private - */ - * _setSessionValues (sessionId, key, value) { - const sessionValues = yield this._getSessionValues(sessionId) - if (_.isObject(key)) { - _.assign(sessionValues, key) - } else { - _.set(sessionValues, key, value) - } - this.sessionPayload = sessionValues - return yield this.constructor.driver.write(sessionId, Store.packValues(this.sessionPayload)) - } - - /** - * Returns a deep clone of session values. - * - * @return {Object} - * - * @example - * yield session.all() - */ - * all () { - const values = yield this._getSessionValues(this.getSessionId()) - return _.cloneDeep(values) - } - - /** - * Saves key/value pair inside the session store. - * - * @param {Mixed} key - Key can be a normal key/value pair key or - * it can be a self contained object - * @param {Mixed} [value] - Value to save next to key. Must not be passed - * when key itself is an object - * - * @return {Boolean} - * - * @example - * yield session.put('name', 'doe') - * yield session.put({name: 'doe'}) - * - * @throws {InvalidArgumentException} If parameters are not defined as intended - */ - * put (key, value) { - if (key && typeof (value) === 'undefined' && !_.isObject(key)) { - throw CE.InvalidArgumentException.invalidParameter('Session.put expects a key/value pair or an object of keys and values') - } - const sessionId = this.getSessionId() - this.setSessionId(sessionId) - return yield this._setSessionValues(sessionId, key, value) - } - - /** - * @see this.put - * @alias this.put - */ - set (key, value) { - return this.put(key, value) - } - - /** - * Removes value set next to a key from the session store. - * - * @param {String} key - * - * @return {Boolean} - * - * @example - * yield session.forget('name') - */ - * forget (key) { - return yield this.put(key, null) - } - - /** - * Get value for a given key from the session store. - * - * @param {String} key - * @param {Mixed} defaultValue - default value when actual value is - * undefined for null - * @return {Mixed} - * - * @example - * yield session.get('name') - * yield session.get('name', 'defaultName') - */ - * get (key, defaultValue) { - const sessionValues = yield this._getSessionValues(this.getSessionId()) - defaultValue = util.existy(defaultValue) ? defaultValue : null - const value = _.get(sessionValues, key) - return util.existy(value) ? value : defaultValue - } - - /** - * Combination of get and forget under single method - * - * @see this.get - * - * @return {Mixed} - * - * @example - * yield session.pull('name') - * yield session.pull('name', 'defaultValue') - * - * @public - */ - * pull (key, defaultValue) { - const value = yield this.get(key, defaultValue) - yield this.forget(key) - return value - } - - /** - * Flush the user session by dropping the cookie and notifying - * the driver to destroy values. - * - * @method flush - * - * @return {Boolean} - */ - * flush () { - yield this.constructor.driver.destroy(this.getSessionId()) - this.sessionPayload = null - this.sessionId = null - return this.cookieManager.remove(this.request, this.response, this.sessionCookieName) - } - -} - -module.exports = Session diff --git a/src/Static/index.js b/src/Static/index.js deleted file mode 100644 index 4b0945cc..00000000 --- a/src/Static/index.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const serveStatic = require('serve-static') - -/** - * serves static files for a given directory - * @class - */ -class Static { - - constructor (Helpers, Config) { - const publicPath = Helpers.publicPath() - const options = Config.get('app.static', {}) - options.fallthrough = false - this.server = serveStatic(publicPath, options) - } - - /** - * serves static file for a given request url - * - * @param {Object} request - * @param {Object} response - * @return {Promise} - * - * @example - * static - * .serve(req, res) - * .then() - * .catch() - * @public - */ - serve (request, response) { - return new Promise((resolve, reject) => { - this.server(request, response, (error) => { - if (!error) { - return resolve() - } - error.message = `Route ${error.message} while resolving ${request.url}` - reject(error) - }) - }) - } - -} - -module.exports = Static diff --git a/src/View/Form/index.js b/src/View/Form/index.js deleted file mode 100644 index 9ac9355a..00000000 --- a/src/View/Form/index.js +++ /dev/null @@ -1,602 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const _ = require('lodash') -const CE = require('../../Exceptions') - -/** - * Form helper for views - * @class - * @alias View.Form - */ -class Form { - - constructor (viewsEnv, Route) { - this.env = viewsEnv - this.specialKeywords = ['url', 'files', 'method', 'route', 'action', 'params'] - this.validFormMethods = ['GET', 'POST'] - this.route = Route - } - - /** - * returns method to be used for - * submitting forms - * - * @param {String} method - * @return {String} - * - * @private - */ - _getMethod (method) { - if (!method) { - return 'POST' - } - method = method.toUpperCase() - return this.validFormMethods.indexOf(method) > -1 ? method : 'POST' - } - - /** - * returns url to be set as form action - * - * @param {Object} options - * @return {String} - * - * @private - */ - _getUrl (options) { - let url = options.url - if (options.route) { - url = this.env.filters.route(options.route, options.params) - } - return url - } - - /** - * makes html attributes from an object - * - * @param {Object} attributes - * @param {Array} [avoid=[]] - * @return {Array} - * - * @private - */ - _makeHtmlAttributes (attributes, avoid) { - avoid = avoid || [] - const htmlAttributes = [] - _.each(attributes, (value, index) => { - if (avoid.indexOf(index) <= -1 && value !== null && value !== undefined) { - htmlAttributes.push(`${index}="${value}"`) - } - }) - return htmlAttributes - } - - /** - * returns enctype to be used for submitting form - * - * @param {Boolean} files - * @return {String} - * - * @private - */ - _getEncType (files) { - return files ? 'multipart/form-data' : 'application/x-www-form-urlencoded' - } - - /** - * adds query string for method spoofing if method is other - * than get and post - * - * @param {String} method - * @param {String} url - * @return {String} - * - * @private - */ - _makeMethodQueryString (method, url) { - if (this.validFormMethods.indexOf(method) > -1) { - return url - } - const symbol = url.indexOf('?') > -1 ? '&' : '?' - return `${url}${symbol}_method=${method.toUpperCase()}` - } - - /** - * make options attributes for the options tag - * - * @param {Mixed} value - * @param {Array} selected - * @return {String} - * - * @private - */ - _makeOptionsAttributes (value, selected) { - const attributes = { - selected: selected.indexOf(value) > -1 ? true : null, - value: value - } - return this._makeHtmlAttributes(attributes).join(' ') - } - - /** - * make options tag - * - * @param {Array|Object} options - * @param {Array|String} selected - * @return {Array} - * - * @private - */ - _makeOptions (options, selected) { - selected = _.isArray(selected) ? selected : [selected] - - if (_.isArray(options)) { - return _.map(options, (option) => { - return `` - }) - } - return _.map(options, (option, key) => { - return `` - }) - } - - /** - * open a form tag and sets it's action,method - * enctype and other attributes - * @param {Object} options - options to be used in order to create - * form tag - * @return {Object} - view specific object - * - * @example - * Form.open({url: '/user/:id', method: 'PUT', params: {id: 1}}) - * Form.open({route: 'user.update', method: 'PUT', params: {id: 1}}) - * Form.open({action: 'UserController.update', method: 'PUT', params: {id: 1}}) - * - * @public - */ - open (options) { - /** - * if user has defined route, fetch actual - * route defination using Route module - * and use the method. - */ - if (options.route) { - const route = this.route.getRoute({ name: options.route }) - - if (route === void 0) { - throw CE.RuntimeException.missingRoute(options.route) - } - - options.method = route.verb[0] - } - - /** - * if user has defined action, fetch actual - * route defination using Route module - * and use the method and route. - */ - if (options.action) { - const route = this.route.getRoute({handler: options.action}) - - if (route === void 0) { - throw CE.RuntimeException.missingRouteAction(options.action) - } - - options.method = route.verb[0] - options.route = route.route - } - - let url = this._getUrl(options) - const actualMethod = options.method || 'POST' - const method = this._getMethod(actualMethod) - const enctype = this._getEncType(options.files) - url = this._makeMethodQueryString(actualMethod, url) - let formAttributes = [] - - formAttributes.push(`method="${method}"`) - formAttributes.push(`action="http://github.com/$%7Burl%7D"`) - formAttributes.push(`enctype="${enctype}"`) - formAttributes = formAttributes.concat(this._makeHtmlAttributes(options, this.specialKeywords)) - return this.env.filters.safe(`
`) - } - - /** - * closes the form tag - * - * @method close - * - * @return {Object} - * - * @public - */ - close () { - return this.env.filters.safe('
') - } - - /** - * creates a label field - * - * @param {String} name - * @param {String} value - * @param {Object} [attributes={}] - * @return {Object} - * - * @example - * Form.label('email', 'Enter your email address') - * Form.label('email', 'Enter your email address', {class: 'bootstrap-class'}) - * - * @public - */ - label (name, value, attributes) { - attributes = attributes || {} - value = value || name - const labelAttributes = [`name="${name}"`].concat(this._makeHtmlAttributes(attributes)) - return this.env.filters.safe(``) - } - - /** - * creates an input field with defined type and attributes. - * Also it will use old values if flash middleware is - * enabled. - * - * @param {String} type - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * - * form.input('text', 'username', '', {class: 'input'}) - */ - input (type, name, value, attributes) { - attributes = attributes || {} - attributes.id = attributes.id || name - - if (!value && this.env.globals.old && !attributes.avoidOld) { - value = this.env.globals.old(name) - } - attributes.avoidOld = null - - if (type === 'textarea') { - const textareaAttributes = [`name="${name}"`].concat(this._makeHtmlAttributes(attributes)) - value = value || '' - return this.env.filters.safe(``) - } - - let inputAttributes = [`type="${type}"`, `name="${name}"`] - if (value) { - inputAttributes.push(`value="${value}"`) - } - inputAttributes = inputAttributes.concat(this._makeHtmlAttributes(attributes)) - return this.env.filters.safe(``) - } - - /** - * creates a text input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.text('username', '', {id: 'profile-username'}) - * - * @public - */ - text (name, value, attributes) { - return this.input('text', name, value, attributes) - } - - /** - * creates a submit input field with name, value - * and other input attributes - * @method submit - * @param {String} value - * @param {String} name - * @param {Object} attributes - * @return {Object} - * - * @example - * form.submit('Click Me', null, {class: 'button-primary'}) - * - * @public - */ - submit (value, name, attributes) { - name = name || 'submit' - return this.input('submit', name, value, attributes) - } - - /** - * creates a button element with name, value and other - * other button attributes - * @method button - * @param {String} value - * @param {String} [name=submit] - * @param {Object} [attributes={}] - * @return {Object} - * - * @example - * form.button('Submit') - * form.button('Create Account', 'create-account') - * form.button('Submit', null, {class: 'button--large'}) - * - * @public - */ - button (value, name, attributes) { - attributes = attributes || {} - let type = 'submit' - if (attributes.type) { - type = attributes.type - attributes.type = null - } - name = name || type - attributes.id = attributes.id || name - const buttonAttributes = [`type="${type}"`, `name="${name}"`, `value="${value}"`].concat(this._makeHtmlAttributes(attributes)) - return this.env.filters.safe(``) - } - - /** - * creates a button element as with reset type. - * @method resetButton - * @param {String} value - * @param {String} [name=submit] - * @param {Object} [attributes={}] - * @return {Object} - * - * @example - * form.button('Submit') - * form.button('Create Account', 'create-account') - * form.button('Submit', null, {class: 'button--large'}) - * - * @public - */ - resetButton (value, name, attributes) { - attributes = attributes || {} - attributes.type = 'reset' - return this.button(value, name, attributes) - } - - /** - * creates a password input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.password('password', '', {}) - * - * @public - */ - password (name, value, attributes) { - return this.input('password', name, value, attributes) - } - - /** - * creates an email input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.email('email', '', {}) - * - * @public - */ - email (name, value, attributes) { - return this.input('email', name, value, attributes) - } - /** - * creates a file input field - * @param {String} name - * @param {Object} attributes - * @return {Object} - * - * @example - * form.file('password', {}) - * - * @public - */ - file (name, attributes) { - return this.input('file', name, null, attributes) - } - - /** - * creates a color input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.color('theme-color', '#ffffff', {}) - * - * @public - */ - color (name, value, attributes) { - return this.input('color', name, value, attributes) - } - - /** - * creates a date input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.date('theme-color', '', {}) - * - * @public - */ - date (name, value, attributes) { - return this.input('date', name, value, attributes) - } - - /** - * creates a url input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.url('http://github.com/profile',%20'',%20%7B%7D) - * - * @public - */ - url (name, value, attributes) { - return this.input('url', name, value, attributes) - } - - /** - * creates a search input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.search('search', '', {}) - * - * @public - */ - search (name, value, attributes) { - return this.input('search', name, value, attributes) - } - - /** - * creates a hidden input field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.hidden('token', '', {}) - * - * @public - */ - hidden (name, value, attributes) { - return this.input('hidden', name, value, attributes) - } - - /** - * creates a textarea field - * @param {String} name - * @param {String} value - * @param {Object} attributes - * @return {Object} - * - * @example - * form.hidden('token', '', {}) - * - * @public - */ - textarea (name, value, attributes) { - return this.input('textarea', name, value, attributes) - } - - /** - * creates a radio input field - * @param {String} name - * @param {String} value - * @param {Boolean} checked - input to be checked or not - * @param {Object} attributes - * @return {Object} - * - * @example - * form.radio('gender', 'male', true, {}) - * form.radio('gender', 'female') - * - * @public - */ - radio (name, value, checked, attributes) { - attributes = attributes || {} - attributes.checked = checked ? 'checked' : null - return this.input('radio', name, value, attributes) - } - - /** - * creates a checkbox input field - * @param {String} name - * @param {String} value - * @param {Boolean} checked - input to be checked or not - * @param {Object} attributes - * @return {Object} - * - * @example - * form.checkbox('terms', 'agree') - * - * @public - */ - checkbox (name, value, checked, attributes) { - attributes = attributes || {} - attributes.checked = checked ? 'checked' : null - return this.input('checkbox', name, value, attributes) - } - - /** - * creates a new select box - * - * @param {String} name - * @param {Object|Array} options - * @param {String|Array} selected - * @param {String} emptyOption - * @param {Object} attributes - * @return {Object} - * - * @example - * - * form.select('country', ['India', 'Us', 'France']) - * form.select('country', {ind: 'India', us: 'Us', fr: 'France'}, ['ind', 'us']) - * form.select('country', ['India', 'Us', 'France'], null, 'Select Country') - * - * @public - */ - select (name, options, selected, emptyOption, attributes) { - attributes = attributes || {} - attributes.id = attributes.id || name - let selectAttributes = [`name="${name}"`] - selectAttributes = selectAttributes.concat(this._makeHtmlAttributes(attributes)) - let selectTag = `' - return this.env.filters.safe(selectTag) - } - - /** - * creates a select box with options in a defined range - * @param {String} name - * @param {Number} start - * @param {Number} end - * @param {Number|Array} selected - * @param {String} emptyOption - * @param {Object} attributes - * @return {Object} - * - * @example - * form.selectRange('shoesize', 4, 12, 7, 'Select shoe size') - * - * @public - */ - selectRange (name, start, end, selected, emptyOption, attributes) { - return this.select(name, _.range(start, end), selected, emptyOption, attributes) - } - -} - -module.exports = Form diff --git a/src/View/filters.js b/src/View/filters.js deleted file mode 100644 index e53bfc7f..00000000 --- a/src/View/filters.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const CE = require('../Exceptions') - -module.exports = function (env, Route) { - /** - * adds route filter and makes use of route to build - * dynamic routes out of the box - */ - env.addFilter('route', function (val, options) { - return Route.url(http://github.com/val,%20options) - }) - - /** - * returns a route from it's controller action - */ - env.addFilter('action', function (val, options) { - const route = Route.getRoute({handler: val}) - - if (route === void 0) { - throw CE.RuntimeException.missingRouteAction(val) - } - - return Route.url(http://github.com/route.route,%20options) - }) - - /** - * output input as json - */ - env.addFilter('json', function (val, identation) { - identation = identation || 4 - return JSON.stringify(val, null, identation) - }) -} diff --git a/src/View/globals.js b/src/View/globals.js deleted file mode 100644 index 2dff24d8..00000000 --- a/src/View/globals.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const Form = require('./Form') - -module.exports = function (env, Route) { - env.addGlobal('form', new Form(env, Route)) - - env.addGlobal('linkTo', function (route, text, options, target) { - const url = env.filters.route(route, options) - target = target ? `target="${target}"` : '' - return env.filters.safe(` ${text} `) - }) - - env.addGlobal('linkToAction', function (action, text, options, target) { - const url = env.filters.action(action, options) - target = target ? `target="${target}"` : '' - return env.filters.safe(` ${text} `) - }) -} diff --git a/src/View/index.js b/src/View/index.js deleted file mode 100644 index e562d222..00000000 --- a/src/View/index.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const nunjucks = require('nunjucks') -const ViewLoader = require('./loader') -const viewFilters = require('./filters') -const viewGlobals = require('./globals') - -/** - * View class for adonis framework to serve jinja like views - * @class - * @alias View - */ -class View { - - constructor (Helpers, Config, Route) { - const viewsPath = Helpers.viewsPath() - const viewsCache = Config.get('app.views.cache', true) - const injectServices = Config.get('app.views.injectServices', false) - this.viewsEnv = new nunjucks.Environment(new ViewLoader(viewsPath, false, !viewsCache)) - - /** - * only register use, make and yield when the end user - * has enabled injectServices inside the config file. - */ - if (injectServices) { - require('./services')(this.viewsEnv) - } - - viewGlobals(this.viewsEnv, Route) - viewFilters(this.viewsEnv, Route) - } - - /** - * compile a view with give template and data - * - * @param {String} template_path - * @param {Object} [data] - * @return {Promise} - * - * @example - * View - * .make('index', {}) - * .then() - * .catch() - * @public - */ - make (templatePath, data) { - let self = this - return new Promise(function (resolve, reject) { - self.viewsEnv.render(templatePath, data, function (err, templateContent) { - if (err) { - reject(err) - return - } - resolve(templateContent) - }) - }) - } - - /** - * makes a view from string instead of path, it is - * helpful for making quick templates on the - * fly. - * - * @param {String} templateString - * @param {Object} [data] - * @return {String} - * - * @example - * view.makeString('Hello {{ user }}', {user: 'doe'}) - * - * @public - */ - makeString (templateString, data) { - return this.viewsEnv.renderString(templateString, data) - } - - /** - * add a filter to view, it also support async execution - * - * @param {String} name - * @param {Function} callback - * @param {Boolean} async - * - * @example - * View.filter('name', function () { - * }, true) - * - * @public - */ - filter (name, callback, async) { - this.viewsEnv.addFilter(name, callback, async) - } - - /** - * add a global method to views - * - * @param {String} name - * @param {Mixed} value - * - * @example - * View.global('key', value) - * - * @public - */ - global (name, value) { - this.viewsEnv.addGlobal(name, value) - } -} - -module.exports = View diff --git a/src/View/loader.js b/src/View/loader.js deleted file mode 100644 index ec4c251c..00000000 --- a/src/View/loader.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const nunjucks = require('nunjucks') -const path = require('path') -const fs = require('fs') - -/** - * Views loader - * @module - * @alias Views.Loader - */ -exports = module.exports = nunjucks.Loader.extend({ - /** - * Initiates views loader - * - * @param {String} viewsPath - * @param {Boolean} noWatch Not considered - * @param {Boolean} noCache - * - * @public - */ - init: function (viewsPath, noWatch, noCache) { - this.viewsPath = path.normalize(viewsPath) - this.async = true - this.noCache = !!noCache - }, - - /** - * get content of a file required while rendering - * template - * - * @param {String} name - * @param {Function} callback - * - * @public - */ - getSource: function (name, callback) { - name = name.replace(/((?!\.+\/)\.(?!njk))/g, '/') - name = path.extname(name) === '.njk' ? name : `${name}.njk` - const viewPath = path.resolve(this.viewsPath, name) - const self = this - - fs.readFile(viewPath, function (err, content) { - if (err) { - callback(null, null) - return - } - callback(null, { - src: content.toString(), - path: viewPath, - noCache: self.noCache - }) - }) - } -}) diff --git a/src/View/services.js b/src/View/services.js deleted file mode 100644 index 09b91b2d..00000000 --- a/src/View/services.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const co = require('co') -const Ioc = require('adonis-fold').Ioc - -/** - * yield tag for views - * @class - */ -function ViewsYield () { - this.tags = ['yield'] - - /** - * nunjucks standard parser it looks for yield - * tag and returns everything inside it. - * - * @param {Object} parser - * @param {Function} nodes - * @param {Object} lexer - * @return {Object} - * - * @public - */ - this.parse = function (parser, nodes) { - var tok = parser.nextToken() - var args = parser.parseSignature(null, true) - parser.advanceAfterBlockEnd(tok.value) - return new nodes.CallExtensionAsync(this, 'run', args) - } - - /** - * nunjucks run function, it will run this Function - * everytime it finds an execution block with yield tag - * - * @param {Object} context - * @param {Object} injections - * @param {Function} callback - * - * @public - */ - this.run = function (context, injections, callback) { - var keys = Object.keys(injections) - var index = keys[0] - var method = injections[index] - - co(function * () { - return yield method - }) - .then(function (response) { - context.ctx[index] = response - callback() - }) - .catch(function (error) { - callback(error) - }) - } -} - -exports = module.exports = function (env) { - env.addExtension('yield', new ViewsYield()) - env.addGlobal('make', Ioc.make) - env.addGlobal('use', Ioc.use) -} diff --git a/src/cli_formatters/routes_list.ts b/src/cli_formatters/routes_list.ts new file mode 100644 index 00000000..4a0d680e --- /dev/null +++ b/src/cli_formatters/routes_list.ts @@ -0,0 +1,490 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import stringWidth from 'string-width' +import type { RouteJSON } from '../../types/http.js' +import type { UIPrimitives } from '../../types/ace.js' +import { cliHelpers } from '../../modules/ace/main.js' +import { type Router } from '../../modules/http/main.js' +import { parseBindingReference } from '../../src/helpers/main.js' + +/** + * Shape of the serialized route specific to the formatter + */ +type SerializedRoute = { + name: string + pattern: string + methods: string[] + middleware: string[] + handler: + | { type: 'closure'; name: string; args?: string } + | { type: 'controller'; moduleNameOrPath: string; method: string } +} + +/** + * Routes list formatter is used to format the routes to JSON or an ANSI string + * with pretty output. + * + * The decisions of colors, padding, alignment are all handled by the lists formatter + */ +export class RoutesListFormatter { + #router: Router + #colors: UIPrimitives['colors'] + #table: UIPrimitives['table'] + + /** + * Options for printing routes + */ + #options: { + displayHeadRoutes?: boolean + maxPrettyPrintWidth?: number + } + + /** + * Filters to apply when finding routes + */ + #filters: { + match?: string + middleware?: string[] + ignoreMiddleware?: string[] + } + + constructor( + router: Router, + ui: UIPrimitives, + options: { + displayHeadRoutes?: boolean + maxPrettyPrintWidth?: number + }, + filters: { + match?: string + middleware?: string[] + ignoreMiddleware?: string[] + } + ) { + this.#router = router + this.#colors = ui.colors + this.#table = ui.table + this.#filters = filters + this.#options = options + this.#router.commit() + } + + /** + * Test if a route clears the applied filters + */ + #isAllowedByFilters(route: SerializedRoute) { + let allowRoute = true + + /** + * Check if the route is allowed by applying the middleware + * filter + */ + if (this.#filters.middleware) { + allowRoute = this.#filters.middleware.every((name) => { + if (name === '*') { + return route.middleware.length > 0 + } + + return route.middleware.includes(name) + }) + } + + /** + * Check if the route has any or the ignored middleware. If yes, do not + * display the route + */ + if (allowRoute && this.#filters.ignoreMiddleware) { + allowRoute = this.#filters.ignoreMiddleware.every((name) => { + if (name === '*') { + return route.middleware.length === 0 + } + + return !route.middleware.includes(name) + }) + } + + /** + * No more filters to be applied + */ + if (!this.#filters.match) { + return allowRoute + } + + /** + * Check if the route name has the match keyword + */ + if (route.name.includes(this.#filters.match)) { + return true + } + + /** + * Check if the route pattern has the match keyword + */ + if (route.pattern.includes(this.#filters.match)) { + return true + } + + /** + * Check if the route handler has the match keyword + */ + if ( + route.handler.type === 'controller' + ? route.handler.moduleNameOrPath.includes(this.#filters.match) + : route.handler.name.includes(this.#filters.match) + ) { + return true + } + + /** + * Disallow route + */ + return false + } + + /** + * Serialize route middleware to an array of names + */ + #serializeMiddleware(middleware: RouteJSON['middleware']): string[] { + return [...middleware.all()].reduce((result, one) => { + if (typeof one === 'function') { + result.push(one.name || 'closure') + return result + } + + if ('name' in one && one.name) { + result.push(one.name) + } + + return result + }, []) + } + + /** + * Serialize route handler reference to display object + */ + async #serializeHandler(handler: RouteJSON['handler']): Promise { + /** + * Value is a controller reference + */ + if ('reference' in handler) { + return { + type: 'controller' as const, + ...(await parseBindingReference(handler.reference)), + } + } + + /** + * Value is an inline closure + */ + return { + type: 'closure' as const, + name: handler.name || 'closure', + args: 'listArgs' in handler ? String(handler.listArgs) : undefined, + } + } + + /** + * Serializes routes JSON to an object that can be used for pretty printing + */ + async #serializeRoute(route: RouteJSON): Promise { + let methods = route.methods + if (!this.#options.displayHeadRoutes) { + methods = methods.filter((method) => method !== 'HEAD') + } + + return { + name: route.name || '', + pattern: route.pattern, + methods: methods, + handler: await this.#serializeHandler(route.handler), + middleware: this.#serializeMiddleware(route.middleware), + } + } + + /** + * Formats the route method for the ansi list and table + */ + #formatRouteMethod(method: string) { + return this.#colors.dim(method) + } + + /** + * Formats route pattern for the ansi list and table + */ + #formatRoutePattern(route: SerializedRoute) { + const pattern = this.#router + .parsePattern(route.pattern) + .map((token) => { + if (token.type === 1) { + return this.#colors.yellow(`:${token.val}`) + } + + if (token.type === 3) { + return this.#colors.yellow(`:${token.val}?`) + } + + if (token.type === 2) { + return this.#colors.red(token.val) + } + + return token.val + }) + .join('/') + + return `${pattern === '/' ? pattern : `/${pattern}`}${ + route.name ? ` ${this.#colors.dim(`(${route.name})`)}` : '' + } ` + } + + /** + * Formats controller name for the ansi list and table + */ + #formatControllerName(route: SerializedRoute) { + return route.handler.type === 'controller' + ? ` ${this.#colors.cyan(route.handler.moduleNameOrPath)}.` + : '' + } + + /** + * Formats action name for the ansi list and table + */ + #formatAction(route: SerializedRoute) { + if (route.handler.type === 'controller') { + return `${this.#colors.cyan(route.handler.method)}` + } + + const functionName = ` ${this.#colors.cyan(route.handler.name)}` + if (route.handler.args) { + return ` ${functionName}${this.#colors.dim(`(${route.handler.args})`)}` + } + + return functionName + } + + /** + * Formats route middleware for the ansi list and table + */ + #formatMiddleware(route: SerializedRoute, mode: 'normal' | 'compact' = 'normal') { + if (mode === 'compact' && route.middleware.length > 3) { + const firstMiddleware = route.middleware[0] + const secondMiddleware = route.middleware[1] + const diff = route.middleware.length - 2 + return this.#colors.dim(`${firstMiddleware}, ${secondMiddleware}, and ${diff} more`) + } + + return this.#colors.dim(`${route.middleware.filter((one) => one).join(', ')}`) + } + + /** + * Formatting the domain headling to be in green color with + * dots around it + */ + #formatDomainHeadline(domain: string) { + if (domain !== 'root') { + return cliHelpers.justify([`${this.#colors.dim('..')} ${this.#colors.green(domain)} `], { + maxWidth: this.#options.maxPrettyPrintWidth || cliHelpers.TERMINAL_SIZE, + paddingChar: this.#colors.dim('.'), + })[0] + } + return '' + } + + /** + * Justify the ansi list + */ + #justifyListTables(tables: { heading: string; rows: [string, string, string, string][] }[]) { + return tables.map((table) => { + /** + * Formatting methods + */ + const methods = table.rows.map((columns) => columns[0]) + const largestMethodsLength = Math.max(...methods.map((method) => stringWidth(method))) + const formattedMethods = cliHelpers.justify(methods, { + maxWidth: largestMethodsLength, + }) + + /** + * Formatting patterns + */ + const patterns = table.rows.map((columns) => columns[1]) + const largestPatternLength = Math.max(...patterns.map((pattern) => stringWidth(pattern))) + const formattedPatterns = cliHelpers.justify(patterns, { + maxWidth: largestPatternLength, + paddingChar: this.#colors.dim('.'), + }) + + /** + * Formatting middleware to be right aligned + */ + const middleware = table.rows.map((columns) => columns[3]) + const largestMiddlewareLength = Math.max(...middleware.map((one) => stringWidth(one))) + const formattedMiddleware = cliHelpers.justify(middleware, { + maxWidth: largestMiddlewareLength, + align: 'right', + paddingChar: ' ', + }) + + /** + * Formatting controllers to be right aligned and take all the remaining + * space after printing route method, pattern and middleware. + */ + const controllers = table.rows.map((columns) => columns[2]) + const largestControllerLength = + (this.#options.maxPrettyPrintWidth || cliHelpers.TERMINAL_SIZE) - + (largestPatternLength + largestMethodsLength + largestMiddlewareLength) + + const formattedControllers = cliHelpers.truncate( + cliHelpers.justify(controllers, { + maxWidth: largestControllerLength, + align: 'right', + paddingChar: this.#colors.dim('.'), + }), + { + maxWidth: largestControllerLength, + } + ) + + return { + heading: table.heading, + rows: formattedMethods.reduce((result, method, index) => { + result.push( + `${method}${formattedPatterns[index]}${formattedControllers[index]}${formattedMiddleware[index]}` + ) + return result + }, []), + } + }) + } + + /** + * Formats routes as an array of objects. Routes are grouped by + * domain. + */ + async formatAsJSON() { + const routes = this.#router.toJSON() + const domains = Object.keys(routes) + let routesJSON: { domain: string; routes: SerializedRoute[] }[] = [] + + for (let domain of domains) { + const domainRoutes = await Promise.all( + routes[domain].map((route) => this.#serializeRoute(route)) + ) + + routesJSON.push({ + domain, + routes: domainRoutes.filter((route) => this.#isAllowedByFilters(route)), + }) + } + + return routesJSON + } + + /** + * Format routes to ansi list of tables. Each domain has its own table + * with heading and rows. Each row has colums with colors and spacing + * around them. + */ + async formatAsAnsiList() { + const routes = this.#router.toJSON() + const domains = Object.keys(routes) + const tables: { heading: string; rows: [string, string, string, string][] }[] = [] + + for (let domain of domains) { + const list: (typeof tables)[number] = { + heading: this.#formatDomainHeadline(domain), + rows: [ + [ + this.#colors.dim('METHOD'), + ` ${this.#colors.dim('ROUTE')} `, + ` ${this.#colors.dim('HANDLER')}`, + ` ${this.#colors.dim('MIDDLEWARE')}`, + ], + ], + } + + /** + * Computing table rows. Each route+method will have its + * own row + */ + for (let route of routes[domain]) { + const serializedRoute = await this.#serializeRoute(route) + if (this.#isAllowedByFilters(serializedRoute)) { + serializedRoute.methods.forEach((method) => { + list.rows.push([ + this.#formatRouteMethod(method), + ` ${this.#formatRoutePattern(serializedRoute)}`, + `${this.#formatControllerName(serializedRoute)}${this.#formatAction( + serializedRoute + )}`, + ` ${this.#formatMiddleware(serializedRoute, 'compact')}`, + ]) + }) + } + } + + tables.push(list) + } + + return this.#justifyListTables(tables) + } + + /** + * Format routes to ansi tables. Each domain has its own table + * with heading and rows. Each row has colums with colors and spacing + * around them. + */ + async formatAsAnsiTable() { + const routes = this.#router.toJSON() + const domains = Object.keys(routes) + const tables: { heading: string; table: ReturnType }[] = [] + + for (let domain of domains) { + const list: (typeof tables)[number] = { + heading: this.#formatDomainHeadline(domain), + table: this.#table() + .fullWidth() + .fluidColumnIndex(2) + .head([ + this.#colors.dim('METHOD'), + this.#colors.dim('ROUTE'), + { hAlign: 'right', content: this.#colors.dim('HANDLER') }, + { content: this.#colors.dim('MIDDLEWARE'), hAlign: 'right' }, + ]), + } + + /** + * Computing table rows. Each route+method will have its + * own row + */ + for (let route of routes[domain]) { + const serializedRoute = await this.#serializeRoute(route) + if (this.#isAllowedByFilters(serializedRoute)) { + serializedRoute.methods.forEach((method) => { + list.table.row([ + this.#formatRouteMethod(method), + this.#formatRoutePattern(serializedRoute), + { + content: `${this.#formatControllerName(serializedRoute)}${this.#formatAction( + serializedRoute + )}`, + hAlign: 'right', + }, + { content: this.#formatMiddleware(serializedRoute), hAlign: 'right' }, + ]) + }) + } + } + + tables.push(list) + } + + return tables + } +} diff --git a/src/config_provider.ts b/src/config_provider.ts new file mode 100644 index 00000000..07229594 --- /dev/null +++ b/src/config_provider.ts @@ -0,0 +1,31 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ApplicationService, ConfigProvider } from './types.js' + +/** + * Helper to create config provider and resolve config from + * them + */ +export const configProvider = { + create(resolver: ConfigProvider['resolver']): ConfigProvider { + return { + type: 'provider', + resolver, + } + }, + + async resolve(app: ApplicationService, provider: unknown): Promise { + if (provider && typeof provider === 'object' && 'type' in provider) { + return (provider as ConfigProvider).resolver(app) + } + + return null + }, +} diff --git a/src/debug.ts b/src/debug.ts new file mode 100644 index 00000000..8052c215 --- /dev/null +++ b/src/debug.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { debuglog } from 'node:util' + +export default debuglog('adonisjs:core') diff --git a/src/exceptions.ts b/src/exceptions.ts new file mode 100644 index 00000000..9a724e46 --- /dev/null +++ b/src/exceptions.ts @@ -0,0 +1,15 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { + Exception, + createError, + RuntimeException, + InvalidArgumentsException, +} from '@poppinss/utils' diff --git a/src/helpers/assert.ts b/src/helpers/assert.ts new file mode 100644 index 00000000..76c58ea9 --- /dev/null +++ b/src/helpers/assert.ts @@ -0,0 +1,15 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { + assertExists, + assertNotNull, + assertIsDefined, + assertUnreachable, +} from '@poppinss/utils/assert' diff --git a/src/helpers/is.ts b/src/helpers/is.ts new file mode 100644 index 00000000..37b1b2d4 --- /dev/null +++ b/src/helpers/is.ts @@ -0,0 +1,11 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import is from '@sindresorhus/is' +export default is diff --git a/src/helpers/main.ts b/src/helpers/main.ts new file mode 100644 index 00000000..1b8d453a --- /dev/null +++ b/src/helpers/main.ts @@ -0,0 +1,26 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { parseImports } from 'parse-imports' +export { createId as cuid, isCuid } from '@paralleldrive/cuid2' +export { + slash, + base64, + compose, + Secret, + joinToURL, + fsReadAll, + safeEqual, + getDirname, + getFilename, + fsImportAll, + MessageBuilder, +} from '@poppinss/utils' +export { VerificationToken } from './verification_token.js' +export { parseBindingReference } from './parse_binding_reference.js' diff --git a/src/helpers/parse_binding_reference.ts b/src/helpers/parse_binding_reference.ts new file mode 100644 index 00000000..b433b29a --- /dev/null +++ b/src/helpers/parse_binding_reference.ts @@ -0,0 +1,93 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { parseImports } from 'parse-imports' +import { LazyImport, Constructor } from '../../types/http.js' + +/** + * The "parseBindingReference" method can be used to parse a binding references + * similar to route controller binding value or event listener binding value. + * + * See the following examples to understand how this function works. + * + * ### Magic strings + * ```ts + * parseBindingReference('#controllers/home_controller') + * // returns { moduleNameOrPath: '#controllers/home_controller', method: 'handle' } + + * parseBindingReference('#controllers/home_controller.index') + * // returns { moduleNameOrPath: '#controllers/home_controller', method: 'index' } + + * parseBindingReference('#controllers/home.controller.index') + * // returns { moduleNameOrPath: '#controllers/home.controller', method: 'index' } + * ``` + * + * ### Class reference + * ```ts + * class HomeController {} + * + * parseBindingReference([HomeController]) + * // returns { moduleNameOrPath: 'HomeController', method: 'handle' } + + * parseBindingReference([HomeController, 'index']) + * // returns { moduleNameOrPath: 'HomeController', method: 'index' } + * ``` + * + * ### Lazy import reference + * ```ts + * const HomeController = () => import('#controllers/home_controller') + * + * parseBindingReference([HomeController]) + * // returns { moduleNameOrPath: '#controllers/home_controller', method: 'handle' } + + * parseBindingReference([HomeController, 'index']) + * // returns { moduleNameOrPath: 'controllers/home_controller', method: 'index' } + * ``` + */ +export async function parseBindingReference( + binding: string | [LazyImport> | Constructor, any?] +): Promise<{ moduleNameOrPath: string; method: string }> { + /** + * The binding reference is a magic string. It might not have method + * name attached to it. Therefore we split the string and attempt + * to find the method or use the default method name "handle". + */ + if (typeof binding === 'string') { + const tokens = binding.split('.') + if (tokens.length === 1) { + return { moduleNameOrPath: binding, method: 'handle' } + } + return { method: tokens.pop()!, moduleNameOrPath: tokens.join('.') } + } + + const [bindingReference, method] = binding + + /** + * Parsing the binding reference for dynamic imports and using its + * import value. + */ + const imports = [...(await parseImports(bindingReference.toString()))] + const importedModule = imports.find( + ($import) => $import.isDynamicImport && $import.moduleSpecifier.value + ) + if (importedModule) { + return { + moduleNameOrPath: importedModule.moduleSpecifier.value!, + method: method || 'handle', + } + } + + /** + * Otherwise using the name of the binding reference. + */ + return { + moduleNameOrPath: bindingReference.name, + method: method || 'handle', + } +} diff --git a/src/helpers/string.ts b/src/helpers/string.ts new file mode 100644 index 00000000..3ee23a6a --- /dev/null +++ b/src/helpers/string.ts @@ -0,0 +1,89 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import he, { EncodeOptions } from 'he' +import prettyHrTime from 'pretty-hrtime' +import string from '@poppinss/utils/string' +import StringBuilder from '@poppinss/utils/string_builder' + +/** + * Collection of string helpers to transform a string value. + */ +const stringHelpers: typeof string & { + /** + * Creates an instance of the string builder + */ + create(value: string | StringBuilder): StringBuilder + + ordinalize: (typeof string)['ordinal'] + + /** + * Convert a string to a sentence + */ + toSentence: (typeof string)['sentence'] + + /** + * Generate a random string value of a given length + */ + generateRandom: (typeof string)['random'] + + /** + * Pretty print hrtime diff + */ + prettyHrTime( + time: [number, number], + options?: { verbose?: boolean | undefined; precise?: boolean | undefined } + ): string + + /** + * Check if a string is empty. + */ + isEmpty(value: string): boolean + + /** + * Escape HTML entities + */ + escapeHTML(value: string, options?: { encodeSymbols?: boolean }): string + + /** + * Encode symbols to html entities + */ + encodeSymbols(value: string, options?: EncodeOptions): string +} = { + ...string, + toSentence: string.sentence, + ordinalize: string.ordinal, + generateRandom: string.random, + + create(value: string | StringBuilder): StringBuilder { + return new StringBuilder(value) + }, + + prettyHrTime(time, options) { + return prettyHrTime(time, options) + }, + + isEmpty(value: string): boolean { + return value.trim().length === 0 + }, + + escapeHTML(value: string, options?: { encodeSymbols?: boolean }): string { + value = he.escape(value) + if (options && options.encodeSymbols) { + value = this.encodeSymbols(value, { allowUnsafeSymbols: true }) + } + return value + }, + + encodeSymbols(value: string, options?: EncodeOptions): string { + return he.encode(value, options) + }, +} + +export default stringHelpers diff --git a/src/helpers/types.ts b/src/helpers/types.ts new file mode 100644 index 00000000..693475c1 --- /dev/null +++ b/src/helpers/types.ts @@ -0,0 +1,46 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import is from '@sindresorhus/is' + +/** + * @deprecated + * Use "is" helpers instead. The types helpers exists + * for backward compatibility + */ +const types = { + lookup: is, + isNull: is.null, + isBoolean: is.boolean, + isBuffer: is.buffer, + isNumber: is.number, + isString: is.string, + isArguments: is.arguments, + isObject: is.object, + isDate: is.date, + isArray: is.array, + isRegexp: is.regExp, + isError: is.error, + isFunction: is.function, + isClass: is.class, + isInteger: is.integer, + isFloat(value: number): value is number { + return value !== (value | 0) + }, + isDecimal(value: string | number, options?: { decimalPlaces?: string }): boolean { + if (typeof value === 'number') { + value = value.toString() + } + + const decimalPlaces = (options && options.decimalPlaces) || '1,' + return new RegExp(`^[-+]?([0-9]+)?(\\.[0-9]{${decimalPlaces}})$`).test(value) + }, +} + +export default types diff --git a/src/helpers/verification_token.ts b/src/helpers/verification_token.ts new file mode 100644 index 00000000..d3bea1a2 --- /dev/null +++ b/src/helpers/verification_token.ts @@ -0,0 +1,147 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createHash } from 'node:crypto' +import string from '@poppinss/utils/string' +import { base64, safeEqual, Secret } from '@poppinss/utils' + +/** + * Verification token class can be used to create tokens publicly + * shareable tokens while storing the token hash within the database. + * + * This class is used by the Auth and the Persona packages to manage + * tokens + */ +export abstract class VerificationToken { + /** + * Decodes a publicly shared token and return the series + * and the token value from it. + * + * Returns null when unable to decode the token because of + * invalid format or encoding. + */ + static decode(value: string): null | { identifier: string; secret: Secret } { + /** + * Ensure value is a string and starts with the prefix. + */ + if (typeof value !== 'string') { + return null + } + + /** + * Remove prefix from the rest of the token. + */ + if (!value) { + return null + } + + const [identifier, ...tokenValue] = value.split('.') + if (!identifier || tokenValue.length === 0) { + return null + } + + const decodedIdentifier = base64.urlDecode(identifier) + const decodedSecret = base64.urlDecode(tokenValue.join('.')) + if (!decodedIdentifier || !decodedSecret) { + return null + } + + return { + identifier: decodedIdentifier, + secret: new Secret(decodedSecret), + } + } + + /** + * Creates a transient token that can be shared with the persistence + * layer. + */ + static createTransientToken( + userId: string | number | BigInt, + size: number, + expiresIn: string | number + ) { + const expiresAt = new Date() + expiresAt.setSeconds(expiresAt.getSeconds() + string.seconds.parse(expiresIn)) + + return { + userId, + expiresAt, + ...this.seed(size), + } + } + + /** + * Creates a secret opaque token and its hash. + */ + static seed(size: number) { + const seed = string.random(size) + const secret = new Secret(seed) + const hash = createHash('sha256').update(secret.release()).digest('hex') + return { secret, hash } + } + + /** + * Identifer is a unique sequence to identify the + * token within database. It should be the + * primary/unique key + */ + declare identifier: string | number | BigInt + + /** + * Reference to the user id for whom the token + * is generated. + */ + declare tokenableId: string | number | BigInt + + /** + * Hash is computed from the seed to later verify the validity + * of seed + */ + declare hash: string + + /** + * Timestamp at which the token will expire + */ + declare expiresAt: Date + + /** + * The value is a public representation of a token. It is created + * by combining the "identifier"."secret" via the "computeValue" + * method + */ + declare value?: Secret + + /** + * Compute the value property using the given secret. You can + * get secret via the static "createTransientToken" method. + */ + protected computeValue(secret: Secret) { + this.value = new Secret( + `${base64.urlEncode(String(this.identifier))}.${base64.urlEncode(secret.release())}` + ) + } + + /** + * Check if the token has been expired. Verifies + * the "expiresAt" timestamp with the current + * date. + */ + isExpired() { + return this.expiresAt < new Date() + } + + /** + * Verifies the value of a token against the pre-defined hash + */ + verify(secret: Secret): boolean { + const newHash = createHash('sha256').update(secret.release()).digest('hex') + return safeEqual(this.hash, newHash) + } +} diff --git a/src/ignitor/ace.ts b/src/ignitor/ace.ts new file mode 100644 index 00000000..7edf58c8 --- /dev/null +++ b/src/ignitor/ace.ts @@ -0,0 +1,95 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Ignitor } from './main.js' +import type { ApplicationService } from '../types.js' + +/** + * The Ace process is used to start the application in the + * console environment. + */ +export class AceProcess { + /** + * Ignitor reference + */ + #ignitor: Ignitor + + /** + * The callback that configures the ace instance before the + * handle method is called + */ + #configureCallback: (app: ApplicationService) => Promise | void = () => {} + + constructor(ignitor: Ignitor) { + this.#ignitor = ignitor + } + + /** + * Register a callback that can be used to configure the ace + * kernel before the handle method is called + */ + configure(callback: (app: ApplicationService) => Promise | void): this { + this.#configureCallback = callback + return this + } + + /** + * Handles the command line arguments and executes + * the matching ace commands + */ + async handle(argv: string[]) { + const app = this.#ignitor.createApp('console') + await app.init() + + const { createAceKernel } = await import('../../modules/ace/create_kernel.js') + const commandNameIndex = argv.findIndex((value) => !value.startsWith('-')) + const commandName = argv[commandNameIndex] + + const kernel = createAceKernel(app, commandName) + app.container.bindValue('ace', kernel) + + /** + * Hook into kernel and start the app when the + * command needs the app. + * + * Since multiple commands can be executed in a single process, + * we add a check to only start the app only once. + */ + kernel.loading(async (metaData) => { + if (metaData.options.startApp && !app.isReady) { + if (metaData.commandName === 'repl') { + app.setEnvironment('repl') + } + await app.boot() + await app.start(() => {}) + } + }) + + await this.#configureCallback(app) + + /** + * Handle command line args + */ + await kernel.handle(argv) + + /** + * Terminate the app when the command does not want to + * hold a long running process + */ + const mainCommand = kernel.getMainCommand() + if (!mainCommand || !mainCommand.staysAlive) { + process.exitCode = kernel.exitCode + await app.terminate() + } else { + app.terminating(() => { + process.exitCode = mainCommand.exitCode + }) + } + } +} diff --git a/src/ignitor/http.ts b/src/ignitor/http.ts new file mode 100644 index 00000000..209016f1 --- /dev/null +++ b/src/ignitor/http.ts @@ -0,0 +1,172 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { Server as NodeHttpsServer } from 'node:https' +import { IncomingMessage, ServerResponse, Server as NodeHttpServer, createServer } from 'node:http' + +import debug from '../debug.js' +import { Ignitor } from './main.js' +import type { ApplicationService, EmitterService, LoggerService } from '../types.js' + +/** + * The HTTP server process is used to start the application in the + * web environment. + */ +export class HttpServerProcess { + /** + * Ignitor reference + */ + #ignitor: Ignitor + + constructor(ignitor: Ignitor) { + this.#ignitor = ignitor + } + + /** + * Calling this method closes the underlying HTTP server + */ + #close(nodeHttpServer: NodeHttpsServer | NodeHttpServer): Promise { + return new Promise((resolve) => { + debug('closing http server process') + nodeHttpServer.close(() => resolve()) + }) + } + + /** + * Monitors the app and the server to close the HTTP server when + * either one of them goes down + */ + #monitorAppAndServer( + nodeHttpServer: NodeHttpsServer | NodeHttpServer, + app: ApplicationService, + logger: LoggerService + ) { + /** + * Close the HTTP server when the application begins to + * terminate + */ + app.terminating(async () => { + debug('terminating signal received') + await this.#close(nodeHttpServer) + }) + + /** + * Terminate the app when the HTTP server crashes + */ + nodeHttpServer.once('error', (error: NodeJS.ErrnoException) => { + debug('http server crashed with error "%O"', error) + logger.fatal({ err: error }, error.message) + process.exitCode = 1 + app.terminate() + }) + } + + /** + * Starts the http server a given host and port + */ + #listen( + nodeHttpServer: NodeHttpsServer | NodeHttpServer + ): Promise<{ port: number; host: string }> { + return new Promise((resolve, reject) => { + const host = process.env.HOST || '0.0.0.0' + const port = Number(process.env.PORT || '3333') + + nodeHttpServer.listen(port, host) + nodeHttpServer.once('listening', () => { + debug('listening to http server, host :%s, port: %s', host, port) + resolve({ port, host }) + }) + + nodeHttpServer.once('error', (error: NodeJS.ErrnoException) => { + reject(error) + }) + }) + } + + /** + * Notifies the app and the parent process that the + * HTTP server is ready + */ + #notifyServerHasStarted( + app: ApplicationService, + logger: LoggerService, + emitter: EmitterService, + payload: { host: string; port: number; duration: [number, number] } + ) { + /** + * Notify parent process + */ + app.notify({ isAdonisJS: true, environment: 'web', ...payload }) + + /** + * Visual notification + */ + logger.info('started HTTP server on %s:%s', payload.host, payload.port) + + /** + * Notify app + */ + emitter.emit('http:server_ready', payload) + } + + /** + * Start the HTTP server by wiring up the application + */ + async start( + serverCallback?: ( + handler: (req: IncomingMessage, res: ServerResponse) => any + ) => NodeHttpsServer | NodeHttpServer + ) { + const startTime = process.hrtime() + + /** + * Method to create the HTTP server + */ + const createHTTPServer = serverCallback || createServer + const app = this.#ignitor.createApp('web') + + await app.init() + await app.boot() + await app.start(async () => { + /** + * Resolve and boot the AdonisJS HTTP server + */ + const server = await app.container.make('server') + await server.boot() + + /** + * Create Node.js HTTP server instance and share it with the + * AdonisJS HTTP server + */ + const httpServer = createHTTPServer(server.handle.bind(server)) + server.setNodeServer(httpServer) + + const logger = await app.container.make('logger') + const emitter = await app.container.make('emitter') + + /** + * Start the server by listening on a port of host + */ + const payload = await this.#listen(httpServer) + + /** + * Notify + */ + this.#notifyServerHasStarted(app, logger, emitter, { + ...payload, + duration: process.hrtime(startTime), + }) + + /** + * Monitor app and the server (after the server is listening) + */ + this.#monitorAppAndServer(httpServer, app, logger) + }) + } +} diff --git a/src/ignitor/main.ts b/src/ignitor/main.ts new file mode 100644 index 00000000..e594e791 --- /dev/null +++ b/src/ignitor/main.ts @@ -0,0 +1,118 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import debug from '../debug.js' +import { AceProcess } from './ace.js' +import { TestRunnerProcess } from './test.js' +import { HttpServerProcess } from './http.js' +import { setApp } from '../../services/app.js' +import { Application } from '../../modules/app.js' +import type { AppEnvironments } from '../../types/app.js' +import type { ApplicationService, IgnitorOptions } from '../types.js' + +/** + * Ignitor is used to instantiate an AdonisJS application in different + * known environments. + */ +export class Ignitor { + /** + * Ignitor options + */ + #options: IgnitorOptions + + /** + * Application root URL + */ + #appRoot: URL + + /** + * Reference to the application instance created using + * the "createApp" method. + * + * We store the output of the last call made to "createApp" method + * and assume that in one process only one entrypoint will + * call this method. + */ + #app?: ApplicationService + + /** + * Reference to the created application + */ + #tapCallbacks: Set<(app: ApplicationService) => void> = new Set() + + constructor(appRoot: URL, options: IgnitorOptions = {}) { + this.#appRoot = appRoot + this.#options = options + } + + /** + * Runs all the tap callbacks + */ + #runTapCallbacks(app: ApplicationService) { + this.#tapCallbacks.forEach((tapCallback) => tapCallback(app)) + } + + /** + * Get access to the application instance created + * by either the http server process or the ace + * process + */ + getApp() { + return this.#app + } + + /** + * Create an instance of AdonisJS application + */ + createApp(environment: AppEnvironments) { + debug('creating application instance') + this.#app = new Application(this.#appRoot, { environment, importer: this.#options.importer }) + + setApp(this.#app) + this.#runTapCallbacks(this.#app) + return this.#app + } + + /** + * Tap to access the application class instance. + */ + tap(callback: (app: ApplicationService) => void): this { + this.#tapCallbacks.add(callback) + return this + } + + /** + * Get instance of the HTTPServerProcess + */ + httpServer() { + return new HttpServerProcess(this) + } + + /** + * Get an instance of the AceProcess class + */ + ace() { + return new AceProcess(this) + } + + /** + * Get an instance of the TestRunnerProcess class + */ + testRunner() { + return new TestRunnerProcess(this) + } + + /** + * Terminates the app by calling the "app.terminate" + * method + */ + async terminate() { + await this.#app?.terminate() + } +} diff --git a/src/ignitor/test.ts b/src/ignitor/test.ts new file mode 100644 index 00000000..0eff24a3 --- /dev/null +++ b/src/ignitor/test.ts @@ -0,0 +1,51 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Ignitor } from './main.js' +import type { ApplicationService } from '../types.js' + +/** + * The Test runner process is used to start the tests runner process + */ +export class TestRunnerProcess { + /** + * Ignitor reference + */ + #ignitor: Ignitor + + /** + * The callback that configures the tests runner. This callback + * runs at the time of starting the app. + */ + #configureCallback: (app: ApplicationService) => Promise | void = () => {} + + constructor(ignitor: Ignitor) { + this.#ignitor = ignitor + } + + /** + * Register a callback that runs after booting the AdonisJS app + * and just before the provider's ready hook + */ + configure(callback: (app: ApplicationService) => Promise | void): this { + this.#configureCallback = callback + return this + } + + /** + * Runs a callback after starting the app + */ + async run(callback: (app: ApplicationService) => Promise | void) { + const app = this.#ignitor.createApp('test') + await app.init() + await app.boot() + await app.start(this.#configureCallback) + await callback(app) + } +} diff --git a/src/internal_helpers.ts b/src/internal_helpers.ts new file mode 100644 index 00000000..802f3bcc --- /dev/null +++ b/src/internal_helpers.ts @@ -0,0 +1,77 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { existsSync } from 'node:fs' +import { ApplicationService } from './types.js' +import { RcFile } from '@adonisjs/application/types' + +/** + * Imports assembler optionally + */ +export async function importAssembler( + app: ApplicationService +): Promise { + try { + return await app.import('@adonisjs/assembler') + } catch {} +} + +/** + * Imports typescript optionally + */ +export async function importTypeScript( + app: ApplicationService +): Promise { + try { + return await app.importDefault('typescript') + } catch {} +} + +/** + * Generates an array of filenames with different JavaScript + * extensions for the given filename + */ +function generateJsFilenames(filename: string) { + const extensions = ['.js', '.ts', '.cjs', '.mjs', '.cts', '.mts'] + return extensions.map((extension) => filename + extension) +} + +/** + * Detects the assets bundler in use. The rcFile.assetsBundler is + * used when exists. + */ +export async function detectAssetsBundler( + app: ApplicationService +): Promise { + if (app.rcFile.assetsBundler === false) { + return false + } + + if (app.rcFile.assetsBundler) { + return app.rcFile.assetsBundler + } + + const possibleViteConfigFiles = generateJsFilenames('vite.config') + if (possibleViteConfigFiles.some((config) => existsSync(app.makePath(config)))) { + return { + name: 'vite', + devServer: { command: 'vite' }, + build: { command: 'vite', args: ['build'] }, + } + } + + const possibleEncoreConfigFiles = generateJsFilenames('webpack.config') + if (possibleEncoreConfigFiles.some((config) => existsSync(app.makePath(config)))) { + return { + name: 'encore', + devServer: { command: 'encore', args: ['dev-server'] }, + build: { command: 'encore', args: ['production'] }, + } + } +} diff --git a/src/test_utils/http.ts b/src/test_utils/http.ts new file mode 100644 index 00000000..923d1942 --- /dev/null +++ b/src/test_utils/http.ts @@ -0,0 +1,79 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import debug from '../debug.js' +import type { TestUtils } from './main.js' +import type { Server as NodeHttpsServer } from 'node:https' +import { IncomingMessage, ServerResponse, Server as NodeHttpServer, createServer } from 'node:http' + +/** + * Http server utils are used to start the AdonisJS HTTP server + * during testing + */ +export class HttpServerUtils { + #utils: TestUtils + + constructor(utils: TestUtils) { + this.#utils = utils + } + + /** + * Starts the http server a given host and port + */ + #listen( + nodeHttpServer: NodeHttpsServer | NodeHttpServer + ): Promise<{ port: number; host: string }> { + return new Promise((resolve, reject) => { + const host = process.env.HOST || '0.0.0.0' + const port = Number(process.env.PORT || '3333') + + nodeHttpServer.listen(port, host) + nodeHttpServer.once('listening', () => { + debug('listening to utils http server, host :%s, port: %s', host, port) + resolve({ port, host }) + }) + + nodeHttpServer.once('error', (error: NodeJS.ErrnoException) => { + reject(error) + }) + }) + } + + /** + * Testing hook to start the HTTP server to listen for new request. + * The return value is a function to close the HTTP server. + */ + async start( + serverCallback?: ( + handler: (req: IncomingMessage, res: ServerResponse) => any + ) => NodeHttpsServer | NodeHttpServer + ): Promise<() => Promise> { + const createHTTPServer = serverCallback || createServer + + const server = await this.#utils.app.container.make('server') + await server.boot() + + const httpServer = createHTTPServer(server.handle.bind(server)) + server.setNodeServer(httpServer) + + await this.#listen(httpServer) + + return () => { + return new Promise((resolve, reject) => { + httpServer.close((error) => { + if (error) { + reject(error) + } else { + resolve() + } + }) + }) + } + } +} diff --git a/src/test_utils/main.ts b/src/test_utils/main.ts new file mode 100644 index 00000000..017efb50 --- /dev/null +++ b/src/test_utils/main.ts @@ -0,0 +1,71 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Socket } from 'node:net' +import Macroable from '@poppinss/macroable' +import { IncomingMessage, ServerResponse } from 'node:http' + +import { HttpServerUtils } from './http.js' +import type { ApplicationService } from '../types.js' +import { CookieClient, type HttpContext } from '../../modules/http/main.js' + +/** + * Test utils has a collection of helper methods to make testing + * experience great for AdonisJS applications + */ +export class TestUtils extends Macroable { + #booted: boolean = false + + /** + * Check if utils have been booted + */ + get isBooted() { + return this.#booted + } + + declare cookies: CookieClient + + constructor(public app: ApplicationService) { + super() + } + + /** + * Boot test utils. It requires the app to be booted + * and container to have all the bindings + */ + async boot() { + if (!this.isBooted) { + this.#booted = true + this.cookies = new CookieClient(await this.app.container.make('encryption')) + } + } + + /** + * Returns an instance of the HTTP server testing + * utils + */ + httpServer() { + return new HttpServerUtils(this) + } + + /** + * Create an instance of HTTP context for testing + */ + async createHttpContext( + options: { req?: IncomingMessage; res?: ServerResponse } = {} + ): Promise { + const req = options.req || new IncomingMessage(new Socket()) + const res = options.res || new ServerResponse(req) + const server = await this.app.container.make('server') + + const request = server.createRequest(req, res) + const response = server.createResponse(req, res) + return server.createHttpContext(request, response, this.app.container.createResolver()) + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..a08feb04 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,136 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { Repl } from '../modules/repl.js' +import type { Importer } from '../types/app.js' +import type { Emitter } from '../modules/events.js' +import type { Kernel } from '../modules/ace/main.js' +import type { Application } from '../modules/app.js' +import type { TestUtils } from './test_utils/main.js' +import type { HttpServerEvents } from '../types/http.js' +import type { Dumper } from '../modules/dumper/dumper.js' +import type { LoggerManager } from '../modules/logger.js' +import type { HashManager } from '../modules/hash/main.js' +import type { Encryption } from '../modules/encryption.js' +import type { ManagerDriverFactory } from '../types/hash.js' +import type { Router, Server } from '../modules/http/main.js' +import type { ContainerResolveEventData } from '../types/container.js' +import type { LoggerConfig, LoggerManagerConfig } from '../types/logger.js' + +/** + * A config provider waits for the application to get booted + * and then resolves the config. It receives an instance + * of the application service. + */ +export type ConfigProvider = { + type: 'provider' + resolver: (app: ApplicationService) => Promise +} + +/** + * Options accepted by ignitor + */ +export type IgnitorOptions = { importer?: Importer } + +/** + * A list of known events. The interface must be extended in + * user land code or packages to register events and their + * types. + */ +export interface EventsList extends HttpServerEvents { + 'container_binding:resolved': ContainerResolveEventData + 'http:server_ready': { port: number; host: string; duration: [number, number] } +} + +/** + * The loggers list inferred from the user application + * config + */ +export interface LoggersList {} +export type InferLoggers> = T['loggers'] + +/** + * A list of known hashers inferred from the user config + */ +export interface HashersList {} +export type InferHashers }>> = + Awaited>['list'] + +/** + * ---------------------------------------------------------------- + * Container services + * ----------------------------------------------------------------- + * + * Types for the container singleton services. Defining them + * upfront so that we do not have to define them in + * multiple places. + */ + +/** + * Application service is a singleton resolved from + * the container + */ +export interface ApplicationService + extends Application ? ContainerBindings : never> {} + +/** + * Logger service is a singleton logger instance registered + * to the container. + */ +export interface LoggerService + extends LoggerManager ? LoggersList : never> {} + +/** + * Emitter service is a singleton emitter instance registered + * to the container. + */ +export interface EmitterService extends Emitter {} + +/** + * Encryption service is a singleton Encryption class instance + * registered to the container. + */ +export interface EncryptionService extends Encryption {} + +/** + * Http server service added to the container as a singleton + */ +export interface HttpServerService extends Server {} + +/** + * Http server service added to the container as a singleton + */ +export interface HttpRouterService extends Router {} + +/** + * Hash service is a singleton instance of the HashManager + * registered in the container + */ +export interface HashService + extends HashManager< + HashersList extends Record ? HashersList : never + > {} + +/** + * A list of known container bindings. + */ +export interface ContainerBindings { + ace: Kernel + dumper: Dumper + app: ApplicationService + logger: LoggerService + config: ApplicationService['config'] + emitter: EmitterService + encryption: EncryptionService + hash: HashService + server: HttpServerService + router: HttpRouterService + testUtils: TestUtils + repl: Repl +} diff --git a/src/vine.ts b/src/vine.ts new file mode 100644 index 00000000..fecf5062 --- /dev/null +++ b/src/vine.ts @@ -0,0 +1,105 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import vine, { symbols, BaseLiteralType } from '@vinejs/vine' +import type { Validation, FieldContext, FieldOptions } from '@vinejs/vine/types' +import type { MultipartFile, FileValidationOptions } from '@adonisjs/bodyparser/types' + +const MULTIPART_FILE: typeof symbols.SUBTYPE = symbols.SUBTYPE ?? Symbol.for('subtype') + +/** + * Validation options accepted by the "file" rule + */ +export type FileRuleValidationOptions = + | Partial + | ((field: FieldContext) => Partial) + +/** + * Checks if the value is an instance of multipart file + * from bodyparser. + */ +function isBodyParserFile(file: unknown): file is MultipartFile { + return !!(file && typeof file === 'object' && 'isMultipartFile' in file) +} + +/** + * VineJS validation rule that validates the file to be an + * instance of BodyParser MultipartFile class. + */ +const isMultipartFile = vine.createRule((file, options, field) => { + /** + * Report error when value is not a field multipart + * file object + */ + if (!isBodyParserFile(file)) { + field.report('The {{ field }} must be a file', 'file', field) + return + } + + const validationOptions = typeof options === 'function' ? options(field) : options + + /** + * Set size when it's defined in the options and missing + * on the file instance + */ + if (file.sizeLimit === undefined && validationOptions.size) { + file.sizeLimit = validationOptions.size + } + + /** + * Set extensions when it's defined in the options and missing + * on the file instance + */ + if (file.allowedExtensions === undefined && validationOptions.extnames) { + file.allowedExtensions = validationOptions.extnames + } + + /** + * Validate file + */ + file.validate() + + /** + * Report errors + */ + file.errors.forEach((error) => { + field.report(error.message, `file.${error.type}`, field, validationOptions) + }) +}) + +/** + * Represents a multipart file uploaded via multipart/form-data HTTP + * request. + */ +export class VineMultipartFile extends BaseLiteralType< + MultipartFile, + MultipartFile, + MultipartFile +> { + #validationOptions?: FileRuleValidationOptions; + + [MULTIPART_FILE] = 'multipartFile' + + constructor( + validationOptions?: FileRuleValidationOptions, + options?: FieldOptions, + validations?: Validation[] + ) { + super(options, validations || [isMultipartFile(validationOptions || {})]) + this.#validationOptions = validationOptions + } + + clone() { + return new VineMultipartFile( + this.#validationOptions, + this.cloneOptions(), + this.cloneValidations() + ) as this + } +} diff --git a/stubs/main.ts b/stubs/main.ts new file mode 100644 index 00000000..e9510d75 --- /dev/null +++ b/stubs/main.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { getDirname } from '@poppinss/utils' + +export const stubsRoot = getDirname(import.meta.url) diff --git a/stubs/make/command/main.stub b/stubs/make/command/main.stub new file mode 100644 index 00000000..56e9557b --- /dev/null +++ b/stubs/make/command/main.stub @@ -0,0 +1,21 @@ +{{#var commandName = generators.commandName(entity.name)}} +{{#var commandTerminalName = generators.commandTerminalName(entity.name)}} +{{#var commandFileName = generators.commandFileName(entity.name)}} +{{{ + exports({ + to: app.commandsPath(entity.path, commandFileName) + }) +}}} +import { BaseCommand } from '@adonisjs/core/ace' +import type { CommandOptions } from '@adonisjs/core/types/ace' + +export default class {{ commandName }} extends BaseCommand { + static commandName = '{{ commandTerminalName }}' + static description = '' + + static options: CommandOptions = {} + + async run() { + this.logger.info('Hello world from "{{ commandName }}"') + } +} diff --git a/stubs/make/controller/actions.stub b/stubs/make/controller/actions.stub new file mode 100644 index 00000000..307f5a41 --- /dev/null +++ b/stubs/make/controller/actions.stub @@ -0,0 +1,14 @@ +{{#var controllerName = generators.controllerName(entity.name, singular)}} +{{#var controllerFileName = generators.controllerFileName(entity.name, singular)}} +{{{ + exports({ + to: app.httpControllersPath(entity.path, controllerFileName) + }) +}}} +import type { HttpContext } from '@adonisjs/core/http' + +export default class {{ controllerName }} { + {{#each actions as action}} + async {{action}}({}: HttpContext) {} + {{/each}} +} diff --git a/stubs/make/controller/api.stub b/stubs/make/controller/api.stub new file mode 100644 index 00000000..d107abff --- /dev/null +++ b/stubs/make/controller/api.stub @@ -0,0 +1,35 @@ +{{#var controllerName = generators.controllerName(entity.name, singular)}} +{{#var controllerFileName = generators.controllerFileName(entity.name, singular)}} +{{{ + exports({ + to: app.httpControllersPath(entity.path, controllerFileName) + }) +}}} +import type { HttpContext } from '@adonisjs/core/http' + +export default class {{ controllerName }} { + /** + * Display a list of resource + */ + async index({}: HttpContext) {} + + /** + * Handle form submission for the create action + */ + async store({ request }: HttpContext) {} + + /** + * Show individual record + */ + async show({ params }: HttpContext) {} + + /** + * Handle form submission for the edit action + */ + async update({ params, request }: HttpContext) {} + + /** + * Delete record + */ + async destroy({ params }: HttpContext) {} +} diff --git a/stubs/make/controller/main.stub b/stubs/make/controller/main.stub new file mode 100644 index 00000000..72a72328 --- /dev/null +++ b/stubs/make/controller/main.stub @@ -0,0 +1,11 @@ +{{#var controllerName = generators.controllerName(entity.name, singular)}} +{{#var controllerFileName = generators.controllerFileName(entity.name, singular)}} +{{{ + exports({ + to: app.httpControllersPath(entity.path, controllerFileName) + }) +}}} +// import type { HttpContext } from '@adonisjs/core/http' + +export default class {{ controllerName }} { +} diff --git a/stubs/make/controller/resource.stub b/stubs/make/controller/resource.stub new file mode 100644 index 00000000..0a2e3c88 --- /dev/null +++ b/stubs/make/controller/resource.stub @@ -0,0 +1,45 @@ +{{#var controllerName = generators.controllerName(entity.name, singular)}} +{{#var controllerFileName = generators.controllerFileName(entity.name, singular)}} +{{{ + exports({ + to: app.httpControllersPath(entity.path, controllerFileName) + }) +}}} +import type { HttpContext } from '@adonisjs/core/http' + +export default class {{ controllerName }} { + /** + * Display a list of resource + */ + async index({}: HttpContext) {} + + /** + * Display form to create a new record + */ + async create({}: HttpContext) {} + + /** + * Handle form submission for the create action + */ + async store({ request }: HttpContext) {} + + /** + * Show individual record + */ + async show({ params }: HttpContext) {} + + /** + * Edit individual record + */ + async edit({ params }: HttpContext) {} + + /** + * Handle form submission for the edit action + */ + async update({ params, request }: HttpContext) {} + + /** + * Delete record + */ + async destroy({ params }: HttpContext) {} +} diff --git a/stubs/make/event/main.stub b/stubs/make/event/main.stub new file mode 100644 index 00000000..dda7cd6f --- /dev/null +++ b/stubs/make/event/main.stub @@ -0,0 +1,17 @@ +{{#var eventName = generators.eventName(entity.name)}} +{{#var eventFileName = generators.eventFileName(entity.name)}} +{{{ + exports({ + to: app.eventsPath(entity.path, eventFileName) + }) +}}} +import { BaseEvent } from '@adonisjs/core/events' + +export default class {{ eventName }} extends BaseEvent { + /** + * Accept event data as constructor parameters + */ + constructor() { + super() + } +} diff --git a/stubs/make/exception/main.stub b/stubs/make/exception/main.stub new file mode 100644 index 00000000..9f8efc0f --- /dev/null +++ b/stubs/make/exception/main.stub @@ -0,0 +1,12 @@ +{{#var exceptionName = generators.exceptionName(entity.name)}} +{{#var exceptionFileName = generators.exceptionFileName(entity.name)}} +{{{ + exports({ + to: app.exceptionsPath(entity.path, exceptionFileName) + }) +}}} +import { Exception } from '@adonisjs/core/exceptions' + +export default class {{ exceptionName }} extends Exception { + static status = 500 +} diff --git a/stubs/make/health/controller.stub b/stubs/make/health/controller.stub new file mode 100644 index 00000000..0061088f --- /dev/null +++ b/stubs/make/health/controller.stub @@ -0,0 +1,21 @@ +{{#var controllerName = generators.controllerName(entity.name, false)}} +{{#var controllerFileName = generators.controllerFileName(entity.name, false)}} +{{{ + exports({ + to: app.httpControllersPath(entity.path, controllerFileName) + }) +}}} +import { healthChecks } from '#start/health' +import type { HttpContext } from '@adonisjs/core/http' + +export default class {{ controllerName }} { + async handle({ response }: HttpContext) { + const report = await healthChecks.run() + + if (report.isHealthy) { + return response.ok(report) + } + + return response.serviceUnavailable(report) + } +} diff --git a/stubs/make/health/main.stub b/stubs/make/health/main.stub new file mode 100644 index 00000000..40284c59 --- /dev/null +++ b/stubs/make/health/main.stub @@ -0,0 +1,12 @@ +{{#var preloadFileName = string(entity.name).snakeCase().removeExtension().ext('.ts').toString()}} +{{{ + exports({ + to: app.startPath(entity.path, preloadFileName) + }) +}}} +import { HealthChecks, DiskSpaceCheck, MemoryHeapCheck } from '@adonisjs/core/health' + +export const healthChecks = new HealthChecks().register([ + new DiskSpaceCheck(), + new MemoryHeapCheck(), +]) diff --git a/stubs/make/listener/for_event.stub b/stubs/make/listener/for_event.stub new file mode 100644 index 00000000..51d17b14 --- /dev/null +++ b/stubs/make/listener/for_event.stub @@ -0,0 +1,15 @@ +{{#var listenerName = generators.listenerName(entity.name)}} +{{#var listenerFileName = generators.listenerFileName(entity.name)}} +{{#var eventName = generators.eventName(event.name)}} +{{#var eventFileName = generators.eventFileName(event.name)}} +{{#var eventImportPath = generators.importPath('#events', event.path, eventFileName)}} +{{{ + exports({ + to: app.listenersPath(entity.path, listenerFileName) + }) +}}} +import type {{ eventName }} from '{{ eventImportPath }}' + +export default class {{ listenerName }} { + async handle(event: {{ eventName }}) {} +} diff --git a/stubs/make/listener/main.stub b/stubs/make/listener/main.stub new file mode 100644 index 00000000..77a49ada --- /dev/null +++ b/stubs/make/listener/main.stub @@ -0,0 +1,9 @@ +{{#var listenerName = generators.listenerName(entity.name)}} +{{#var listenerFileName = generators.listenerFileName(entity.name)}} +{{{ + exports({ + to: app.listenersPath(entity.path, listenerFileName) + }) +}}} +export default class {{ listenerName }} { +} diff --git a/stubs/make/middleware/main.stub b/stubs/make/middleware/main.stub new file mode 100644 index 00000000..af4adb2d --- /dev/null +++ b/stubs/make/middleware/main.stub @@ -0,0 +1,24 @@ +{{#var middlewareName = generators.middlewareName(entity.name)}} +{{#var middlewareFileName = generators.middlewareFileName(entity.name)}} +{{{ + exports({ + to: app.middlewarePath(entity.path, middlewareFileName) + }) +}}} +import type { HttpContext } from '@adonisjs/core/http' +import type { NextFn } from '@adonisjs/core/types/http' + +export default class {{ middlewareName }} { + async handle(ctx: HttpContext, next: NextFn) { + /** + * Middleware logic goes here (before the next call) + */ + console.log(ctx) + + /** + * Call next method in the pipeline and return its output + */ + const output = await next() + return output + } +} diff --git a/stubs/make/preload/main.stub b/stubs/make/preload/main.stub new file mode 100644 index 00000000..bb518063 --- /dev/null +++ b/stubs/make/preload/main.stub @@ -0,0 +1,6 @@ +{{#var preloadFileName = string(entity.name).snakeCase().removeExtension().ext('.ts').toString()}} +{{{ + exports({ + to: app.startPath(entity.path, preloadFileName) + }) +}}} diff --git a/stubs/make/provider/main.stub b/stubs/make/provider/main.stub new file mode 100644 index 00000000..024d549f --- /dev/null +++ b/stubs/make/provider/main.stub @@ -0,0 +1,37 @@ +{{#var providerName = generators.providerName(entity.name)}} +{{#var providerFileName = generators.providerFileName(entity.name)}} +{{{ + exports({ + to: app.providersPath(entity.path, providerFileName) + }) +}}} +import type { ApplicationService } from '@adonisjs/core/types' + +export default class {{ providerName }} { + constructor(protected app: ApplicationService) {} + + /** + * Register bindings to the container + */ + register() {} + + /** + * The container bindings have booted + */ + async boot() {} + + /** + * The application has been booted + */ + async start() {} + + /** + * The process has been started + */ + async ready() {} + + /** + * Preparing to shutdown the app + */ + async shutdown() {} +} diff --git a/stubs/make/service/main.stub b/stubs/make/service/main.stub new file mode 100644 index 00000000..1f149ba9 --- /dev/null +++ b/stubs/make/service/main.stub @@ -0,0 +1,10 @@ +{{#var serviceName = generators.serviceName(entity.name)}} +{{#var serviceFileName = generators.serviceFileName(entity.name)}} +{{{ + exports({ + to: app.servicesPath(entity.path, serviceFileName) + }) +}}} +export class {{ serviceName }} { + // Your code here +} diff --git a/stubs/make/test/main.stub b/stubs/make/test/main.stub new file mode 100644 index 00000000..35113f49 --- /dev/null +++ b/stubs/make/test/main.stub @@ -0,0 +1,13 @@ +{{#var testGroupName = generators.testGroupName(entity)}} +{{#var testFileName = generators.testFileName(entity.name)}} +{{{ + exports({ + to: app.makePath(suite.directory, entity.path, testFileName) + }) +}}} +import { test } from '@japa/runner' + +test.group('{{ testGroupName }}', () => { + test('example test', async ({ assert }) => { + }) +}) diff --git a/stubs/make/validator/main.stub b/stubs/make/validator/main.stub new file mode 100644 index 00000000..4564ca72 --- /dev/null +++ b/stubs/make/validator/main.stub @@ -0,0 +1,7 @@ +{{#var validatorFileName = generators.validatorFileName(entity.name)}} +{{{ + exports({ + to: app.validatorsPath(entity.path, validatorFileName) + }) +}}} +import vine from '@vinejs/vine' diff --git a/stubs/make/validator/resource.stub b/stubs/make/validator/resource.stub new file mode 100644 index 00000000..475429a3 --- /dev/null +++ b/stubs/make/validator/resource.stub @@ -0,0 +1,26 @@ +{{#var validatorName = string(generators.validatorName(entity.name)).noCase()}} +{{#var validatorFileName = generators.validatorFileName(entity.name)}} +{{#var createAction = generators.validatorActionName(entity.name, 'create')}} +{{#var updateAction = generators.validatorActionName(entity.name, 'update')}} +{{{ + exports({ + to: app.validatorsPath(entity.path, validatorFileName) + }) +}}} +import vine from '@vinejs/vine' + +/** + * Validator to validate the payload when creating + * a new {{ validatorName }}. + */ +export const {{ createAction }} = vine.compile( + vine.object({}) +) + +/** + * Validator to validate the payload when updating + * an existing {{ validatorName }}. + */ +export const {{ updateAction }} = vine.compile( + vine.object({}) +) diff --git a/stubs/make/view/main.stub b/stubs/make/view/main.stub new file mode 100644 index 00000000..f570e655 --- /dev/null +++ b/stubs/make/view/main.stub @@ -0,0 +1,6 @@ +{{#var viewFileName = generators.viewFileName(entity.name)}} +{{{ + exports({ + to: app.viewsPath(entity.path, viewFileName) + }) +}}} diff --git a/test/acceptance/app.spec.js b/test/acceptance/app.spec.js deleted file mode 100644 index e2897d2c..00000000 --- a/test/acceptance/app.spec.js +++ /dev/null @@ -1,177 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2015 Harminder Virk - * MIT Licensed -*/ - -const Browser = require('zombie') -const Route = require('../../src/Route') -const chai = require('chai') -const expect = chai.expect -const server = require('./setup') -const Helpers = require('../../src/Helpers') -const Ioc = require('adonis-fold').Ioc -const queryString = require('querystring') -const Middleware = require('../../src/Middleware') -require('co-mocha') - -Browser.localhost('localhost', 3333) -const browser = new Browser() - -describe('App Exceptations', function () { - before(function () { - server().listen('0.0.0.0', 3333) - Ioc.autoload(Helpers.appNameSpace(), Helpers.appPath()) - }) - - beforeEach(function () { - Middleware.new() - Route.new() - }) - - it('should respond to http request with plain text', function * () { - Route.get('/', function * (request, response) { - response.send('Hello zombie') - }) - yield browser.visit('/') - expect(browser.text('body').trim()).to.equal('Hello zombie') - }) - - it('should respond to http request as json', function * () { - const user = { - username: 'virk' - } - Route.get('/json', function * (request, response) { - response.json({user}) - }) - yield browser.visit('/json') - expect(browser.text('body')).to.equal(JSON.stringify({user})) - }) - - it('should respond via controller', function * () { - Route.get('/', 'HomeController.index') - yield browser.visit('/') - expect(browser.text('body').trim()).to.equal('Hello zombie via controller') - }) - - it('should set cookies on response', function * () { - Route.get('/', 'HomeController.cookies') - yield browser.visit('/') - const cartCookie = JSON.parse(queryString.unescape(browser.getCookie('cart')).replace('j:', '')) - expect(cartCookie).to.have.property('price') - expect(cartCookie).to.have.property('items') - }) - - it('should serve static resources when route is not registered', function * () { - yield browser.visit('/style.css') - expect(browser.text).to.match(/(?:\s*\S+\s*{[^}]*})+/g) - }) - - it('should throw 404 when nothing is found', function * () { - try { - yield browser.visit('/production.js') - } catch (e) { - browser.assert.status(404) - } - }) - - it('should run global middleware even if no route is defined', function * () { - Middleware.global(['App/Http/Middleware/Greet']) - yield browser.visit('/') - expect(browser.text('body').trim()).to.equal('Greetings!') - }) - - it('should reach route action when middleware yields next', function * () { - Middleware.global(['App/Http/Middleware/Counter']) - Route.get('/', function * (request, response) { - response.send(request.counter) - }) - yield browser.visit('/') - expect(browser.text('body').trim()).to.equal('1') - }) - - it('should return the request elapsed when controller has set timeout', function * () { - Middleware.global(['App/Http/Middleware/Logger']) - const done = function () { - return new Promise(function (resolve) { - setTimeout(function () { - resolve() - }, 1000) - }) - } - Route.get('/', function * (request, response) { - yield done(request) - }) - yield browser.visit('/') - expect(parseInt(browser.text('body').trim())).to.be.above(999) - }) - - it('should redirect request to a named route', function * () { - Route.get('/', 'HomeController.redirect') - Route.get('/:id', 'HomeController.profile').as('profile') - yield browser.visit('/') - expect(browser.text('body').trim()).to.equal('2') - }) - - it('should clear existing cookies', function * () { - Route.get('/', function * (request, response) { - response.clearCookie('name').send('') - }) - browser.setCookie({name: 'name', value: 'virk'}) - yield browser.visit('/') - expect(browser.getCookie('name')).to.equal(null) - }) - - it('should response to a url with .json extension', function * () { - Route.get('/', function * (request, response) { - response.send('sending via json route') - }).formats(['json']) - yield browser.visit('/.json') - expect(browser.text('body').trim()).to.equal('sending via json route') - }) - - it('should throw 404 when route is not found using a non-registered extension', function * () { - Route.get('/', function * (request, response) { - response.send('sending via json route') - }).formats(['json']) - try { - yield browser.visit('/.xml') - } catch (e) { - browser.assert.status(404) - } - }) - - it('should return request format', function * () { - Route.get('/admin', function * (request, response) { - response.send(request.format()) - }).formats(['json']) - yield browser.visit('/admin.json') - expect(browser.text('body').trim()).to.equal('json') - }) - - it('should send a view using sendView method', function * () { - Route.get('/', function * (request, response) { - yield response.sendView('index') - }) - yield browser.visit('/') - expect(browser.text('body').trim()).to.equal('sending via view') - }) - - it('should make use of form global helper to setup a form', function * () { - Route.get('/', function * (request, response) { - yield response.sendView('form') - }) - yield browser.visit('/') - expect(browser.html('form')).not.to.equal('') - expect(browser.html('input')).not.to.equal('') - expect(browser.html('button')).not.to.equal('') - }) - - it('should render a view using router render method', function * () { - Route.on('/signup').render('signup') - yield browser.visit('/signup?name=virk') - expect(browser.text('body')).to.equal('the url is /signup and the name is virk') - }) -}) diff --git a/test/acceptance/app/Http/Controllers/HomeController.js b/test/acceptance/app/Http/Controllers/HomeController.js deleted file mode 100644 index 43706d41..00000000 --- a/test/acceptance/app/Http/Controllers/HomeController.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict' - -class HomeController { - - * index (request, response) { - response.send('Hello zombie via controller') - } - - * cookies (request, response) { - response.cookie('cart', {price: 20,items: 2}).send('') - } - - * redirect (request, response) { - response.route('profile', {id: 2}) - } - - * profile (request, response) { - response.send(request.param('id')) - } - -} - -module.exports = HomeController diff --git a/test/acceptance/app/Http/Middleware/Counter.js b/test/acceptance/app/Http/Middleware/Counter.js deleted file mode 100644 index 2279cf27..00000000 --- a/test/acceptance/app/Http/Middleware/Counter.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class Counter { - - * handle (request, response, next) { - request.counter = 1 - yield next - } - -} - -module.exports = Counter diff --git a/test/acceptance/app/Http/Middleware/Greet.js b/test/acceptance/app/Http/Middleware/Greet.js deleted file mode 100644 index 65a584c2..00000000 --- a/test/acceptance/app/Http/Middleware/Greet.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -class Greet { - - * handle (request, response, next) { - response.status(200).send('Greetings!') - } - -} - -module.exports = Greet diff --git a/test/acceptance/app/Http/Middleware/Logger.js b/test/acceptance/app/Http/Middleware/Logger.js deleted file mode 100644 index fb0088c9..00000000 --- a/test/acceptance/app/Http/Middleware/Logger.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -class Logger { - - * handle (request, response, next) { - const start = new Date().getTime() - yield next - const elapsed = new Date().getTime() - start - response.send(elapsed) - } - -} - -module.exports = Logger diff --git a/test/acceptance/package.test.json b/test/acceptance/package.test.json deleted file mode 100644 index c2b3d440..00000000 --- a/test/acceptance/package.test.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "autoload": { - "App": "./app" - } -} diff --git a/test/acceptance/public/style.css b/test/acceptance/public/style.css deleted file mode 100644 index 26a891bb..00000000 --- a/test/acceptance/public/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body{ - color: #fff; -} diff --git a/test/acceptance/resources/views/form.njk b/test/acceptance/resources/views/form.njk deleted file mode 100644 index ce1768a5..00000000 --- a/test/acceptance/resources/views/form.njk +++ /dev/null @@ -1,4 +0,0 @@ -{{ form.open({url: '/user', method: 'POST'}) }} - {{ form.text('username') }} - {{ form.button('SignUp') }} -{{ form.close() }} diff --git a/test/acceptance/resources/views/index.njk b/test/acceptance/resources/views/index.njk deleted file mode 100644 index 7a6b6316..00000000 --- a/test/acceptance/resources/views/index.njk +++ /dev/null @@ -1 +0,0 @@ -

sending via view

diff --git a/test/acceptance/resources/views/signup.njk b/test/acceptance/resources/views/signup.njk deleted file mode 100644 index 85ade68b..00000000 --- a/test/acceptance/resources/views/signup.njk +++ /dev/null @@ -1 +0,0 @@ -the url is {{ request.url() }} and the name is {{ request.input('name') }} diff --git a/test/acceptance/setup/index.js b/test/acceptance/setup/index.js deleted file mode 100644 index 5a238404..00000000 --- a/test/acceptance/setup/index.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict' - -const Server = require('../../../src/Server') -const Route = require('../../../src/Route') -const Request = require('../../../src/Request') -const ResponseBuilder = require('../../../src/Response') -const View = require('../../../src/View') -const Middleware = require('../../../src/Middleware') -const EventProvider = require('../../../src/Event') -const Helpers = require('../../../src/Helpers') -const path = require('path') -const Static = require('../../../src/Static') - -class Session { -} - -const Config = { - get: function (key) { - switch (key) { - case 'app.appKey': - return null - case 'app.static': - return {} - default: - return 0 - } - } -} - -module.exports = function () { - Helpers.load(path.join(__dirname, '../package.test.json')) - const view = new View(Helpers, Config, Route) - const Response = new ResponseBuilder(view, Route, Config) - const staticServer = new Static(Helpers, Config) - const Event = new EventProvider(Config) - const server = new Server(Request, Response, Route, Helpers, Middleware, staticServer, Session, Config, Event) - return server -} diff --git a/test/functional/app/.env b/test/functional/app/.env deleted file mode 100644 index e69de29b..00000000 diff --git a/test/functional/providers.spec.js b/test/functional/providers.spec.js deleted file mode 100644 index e23d580e..00000000 --- a/test/functional/providers.spec.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const chai = require('chai') -const expect = chai.expect -const fold = require('adonis-fold') -const Ioc = fold.Ioc -const Registrar = fold.Registrar -const path = require('path') -const sinon = require('sinon') -const fs = require('fs') -require('co-mocha') - -function randomString (length) { - var s = [] - while (s.length < length) { - s.push(String.fromCharCode(Math.floor(Math.random() * 100))) - } - return s.join('') -} - -describe('Providers', function () { - beforeEach(function * () { - const providersDir = path.join(__dirname, '../../providers') - const providers = fs.readdirSync(providersDir).map((file) => path.join(providersDir, file)) - yield Registrar.register(providers) - Ioc.use('Adonis/Src/Helpers').loadInFuture(path.join(__dirname, './app')) - }) - - context('Config', function () { - it('should inject Helpers provider', function () { - const Config = require('../../src/Config') - const Helpers = Ioc.use('Adonis/Src/Helpers') - sinon.spy(Helpers, 'configPath') - const config = Ioc.use('Adonis/Src/Config') - expect(config).to.be.an.instanceof(Config) - expect(Helpers.configPath.calledOnce).to.equal(true) - Helpers.configPath.restore() - }) - }) - - context('Encryption', function () { - it('should inject Config provider', function () { - const Encryption = require('../../src/Encryption') - const Config = Ioc.use('Adonis/Src/Config') - sinon - .stub(Config, 'get') - .onFirstCall() - .returns(randomString(32)) - .onSecondCall() - .returns('aes-256-cbc') - - const encryption = Ioc.use('Adonis/Src/Encryption') - expect(encryption).to.be.an.instanceof(Encryption) - expect(Config.get.calledTwice).to.equal(true) - Config.get.restore() - }) - }) - - context('Env', function () { - it('should inject Helpers provider', function () { - const Helpers = Ioc.use('Adonis/Src/Helpers') - sinon.spy(Helpers, 'basePath') - const Env = require('../../src/Env') - const env = Ioc.use('Adonis/Src/Env') - expect(env).to.be.an.instanceof(Env) - expect(Helpers.basePath.calledOnce).to.equal(true) - Helpers.basePath.restore() - }) - }) - - context('Event', function () { - it('should inject Config and Helpers providers', function () { - const Config = Ioc.use('Adonis/Src/Config') - sinon.spy(Config, 'get') - const event = Ioc.use('Adonis/Src/Event') - const Event = require('../../src/Event') - expect(event).to.be.an.instanceof(Event) - expect(Config.get.calledOnce).to.equal(true) - expect(Config.get.calledWith('event')).to.equal(true) - Config.get.restore() - }) - }) - - context('Response', function () { - it('should return Request class to be initiated at each request', function () { - const Response = Ioc.use('Adonis/Src/Response') - expect(Response.name).to.equal('Response') - }) - }) - - context('Session', function () { - it('should return Session class', function () { - const Config = Ioc.use('Adonis/Src/Config') - const get = sinon.stub(Config, 'get') - get.withArgs('session.driver').returns('cookie') - const sessionManager = Ioc.use('Adonis/Src/Session') - const Session = require('../../src/Session') - expect(sessionManager).deep.equal(Session) - Config.get.restore() - }) - }) -}) diff --git a/test/unit/app/.env b/test/unit/app/.env deleted file mode 100644 index b368072e..00000000 --- a/test/unit/app/.env +++ /dev/null @@ -1 +0,0 @@ -APP_PORT=3000 diff --git a/test/unit/app/Http/Controllers/HomeController.js b/test/unit/app/Http/Controllers/HomeController.js deleted file mode 100644 index 6c63f814..00000000 --- a/test/unit/app/Http/Controllers/HomeController.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -class HomeController { - - * index (request, response) { - response.send({rendered: true}) - } - -} - -module.exports = HomeController diff --git a/test/unit/app/Http/Controllers/UserController.js b/test/unit/app/Http/Controllers/UserController.js deleted file mode 100644 index 458a4da1..00000000 --- a/test/unit/app/Http/Controllers/UserController.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -class UserController { - - * index (request, response) { - response.send(request.count) - } - -} - -module.exports = UserController diff --git a/test/unit/app/Http/Middleware/Auth.js b/test/unit/app/Http/Middleware/Auth.js deleted file mode 100644 index 524b319e..00000000 --- a/test/unit/app/Http/Middleware/Auth.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class Auth { - - * handle (request, response, next) { - request.count++ - yield next - } - -} - -module.exports = Auth diff --git a/test/unit/app/Http/Middleware/AuthMiddleware.js b/test/unit/app/Http/Middleware/AuthMiddleware.js deleted file mode 100644 index 1a102dcf..00000000 --- a/test/unit/app/Http/Middleware/AuthMiddleware.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class AuthMiddleware { - - * handle (request, response, next, scheme) { - request.scheme = scheme - yield next - } - -} - -module.exports = AuthMiddleware diff --git a/test/unit/app/Http/Middleware/Cycle.js b/test/unit/app/Http/Middleware/Cycle.js deleted file mode 100644 index e44ea3ae..00000000 --- a/test/unit/app/Http/Middleware/Cycle.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' - -class Cycle { - - * handle (request, response, next) { - request.count++ - } - -} - -module.exports = Cycle diff --git a/test/unit/app/Http/Middleware/Cycle2.js b/test/unit/app/Http/Middleware/Cycle2.js deleted file mode 100644 index 47070064..00000000 --- a/test/unit/app/Http/Middleware/Cycle2.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class Cycle2 { - - * handle (request, response, next) { - request.count++ - yield next - } - -} - -module.exports = Cycle2 diff --git a/test/unit/app/Http/Middleware/Global.js b/test/unit/app/Http/Middleware/Global.js deleted file mode 100644 index 782e5b63..00000000 --- a/test/unit/app/Http/Middleware/Global.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class Global { - - * handle (request, response, next) { - request.count = 2 - yield next - } - -} - -module.exports = Global diff --git a/test/unit/app/Http/Middleware/GlobalCatch.js b/test/unit/app/Http/Middleware/GlobalCatch.js deleted file mode 100644 index 0b503c5c..00000000 --- a/test/unit/app/Http/Middleware/GlobalCatch.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict' - -class GlobalCatch { - - * handle (request, response, next) { - response.status(401).send('Login') - } -} - -module.exports = GlobalCatch diff --git a/test/unit/app/Http/Middleware/GlobalThrow.js b/test/unit/app/Http/Middleware/GlobalThrow.js deleted file mode 100644 index 2b4e7da7..00000000 --- a/test/unit/app/Http/Middleware/GlobalThrow.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class GlobalCatch { - - * handle (request, response, next) { - let error = new Error('Login') - error.status = 401 - throw error - } -} - -module.exports = GlobalCatch diff --git a/test/unit/app/Http/Middleware/NoHandle.js b/test/unit/app/Http/Middleware/NoHandle.js deleted file mode 100644 index eb921743..00000000 --- a/test/unit/app/Http/Middleware/NoHandle.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -class NoHandle { - -} - -module.exports = NoHandle diff --git a/test/unit/app/Http/Middleware/Parser.js b/test/unit/app/Http/Middleware/Parser.js deleted file mode 100644 index 18b94813..00000000 --- a/test/unit/app/Http/Middleware/Parser.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class Parser { - - * handle (request, response, next) { - request.count = 0 - yield next - } - -} - -module.exports = Parser diff --git a/test/unit/app/views/async.njk b/test/unit/app/views/async.njk deleted file mode 100644 index 6f96f359..00000000 --- a/test/unit/app/views/async.njk +++ /dev/null @@ -1,3 +0,0 @@ - -{% yield profile = profile.get() %} -{{ profile }} diff --git a/test/unit/app/views/extends.njk b/test/unit/app/views/extends.njk deleted file mode 100644 index f0f24447..00000000 --- a/test/unit/app/views/extends.njk +++ /dev/null @@ -1,4 +0,0 @@ -{% extends 'subviews.master' %} - -{% block content %}

Hello world

{% endblock %} - diff --git a/test/unit/app/views/filter.njk b/test/unit/app/views/filter.njk deleted file mode 100644 index 1ca0dd16..00000000 --- a/test/unit/app/views/filter.njk +++ /dev/null @@ -1 +0,0 @@ -{{ 'virk' | mycase }} diff --git a/test/unit/app/views/global.njk b/test/unit/app/views/global.njk deleted file mode 100644 index 27c478dd..00000000 --- a/test/unit/app/views/global.njk +++ /dev/null @@ -1 +0,0 @@ -{{ time() }} diff --git a/test/unit/app/views/include.njk b/test/unit/app/views/include.njk deleted file mode 100644 index 1ca9b705..00000000 --- a/test/unit/app/views/include.njk +++ /dev/null @@ -1 +0,0 @@ -{% include 'subviews.index' %} diff --git a/test/unit/app/views/index.njk b/test/unit/app/views/index.njk deleted file mode 100644 index 6b5dbc50..00000000 --- a/test/unit/app/views/index.njk +++ /dev/null @@ -1 +0,0 @@ -

Hello world

diff --git a/test/unit/app/views/json.njk b/test/unit/app/views/json.njk deleted file mode 100644 index 0e57e92a..00000000 --- a/test/unit/app/views/json.njk +++ /dev/null @@ -1 +0,0 @@ -{{ profile | json(2) | safe }} diff --git a/test/unit/app/views/profile.njk b/test/unit/app/views/profile.njk deleted file mode 100644 index 1cbc0b17..00000000 --- a/test/unit/app/views/profile.njk +++ /dev/null @@ -1 +0,0 @@ -{{ 'profile' | route({id:1}) }} diff --git a/test/unit/app/views/profileAction.njk b/test/unit/app/views/profileAction.njk deleted file mode 100644 index e7bb7cd3..00000000 --- a/test/unit/app/views/profileAction.njk +++ /dev/null @@ -1 +0,0 @@ -{{ 'ProfileController.show' | action({id:1}) }} diff --git a/test/unit/app/views/services.njk b/test/unit/app/views/services.njk deleted file mode 100644 index 4ed5f83c..00000000 --- a/test/unit/app/views/services.njk +++ /dev/null @@ -1 +0,0 @@ -{{ typeof(use) }} diff --git a/test/unit/app/views/subviews/index.njk b/test/unit/app/views/subviews/index.njk deleted file mode 100644 index 6b5dbc50..00000000 --- a/test/unit/app/views/subviews/index.njk +++ /dev/null @@ -1 +0,0 @@ -

Hello world

diff --git a/test/unit/app/views/subviews/internal.njk b/test/unit/app/views/subviews/internal.njk deleted file mode 100644 index 3e8c5935..00000000 --- a/test/unit/app/views/subviews/internal.njk +++ /dev/null @@ -1 +0,0 @@ -{% include '../index' %} diff --git a/test/unit/app/views/subviews/master.njk b/test/unit/app/views/subviews/master.njk deleted file mode 100644 index 4275f805..00000000 --- a/test/unit/app/views/subviews/master.njk +++ /dev/null @@ -1,2 +0,0 @@ -{% block content %} -{% endblock %} diff --git a/test/unit/config.spec.js b/test/unit/config.spec.js deleted file mode 100644 index 6396efd0..00000000 --- a/test/unit/config.spec.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const Config = require('../../src/Config') -const chai = require('chai') -const path = require('path') -const expect = chai.expect -const Helpers = { - configPath: function () { - return path.join(__dirname, '/config') - } -} - -describe('Config', function () { - it('should ignore any files apart from .js files inside the config directory', function () { - const config = new Config(Helpers) - expect(config.config).not.have.property('.gitkeep') - expect(config.config).to.have.property('database') - }) - - it('should get values for a given key from config store', function () { - const config = new Config(Helpers) - const host = config.get('database.mysql.connection.host') - expect(host).to.equal('localhost') - }) - - it('should return default value when actual value does not exists', function () { - const config = new Config(Helpers) - const database = config.get('database.mysql.connection.database', 'adonis') - expect(database).to.equal('adonis') - }) - - it('should return null when default value is not defined', function () { - const config = new Config(Helpers) - const database = config.get('database.mysql.connection.database') - expect(database).to.equal(null) - }) - - it('should return actual value when value is a false boolean', function () { - const config = new Config(Helpers) - const database = config.get('database.connection') - expect(database).to.equal(false) - }) - - it('should return default value when default value is a false boolean', function () { - const config = new Config(Helpers) - const database = config.get('database.foo', false) - expect(database).to.equal(false) - }) - - it('should set value for a given key', function () { - const config = new Config(Helpers) - config.set('database.mysql.connection.database', 'blog') - const database = config.get('database.mysql.connection.database') - expect(database).to.equal('blog') - }) - - it('should set mid level paths via key', function () { - const config = new Config(Helpers) - config.set('database.mysql', { - connection: { - host: '127.0.0.1' - } - }) - const host = config.get('database.mysql.connection.host') - expect(host).to.equal('127.0.0.1') - }) - - it('should return booleans as booleans', function () { - const config = new Config(Helpers) - config.set('database.mysql.debug', true) - const debug = config.get('database.mysql.debug') - expect(debug).to.equal(true) - }) -}) diff --git a/test/unit/config/.gitkeep b/test/unit/config/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/test/unit/config/database.js b/test/unit/config/database.js deleted file mode 100644 index 11d0c23f..00000000 --- a/test/unit/config/database.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - mysql: { - client: 'mysql', - connection: { - host: 'localhost', - password: '', - user: '' - } - }, - - connection: false - -} diff --git a/test/unit/encryption.spec.js b/test/unit/encryption.spec.js deleted file mode 100644 index 7dce9776..00000000 --- a/test/unit/encryption.spec.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const Encryption = require('../../src/Encryption') -const chai = require('chai') -const crypto = require('crypto') -const expect = chai.expect -let config -let encryption - -class Config { - constructor (key, algorithm) { - this.key = key - this.algorithm = algorithm || 'aes-256-cbc' - } - get (key) { - if (key === 'app.appKey') { - return this.key - } - if (key === 'app.encryption.algorithm') { - return this.algorithm - } - } -} - -describe('Encryption', function () { - before(function () { - config = new Config('a'.repeat(32), 'aes-256-cbc') - encryption = new Encryption(config) - }) - - it('should throw error when APP_KEY is not defined', function () { - const fn = function () { - return new Encryption(new Config()) - } - expect(fn).to.throw('RuntimeException: E_MISSING_APPKEY: App key needs to be specified in order to make use of Encryption') - }) - - it('should throw error when APP_KEY to long', function () { - const fn = function () { - return new Encryption(new Config('a'.repeat(32), 'aes-128-cbc')) - } - expect(fn).to.throw('RuntimeException: E_INVALID_ENCRPYTION_CIPHER: The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths') - }) - - it('should throw error when APP_KEY is wrong', function () { - const fn = function () { - return new Encryption(new Config('a'.repeat(5), 'aes-256-cbc')) - } - expect(fn).to.throw('RuntimeException: E_INVALID_ENCRPYTION_CIPHER: The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths') - }) - - it('should throw error when cipher is unsupported', function () { - const fn = function () { - return new Encryption(new Config('a'.repeat(16), 'AES-256-CFB8')) - } - expect(fn).to.throw('RuntimeException: E_INVALID_ENCRPYTION_CIPHER: The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths') - }) - - it('should throw error when APP_KEY length is wrong and cipher is unsupported', function () { - const fn = function () { - return new Encryption(new Config('a'.repeat(16), 'AES-256-CFB8')) - } - expect(fn).to.throw('RuntimeException: E_INVALID_ENCRPYTION_CIPHER: The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths') - }) - - it('should calculate a correct sha256 hash', function () { - const hash = encryption.hash('These Aren\'t the Droids ', 'You\'re Looking For') - expect(hash).to.equal(crypto.createHmac('sha256', config.get('app.appKey')).update('These Aren\'t the Droids You\'re Looking For').digest('hex')) - }) - - it('should calculate a correct sha256 hash using HMAC method', function () { - const hmac = encryption.hashHmac('sha256', 'These Aren\'t the Droids You\'re Looking For', config.get('app.appKey')) - expect(hmac).to.equal(crypto.createHmac('sha256', config.get('app.appKey')).update('These Aren\'t the Droids You\'re Looking For').digest('hex')) - }) - - it('should encode base64', function () { - const base64 = encryption.base64Encode('These Aren\'t the Droids You\'re Looking For') - expect(base64).to.equal('VGhlc2UgQXJlbid0IHRoZSBEcm9pZHMgWW91J3JlIExvb2tpbmcgRm9y') - }) - - it('should decode base64', function () { - const plain = encryption.base64Decode('VGhlc2UgQXJlbid0IHRoZSBEcm9pZHMgWW91J3JlIExvb2tpbmcgRm9y') - expect(plain).to.equal('These Aren\'t the Droids You\'re Looking For') - }) - - it('should detect valid payload', function () { - const invalid = encryption.invalidPayload({iv: '', value: '', mac: ''}) - expect(invalid).to.equal(false) - }) - - it('should detect valid mac', function () { - const payload = {iv: 'gD+wK78S1q4L3Vzgullp8Q==', value: 'These Aren\'t the Droids You\'re Looking For', mac: 'ffcfa6ced2727ba646467688e1f3ae0d38ccb7c5b4a9c6f9876d6d749100c2bd'} - const invalid = encryption.validMac(payload) - expect(invalid).to.equal(true) - }) - - it('should throw error when payload is invalid', function () { - const fn = function () { - return encryption.getJsonPayload('Int9Ig==') - } - expect(fn).to.throw('RuntimeException: E_INVALID_ENCRYPTION_PAYLOAD: The payload is invalid') - }) - - it('should throw error when payload is not an json object', function () { - const fn = function () { - return encryption.getJsonPayload('foo') - } - expect(fn).to.throw('RuntimeException: E_MALFORMED_JSON: The payload is not a json object') - }) - - it('should throw error when mac is invalid', function () { - let iv = crypto.randomBytes(16) - const mac = encryption.hash(iv = encryption.base64Encode(iv), 'These Aren\'t the Droids You\'re Looking For') - const json = JSON.stringify({iv: iv, value: 'These Are the Droids You\'re Looking For', mac: mac}) - const base64 = encryption.base64Encode(json) - const fn = function () { - return encryption.getJsonPayload(base64) - } - expect(fn).to.throw('RuntimeException: E_INVALID_ENCRYPTION_MAC: The MAC is invalid') - }) - - it('should throw error when encrypt value is empty', function () { - const fn = () => encryption.encrypt('') - expect(fn).to.throw('InvalidArgumentException: E_MISSING_PARAMETER: Could not encrypt the data') - }) - - it('should decrypt values using defined algorithm', function () { - const encrypted = encryption.encrypt('These Aren\'t the Droids You\'re Looking For') - const decrypted = encryption.decrypt(encrypted) - expect(decrypted).to.equal('These Aren\'t the Droids You\'re Looking For') - }) - - it('should throw error with different keys', function () { - const fn = function () { - const a = new Encryption(new Config('a'.repeat(32), 'aes-256-cbc')) - const b = new Encryption(new Config('b'.repeat(32), 'aes-256-cbc')) - console.log(b.decrypt(a.encrypt('These Aren\'t the Droids You\'re Looking For'))) - } - expect(fn).to.throw('RuntimeException: E_INVALID_ENCRYPTION_MAC: The MAC is invalid') - }) -}) diff --git a/test/unit/env.spec.js b/test/unit/env.spec.js deleted file mode 100644 index d6799b9e..00000000 --- a/test/unit/env.spec.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const Env = require('../../src/Env') -const stderr = require('test-console').stderr -const path = require('path') -const chai = require('chai') -const expect = chai.expect - -class Event { - static fire () {} -} - -const Helpers = { - basePath: function () { - return path.join(__dirname, './app') - } -} - -describe('Env', function () { - it('should load .env file by initiating Env class', function () { - /*eslint-disable no-new*/ - new Env(Helpers, Event) - }) - - it('should load .env file from the location defined as ENV_PATH flag', function () { - const inspect = stderr.inspect() - process.env.ENV_PATH = '/users/.env' - /*eslint-disable no-new*/ - new Env(Helpers, Event) - inspect.restore() - expect(inspect.output[0]).to.match(/\/users\/\.env/) - process.env.ENV_PATH = '' - }) - - it('should not inherit path from the basePath when ENV_PATH location has absolute path', function () { - const inspect = stderr.inspect() - process.env.ENV_PATH = '/.env' - /*eslint-disable no-new*/ - new Env(Helpers, Event) - inspect.restore() - expect(inspect.output[0]).to.match(/\.env/) - process.env.ENV_PATH = '' - }) - - it('should get values defined in .env file', function () { - const env = new Env(Helpers, Event) - expect(env.get('APP_PORT')).to.equal('3000') - }) - - it('should return default value when it does exists in .env file', function () { - const env = new Env(Helpers, Event) - expect(env.get('APP_KEY', 'foo')).to.equal('foo') - }) - - it('should return default value when it does exists in .env file and default value is a boolean', function () { - const env = new Env(Helpers, Event) - expect(env.get('APP_KEY', false)).to.equal(false) - }) - - it('should override defined values', function () { - const env = new Env(Helpers, Event) - env.set('APP_PORT', 4000) - expect(env.get('APP_PORT')).to.equal('4000') - }) - - it('should convert boolean strings into a valid boolean', function () { - const env = new Env(Helpers, Event) - env.set('CACHE_VIEWS', false) - expect(env.get('CACHE_VIEWS')).to.equal(false) - }) - - it('should convert 0 and 1 to true and false', function () { - const env = new Env(Helpers, Event) - env.set('CACHE_VIEWS', 0) - expect(env.get('CACHE_VIEWS')).to.equal(false) - }) - - it('should convert true defined as string to a boolean', function () { - const env = new Env(Helpers, Event) - env.set('CACHE_VIEWS', true) - expect(env.get('CACHE_VIEWS')).to.equal(true) - }) -}) diff --git a/test/unit/event.spec.js b/test/unit/event.spec.js deleted file mode 100644 index 4bd5b5e3..00000000 --- a/test/unit/event.spec.js +++ /dev/null @@ -1,399 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const chai = require('chai') -const expect = chai.expect -const Ioc = require('adonis-fold').Ioc -const Event = require('../../src/Event') - -const Config = { - get: function () { - return { - wildcard: true - } - } -} - -const Helpers = { - makeNameSpace: function (base, toPath) { - return `App/${base}/${toPath}` - } -} - -describe('Event', function () { - it('should throw an exception when event handler is not a valid function or reference to function', function () { - const event = new Event(Config, Helpers) - const fn = () => event.on('foo', {}) - expect(fn).to.throw('InvalidArgumentException: E_INVALID_IOC_BINDING: Handler must point to a valid namespace or a closure') - }) - - it('should throw an exception when event.once handler is not a valid function or reference to function', function () { - const event = new Event(Config, Helpers) - const fn = () => event.once('foo', {}) - expect(fn).to.throw('InvalidArgumentException: E_INVALID_IOC_BINDING: Handler must point to a valid namespace or a closure') - }) - - it('should throw an exception when event.any handler is not a valid function or reference to function', function () { - const event = new Event(Config, Helpers) - const fn = () => event.any({}) - expect(fn).to.throw('InvalidArgumentException: E_INVALID_IOC_BINDING: Handler must point to a valid namespace or a closure') - }) - - it('should be able to register an event', function (done) { - const event = new Event(Config, Helpers) - event.on('foo', function (data) { - expect(data).deep.equal({foo: 'bar'}) - done() - }) - event.fire('foo', {foo: 'bar'}) - }) - - it('should be able to pass multiple arguments to the emit method', function (done) { - const event = new Event(Config, Helpers) - event.on('foo', function (data, bar) { - expect(data).deep.equal({foo: 'bar'}) - expect(bar).deep.equal({bar: 'baz'}) - done() - }) - event.emit('foo', {foo: 'bar'}, {bar: 'baz'}) - }) - - it('should be able to pass multiple arguments to the fire method', function (done) { - const event = new Event(Config, Helpers) - event.on('foo', function (data, bar) { - expect(data).deep.equal({foo: 'bar'}) - expect(bar).deep.equal({bar: 'baz'}) - done() - }) - event.fire('foo', {foo: 'bar'}, {bar: 'baz'}) - }) - - it('should be able to bind a class instance to the callback', function (done) { - const event = new Event(Config, Helpers) - class Foo { - constructor () { - this.name = 'foo' - } - sayFoo () { - expect(this.name).to.equal('foo') - done() - } - } - const fooInstance = new Foo() - event.on('foo', fooInstance.sayFoo.bind(fooInstance)) - event.fire('foo') - }) - - it('should be able to register generator method as callback', function (done) { - const event = new Event(Config, Helpers) - const getName = function () { - return new Promise(function (resolve) { - setTimeout(function () { - resolve('foo') - }) - }) - } - event.on('foo', function * () { - const name = yield getName() - expect(name).to.equal('foo') - done() - }) - event.fire('foo') - }) - - it('should be able to make class instance from Ioc Container', function (done) { - const event = new Event(Config, Helpers) - class Foo { - - constructor () { - this.name = 'foo' - } - - sayFoo () { - expect(this.name).to.equal('foo') - done() - } - } - Ioc.bind('App/Listeners/Foo', function () { - return new Foo() - }) - - event.on('foo', 'Foo.sayFoo') - event.fire('foo') - }) - - it('should be able to bind generator method', function (done) { - const event = new Event(Config, Helpers) - class Foo { - constructor () { - this.name = null - } - - * setName () { - this.name = 'foo' - } - - * sayFoo () { - yield this.setName() - expect(this.name).to.equal('foo') - done() - } - } - Ioc.bind('App/Listeners/Foo', function () { - return new Foo() - }) - - event.on('foo', 'Foo.sayFoo') - event.fire('foo') - }) - - it('should be able to get data passed by the fire method', function (done) { - const event = new Event(Config, Helpers) - class Foo { - sayFoo (data) { - expect(data).deep.equal({foo: 'bar'}) - done() - } - } - Ioc.bind('App/Listeners/Foo', function () { - return new Foo() - }) - - event.on('foo', 'Foo.sayFoo') - event.fire('foo', {foo: 'bar'}) - }) - - it('should be able to get data passed by the fire method inside a generator method', function (done) { - const event = new Event(Config, Helpers) - class Foo { - * sayFoo (data) { - expect(data).deep.equal({foo: 'bar'}) - done() - } - } - Ioc.bind('App/Listeners/Foo', function () { - return new Foo() - }) - - event.on('foo', 'Foo.sayFoo') - event.fire('foo', {foo: 'bar'}) - }) - - it('should be able to listen for a event using the when method', function (done) { - const event = new Event(Config, Helpers) - event.when('user.login', function (data) { - expect(data).deep.equal({username: 'doe'}) - done() - }) - event.fire('user.login', {username: 'doe'}) - }) - - it('should be able to listen for a event using the listen method', function (done) { - const event = new Event(Config, Helpers) - event.listen('user.login', function (data) { - expect(data).deep.equal({username: 'doe'}) - done() - }) - event.fire('user.login', {username: 'doe'}) - }) - - it('should be able to listen for any event', function (done) { - const event = new Event(Config, Helpers) - event.any(function (event, data) { - expect(event).to.equal('foo') - expect(data).to.equal('bar') - done() - }) - event.fire('foo', 'bar') - }) - - it('should be able to register one time only event listener', function (done) { - let count = 0 - const event = new Event(Config, Helpers) - event.once('foo', function () { - count++ - }) - event.fire('foo') - event.fire('foo') - expect(count).to.equal(1) - done() - }) - - it('should be able to get list of listeners for a specific event', function () { - const event = new Event(Config, Helpers) - event.once('foo', function () {}) - const listeners = event.getListeners('foo') - expect(listeners).to.be.an('array') - expect(listeners.length).to.equal(1) - }) - - it('should be able to get list of listeners for wildcard events', function () { - const event = new Event(Config, Helpers) - event.once('foo.bar', function () {}) - const listeners = event.getListeners('foo.*') - expect(listeners).to.be.an('array') - expect(listeners.length).to.equal(1) - }) - - it('should tell whether there are any listeners for a given event', function () { - const event = new Event(Config, Helpers) - event.once('foo.bar', function () {}) - expect(event.hasListeners('foo.*')).to.equal(true) - }) - - it('should tell whether wildcard is enabled or not', function () { - const event = new Event(Config, Helpers) - expect(event.wildcard()).to.equal(true) - }) - - it('should be able to define named events', function (done) { - const event = new Event(Config, Helpers) - event.on('foo', 'fooEvent', function () { - done() - }) - event.fire('foo') - }) - - it('should be able to remove named events', function () { - const event = new Event(Config, Helpers) - event.on('foo', 'fooEvent', function () {}) - event.on('foo', 'anotherEvent', function () {}) - event.removeListener('foo', 'fooEvent') - const listeners = event.getListeners('foo') - expect(listeners.length).to.equal(1) - }) - - it('should throw error when trying to remove unregistered named event', function () { - const event = new Event(Config, Helpers) - const fn = function () { - event.removeListener('foo', 'fooEvent') - } - expect(fn).to.throw('InvalidArgumentException: E_MISSING_NAMED_EVENT: Cannot find an event with fooEvent name for foo event') - }) - - it('should be able to remove the correct named events', function (done) { - const event = new Event(Config, Helpers) - event.on('foo', 'fooEvent', function () { - expect(true).to.equal(false) - }) - event.on('foo', 'anotherEvent', function () { - expect(true).to.equal(true) - done() - }) - event.removeListener('foo', 'fooEvent') - event.fire('foo') - }) - - it('should be able to remove all listeners for a given event', function () { - const event = new Event(Config, Helpers) - event.on('foo', function () {}) - event.on('foo', function () {}) - event.removeListeners('foo') - const listeners = event.getListeners('foo') - expect(listeners.length).to.equal(0) - }) - - it('should be able to remove all listeners for all events', function () { - const event = new Event(Config, Helpers) - event.on('foo', function () {}) - event.on('bar', function () {}) - event.removeListeners() - expect(event.getListeners('foo').length).to.equal(0) - expect(event.getListeners('bar').length).to.equal(0) - }) - - it('should be able to define the number for times a event should be executed', function () { - const event = new Event(Config, Helpers) - let count = 0 - event.times(4).on('foo', function () { - count++ - }) - event.fire('foo') - event.fire('foo') - event.fire('foo') - event.fire('foo') - event.fire('foo') - event.fire('foo') - event.fire('foo') - expect(count).to.equal(4) - }) - - it('should have access to the actual event via the emitter property on context', function (done) { - const event = new Event(Config, Helpers) - event.on('foo', function () { - expect(this.emitter.event).to.equal('foo') - done() - }) - event.fire('foo') - }) - - it('should have access to the actual event via the emitter property on context when a generator method is binded', function (done) { - const event = new Event(Config, Helpers) - const getName = function () { - return new Promise((resolve) => resolve('done')) - } - event.on('foo', function * () { - yield getName() - expect(this.emitter.event).to.equal('foo') - done() - }) - event.fire('foo') - }) - - it('should have access to the actual event resolving out of the IoC container', function (done) { - const event = new Event(Config, Helpers) - class FooListener { - sayFoo () { - expect(this.constructor.name).to.equal('FooListener') - expect(this.emitter.event).to.equal('foo') - done() - } - } - Ioc.bind('App/Listeners/Foo', function () { - return new FooListener() - }) - event.on('foo', 'Foo.sayFoo') - event.fire('foo') - }) - - it('should have access to the actual event resolving out of the IoC container within a generator method', function (done) { - const event = new Event(Config, Helpers) - class FooListener { - * sayFoo () { - expect(this.constructor.name).to.equal('FooListener') - expect(this.emitter.event).to.equal('foo') - done() - } - } - Ioc.bind('App/Listeners/Foo', function () { - return new FooListener() - }) - event.on('foo', 'Foo.sayFoo') - event.fire('foo') - }) - - it('should return the actual when with emitting event as an array', function (done) { - const Config = { - get: function () { - return { - delimiter: ':', - wildcard: true - } - } - } - const event = new Event(Config, Helpers) - event.on('Http:error', function () { - expect(this.emitter.eventName).to.equal('Http:error') - done() - }) - event.fire(['Http', 'error'], {foo: 'bar'}) - }) -}) diff --git a/test/unit/file.spec.js b/test/unit/file.spec.js deleted file mode 100644 index 9b93c359..00000000 --- a/test/unit/file.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const File = require('../../src/File') -const chai = require('chai') -const path = require('path') -const fs = require('fs') -const expect = chai.expect - -require('co-mocha') - -describe('File', function () { - beforeEach(function () { - this.file = new File({ - name: 'npm-logo.svg', - path: path.join(__dirname, './uploads/npm-logo.svg'), - type: 'svg', - size: '233' - }) - }) - - after(function (done) { - fs.rename(path.join(__dirname, '/public/logo.svg'), path.join(__dirname, '/uploads/npm-logo.svg'), function (err) { - if (err) { - done(err) - } else { - done() - } - }) - }) - - it('should throw an error when unable to move file', function * () { - yield this.file.move('./boom') - expect(this.file.moved()).to.equal(false) - expect(this.file.errors().message).to.match(/no such file or directory/) - }) - - it('should move file to a given path with its original name', function * () { - yield this.file.move(path.join(__dirname, './public')) - expect(this.file.moved()).to.equal(true) - }) - - it('should move file to a given path with new name', function * () { - this.file.file.path = path.join(__dirname, './public/npm-logo.svg') - yield this.file.move(path.join(__dirname, './public'), 'logo.svg') - expect(this.file.moved()).to.equal(true) - expect(this.file.uploadName()).to.equal('logo.svg') - expect(this.file.uploadPath()).to.equal(path.join(__dirname, './public/logo.svg')) - }) - - it('should return file mime type', function * () { - expect(this.file.mimeType()).to.equal('svg') - }) - - it('should return file extension', function * () { - expect(this.file.extension()).to.equal('svg') - }) - - it('should return file size', function * () { - expect(this.file.clientSize()).to.equal('233') - }) - - it('should tell whether file exists on tmp path or not', function * () { - expect(this.file.exists()).to.equal(true) - }) -}) diff --git a/test/unit/filedriver.spec.js b/test/unit/filedriver.spec.js deleted file mode 100644 index ed88bfe3..00000000 --- a/test/unit/filedriver.spec.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const chai = require('chai') -const expect = chai.expect -const fs = require('co-fs-extra') -const path = require('path') -const FileDriver = require('../../src/Session/Drivers').file - -const Helpers = { - storagePath: function () { - return path.join(__dirname, '/storage/sessions') - } -} - -const Config = { - get: function () { - return 'sessions' - } -} - -require('co-mocha') - -describe('Session File Driver', function () { - this.timeout(5000) - - this.beforeEach(function * () { - yield fs.remove(path.join(__dirname, '/storage/sessions')) - }) - - it('should save session values using create method', function * () { - const fileDriver = new FileDriver(Helpers, Config) - const sessionId = '102102201' - yield fileDriver.write(sessionId, {greeting: 'bye world'}) - const contents = yield fs.readFile(path.join(__dirname, '/storage/sessions/' + sessionId), 'utf-8') - expect(JSON.parse(contents)).deep.equal({greeting: 'bye world'}) - }) - - it('should make use of sessions directory when no directory is specified under config', function * () { - Config.get = function () { - return null - } - const fileDriver = new FileDriver(Helpers, Config) - const sessionId = '102102201' - yield fileDriver.write(sessionId, {greeting: 'bye world'}) - const contents = yield fs.readFile(path.join(__dirname, '/storage/sessions/' + sessionId), 'utf-8') - expect(JSON.parse(contents)).deep.equal({greeting: 'bye world'}) - }) - - it('should read session value from a given file', function * () { - Config.get = function () { - return null - } - const fileDriver = new FileDriver(Helpers, Config) - const sessionId = '102102201' - yield fileDriver.write(sessionId, JSON.stringify({name: 'virk'})) - const contents = yield fileDriver.read(sessionId) - expect(JSON.parse(contents)).deep.equal({name: 'virk'}) - }) - - it('should return empty object when unable to read file', function * () { - Config.get = function () { - return null - } - const fileDriver = new FileDriver(Helpers, Config) - const sessionId = '10010' - yield fileDriver.write(sessionId, JSON.stringify({name: 'virk'})) - const contents = yield fileDriver.read('102102202') - expect(contents).deep.equal({}) - }) - - it('should be able to destroy a session file', function * () { - Config.get = function () { - return null - } - const fileDriver = new FileDriver(Helpers, Config) - const sessionId = '10010' - yield fileDriver.write(sessionId, JSON.stringify({name: 'virk'})) - yield fileDriver.destroy(sessionId) - const contents = yield fileDriver.read(sessionId) - expect(contents).deep.equal({}) - }) - - it('should return silently when session file does not exists', function * () { - Config.get = function () { - return null - } - const fileDriver = new FileDriver(Helpers, Config) - const sessionId = 'abc' - yield fileDriver.destroy(sessionId) - const contents = yield fileDriver.read(sessionId) - expect(contents).deep.equal({}) - }) -}) diff --git a/test/unit/hash.spec.js b/test/unit/hash.spec.js deleted file mode 100644 index e1357749..00000000 --- a/test/unit/hash.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed - */ - -const Hash = require('../../src/Hash') -const chai = require('chai') -const expect = chai.expect -require('co-mocha') - -describe('Hashing', function () { - it('should hash a value using make method', function * () { - yield Hash.make('foo') - }) - it('should compare hash value using make method', function * () { - const hashed = yield Hash.make('foo') - const verified = yield Hash.verify('foo', hashed) - expect(verified).to.equal(true) - }) - it('should return false when wrong values are passed', function * () { - const hashed = yield Hash.make('foo') - const verified = yield Hash.verify('bar', hashed) - expect(verified).to.equal(false) - }) - it('should throw error when wrong values are passed during make method', function * () { - try { - yield Hash.make('foo', 'bar') - expect(true).to.equal(false) - } catch (e) { - expect(e.message).to.match(/Invalid salt version/) - } - }) - - it('should throw error when wrong values are passed during verify method', function * () { - try { - yield Hash.verify('foo') - expect(true).to.equal(false) - } catch (e) { - expect(e.message).to.match(/Illegal arguments/) - } - }) -}) diff --git a/test/unit/helpers.spec.js b/test/unit/helpers.spec.js deleted file mode 100644 index 52c4c7ba..00000000 --- a/test/unit/helpers.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const Helpers = require('../../src/Helpers') -const chai = require('chai') -const path = require('path') -const expect = chai.expect -const NE = require('node-exceptions') - -const basePath = path.join(__dirname, '../acceptance') - -describe('Helpers', function () { - it('should throw error when autoload does exists in package file', function () { - const fn = function () { - Helpers.load(path.join(__dirname, './package.test.json')) - } - expect(fn).to.throw(NE.DomainException, /autoload must be enable/) - }) - - it('should throw error when autoload exists but not configured', function () { - const fn = function () { - Helpers.load(path.join(__dirname, './package.another.json')) - } - expect(fn).to.throw(NE.DomainException, /autoload must be enable/) - }) - - it('should set autoload values when Ioc instance is passed to load method', function () { - let appKey, appNamesapce - const Ioc = { - autoload: function (namespace, key) { - appKey = key - appNamesapce = namespace - } - } - Helpers.load(path.join(basePath, './package.test.json'), Ioc) - expect(appKey).to.equal(path.join(basePath, './app')) - expect(appNamesapce).to.equal('App') - }) - - context('After Load', function () { - beforeEach(function () { - Helpers.load(path.join(basePath, './package.test.json')) - }) - - it('should return project base directory path', function () { - expect(Helpers.basePath()).to.equal(basePath) - }) - - it('should return the tree of appDirectories', function () { - expect(Helpers.getProjectDirectories()).deep.equal({ - public: 'public', - storage: 'storage', - database: 'database', - resources: 'resources', - config: 'config', - app: './app' - }) - }) - - it('should be able to override a given directory path', function () { - Helpers.setProjectDirectory('public', 'dist') - expect(Helpers.publicPath()).to.match(/dist$/) - }) - - it('should return project app directory path', function () { - expect(Helpers.appPath()).to.equal(path.join(basePath, './app')) - }) - - it('should return project public directory path', function () { - expect(Helpers.publicPath()).to.equal(path.join(basePath, './public')) - }) - - it('should return public path to a given file', function () { - expect(Helpers.publicPath('style.css')).to.equal(path.join(basePath, './public/style.css')) - }) - - it('should return project config directory path', function () { - expect(Helpers.configPath()).to.equal(path.join(basePath, './config')) - }) - - it('should return config path to a given file', function () { - expect(Helpers.configPath('database.js')).to.equal(path.join(basePath, './config/database.js')) - }) - - it('should return project storage directory path', function () { - expect(Helpers.storagePath()).to.equal(path.join(basePath, './storage')) - }) - - it('should return storage path to a given file', function () { - expect(Helpers.storagePath('cache')).to.equal(path.join(basePath, './storage/cache')) - }) - - it('should return project resources directory path', function () { - expect(Helpers.resourcesPath()).to.equal(path.join(basePath, './resources')) - }) - - it('should return resources path to a given file', function () { - expect(Helpers.resourcesPath('views')).to.equal(path.join(basePath, './resources/views')) - }) - - it('should return project migrations directory path', function () { - expect(Helpers.migrationsPath()).to.equal(path.join(basePath, './database/migrations')) - }) - - it('should return migrations path to a given file', function () { - expect(Helpers.migrationsPath('1234.js')).to.equal(path.join(basePath, './database/migrations/1234.js')) - }) - - it('should return project seeds directory path', function () { - expect(Helpers.seedsPath()).to.equal(path.join(basePath, './database/seeds')) - }) - - it('should return migrations path to a given file', function () { - expect(Helpers.seedsPath('1234.js')).to.equal(path.join(basePath, './database/seeds/1234.js')) - }) - - it('should return project factories directory path', function () { - expect(Helpers.factoriesPath()).to.equal(path.join(basePath, './database/factories')) - }) - - it('should return migrations path to a given file', function () { - expect(Helpers.factoriesPath('1234.js')).to.equal(path.join(basePath, './database/factories/1234.js')) - }) - - it('should return project views directory path', function () { - expect(Helpers.viewsPath()).to.equal(path.join(basePath, './resources/views')) - }) - - it('should make complete namespace for a given namespace', function () { - const hook = Helpers.makeNameSpace('Model/Hooks', 'UserHook.validate') - expect(hook).to.equal('App/Model/Hooks/UserHook.validate') - }) - - it('should return complete namespace when toPath is already a complete namespace', function () { - const hook = Helpers.makeNameSpace('Model/Hooks', 'App/Model/Hooks/UserHook.validate') - expect(hook).to.equal('App/Model/Hooks/UserHook.validate') - }) - }) -}) diff --git a/test/unit/middleware.spec.js b/test/unit/middleware.spec.js deleted file mode 100644 index b8b4789e..00000000 --- a/test/unit/middleware.spec.js +++ /dev/null @@ -1,176 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const chai = require('chai') -const expect = chai.expect -const Ioc = require('adonis-fold').Ioc -const path = require('path') -const Middleware = require('../../src/Middleware') -require('co-mocha') - -describe('Middleware', function () { - afterEach(function () { - Middleware.new() - Ioc.new() - Ioc.autoload('App', path.join(__dirname, './app')) - }) - - it('should register a global middleware', function () { - Middleware.register('App/Foo/Bar') - const global = Middleware.getGlobal() - expect(global[0]).to.equal('App/Foo/Bar') - }) - - it('should register a named middleware', function () { - Middleware.register('bar', 'App/Foo/Bar') - const named = Middleware.getNamed() - expect(named.bar).to.equal('App/Foo/Bar') - }) - - it('should bulk register global middleware', function () { - Middleware.global(['App/Foo/Bar', 'App/Foo/Baz']) - const global = Middleware.getGlobal() - expect(global).deep.equal(['App/Foo/Bar', 'App/Foo/Baz']) - }) - - it('should register only unique middleware to the global list', function () { - Middleware.global(['App/Foo/Bar', 'App/Foo/Bar']) - const global = Middleware.getGlobal() - expect(global).have.length(1) - expect(global[0]).to.equal('App/Foo/Bar') - }) - - it('should bulk register a named middleware', function () { - const namedMiddleware = { - 'bar': 'App/Foo/Bar', - 'baz': 'App/Foo/Baz' - } - Middleware.named(namedMiddleware) - const named = Middleware.getNamed() - expect(named).deep.equal(namedMiddleware) - }) - - it('should fetch parameters from named middleware', function () { - expect(Middleware.fetchParams('basic')).deep.equal(['basic']) - }) - - it('should fetch parameters from multiple named middleware', function () { - expect(Middleware.fetchParams('basic,false')).deep.equal(['basic', 'false']) - }) - - it('should resolve all global middleware using resolve method', function () { - Middleware.global(['App/Http/Middleware/Global']) - const resolved = Middleware.resolve({}, true) - expect(resolved).to.be.an('array') - expect(resolved.length).to.equal(1) - expect(resolved[0]).to.have.property('instance') - expect(resolved[0]).to.have.property('method') - expect(resolved[0]).to.have.property('parameters') - }) - - it('should format named middleware keys to namespace params mappings', function () { - Middleware.register('auth', 'App/Http/Middleware/AuthMiddleware') - const formatted = Middleware.formatNamedMiddleware(['auth:basic']) - expect(formatted).to.deep.equal({'App/Http/Middleware/AuthMiddleware': ['basic']}) - }) - - it('should throw error when unable to find mapping inside middleware store', function () { - const formatted = function () { - return Middleware.formatNamedMiddleware(['auth:basic']) - } - expect(formatted).to.throw('RuntimeException: E_MISSING_NAMED_MIDDLEWARE: auth is not registered as a named middleware') - }) - - it('should resolve named middleware using resolve method', function () { - Middleware.register('auth', 'App/Http/Middleware/AuthMiddleware') - const formatted = Middleware.formatNamedMiddleware(['auth:basic']) - const resolved = Middleware.resolve(formatted, false) - expect(resolved.length).to.equal(1) - expect(resolved[0]).to.have.property('instance') - expect(resolved[0]).to.have.property('method') - expect(resolved[0]).to.have.property('parameters') - expect(resolved[0].parameters).deep.equal(['basic']) - }) - - it('should resolve global and named named middleware using resolve method', function () { - Middleware.register('auth', 'App/Http/Middleware/AuthMiddleware') - Middleware.global(['App/Http/Middleware/Global']) - const formatted = Middleware.formatNamedMiddleware(['auth:basic']) - const resolved = Middleware.resolve(formatted, true) - expect(resolved.length).to.equal(2) - expect(resolved[0]).to.have.property('instance') - expect(resolved[0]).to.have.property('method') - expect(resolved[0]).to.have.property('parameters') - expect(resolved[0].parameters).deep.equal([]) - expect(resolved[1]).to.have.property('instance') - expect(resolved[1]).to.have.property('method') - expect(resolved[1]).to.have.property('parameters') - expect(resolved[1].parameters).deep.equal(['basic']) - }) - - it('should compose global middleware using compose method', function * () { - Middleware.global(['App/Http/Middleware/Global']) - const request = {} - const response = {} - const resolved = Middleware.resolve([], true) - const compose = Middleware.compose(resolved, request, response) - yield compose() - expect(request.count).to.equal(2) - }) - - it('should abort request in between when middleware throws an error', function * () { - Middleware.global(['App/Http/Middleware/GlobalThrow', 'App/Http/Middleware/Parser']) - const request = {} - const response = {} - const resolved = Middleware.resolve([], true) - const compose = Middleware.compose(resolved, request, response) - try { - yield compose() - expect(true).to.equal(false) - } catch (e) { - expect(e.message).to.equal('Login') - expect(request.count).to.equal(undefined) - } - }) - - it('should call middleware one by one', function * () { - Middleware.global(['App/Http/Middleware/Parser', 'App/Http/Middleware/Cycle2']) - const request = {} - const response = {} - const resolved = Middleware.resolve([], true) - const compose = Middleware.compose(resolved, request, response) - yield compose() - expect(request.count).to.equal(1) - }) - - it('should pass parameters to the middleware', function * () { - Middleware.global(['App/Http/Middleware/Parser', 'App/Http/Middleware/Cycle2']) - Middleware.register('auth', 'App/Http/Middleware/AuthMiddleware') - const request = {} - const response = {} - const formatted = Middleware.formatNamedMiddleware(['auth:basic']) - const resolved = Middleware.resolve(formatted, true) - const compose = Middleware.compose(resolved, request, response) - yield compose() - expect(request.count).to.equal(1) - expect(request.scheme).to.equal('basic') - }) - - it('should be able to compose a closure attached to the middleware', function * () { - const request = {} - const response = {} - const middleware = function * (request, response) { - request.count = 1 - response.count = 1 - } - const compose = Middleware.compose([middleware], request, response) - yield compose() - expect(request.count).to.equal(1) - expect(response.count).to.equal(1) - }) -}) diff --git a/test/unit/package.another.json b/test/unit/package.another.json deleted file mode 100644 index 4d993ff0..00000000 --- a/test/unit/package.another.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "autoload": {} -} diff --git a/test/unit/package.test.json b/test/unit/package.test.json deleted file mode 100644 index 0db3279e..00000000 --- a/test/unit/package.test.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/test/unit/public/favicon.ico b/test/unit/public/favicon.ico deleted file mode 100644 index e69de29b..00000000 diff --git a/test/unit/public/style.css b/test/unit/public/style.css deleted file mode 100644 index 64a71844..00000000 --- a/test/unit/public/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body{ - background: #fff; -} diff --git a/test/unit/request.spec.js b/test/unit/request.spec.js deleted file mode 100644 index de61b4db..00000000 --- a/test/unit/request.spec.js +++ /dev/null @@ -1,974 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const chai = require('chai') -const expect = chai.expect -const Request = require('../../src/Request') -const http = require('http') -const File = require('../../src/File') -const https = require('https') -const supertest = require('co-supertest') -const path = require('path') -const pem = require('pem') -const formidable = require('formidable') - -const Config = { - get: function (key) { - switch (key) { - case 'http.trustProxy': - return true - case 'app.appKey': - return null - default: - return 2 - } - } -} - -require('co-mocha') - -describe('Request', function () { - it('should get request query string', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const query = request.get() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({query}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.query).to.be.an('object') - expect(res.body.query).deep.equal({name: 'foo'}) - }) - - it('should return empty object when request does not have query string', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const query = request.get() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({query}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.query).to.be.an('object') - expect(res.body.query).deep.equal({}) - }) - - it('should get request post data', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {name: 'foo'} - const body = request.post() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({body}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.body).to.be.an('object') - expect(res.body.body).deep.equal({name: 'foo'}) - }) - - it('should return empty object when post body does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const body = request.post() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({body}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.body).to.be.an('object') - expect(res.body.body).deep.equal({}) - }) - - it('should get value for a given key using input method', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const name = request.input('name') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({name}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.name).to.equal('foo') - }) - - it('should return null when value for input key is not available', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const name = request.input('name') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({name}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.name).to.equal(null) - }) - - it('should get nested value for a given key using input method', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const name = request.input('profile.name') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({name}), 'utf8') - }) - - const res = yield supertest(server).get('/?profile[name]=foo').expect(200).end() - expect(res.body.name).to.equal('foo') - }) - - it('should return default value when value for input key is not available', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const name = request.input('name', 'doe') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({name}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.name).to.equal('doe') - }) - - it('should return get and post values when using all', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {age: 22} - const all = request.all() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({all}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.all).deep.equal({name: 'foo', age: 22}) - }) - - it('should group and return an array of items', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {username: ['virk', 'aman', 'nikk'], email: ['virk@gmail.com', 'aman@gmail.com', 'nikk@gmail.com']} - const contacts = request.collect('username', 'email') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({contacts}), 'utf8') - }) - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.contacts).deep.equal([{username: 'virk', email: 'virk@gmail.com'}, {username: 'aman', email: 'aman@gmail.com'}, {username: 'nikk', email: 'nikk@gmail.com'}]) - }) - - it('should group and return null for fields not present inside the object', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {username: ['virk', 'aman', 'nikk']} - const contacts = request.collect('username', 'age') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({contacts}), 'utf8') - }) - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.contacts).deep.equal([{username: 'virk', age: null}, {username: 'aman', age: null}, {username: 'nikk', age: null}]) - }) - - it('should group and return null for fields not present inside the object at different order', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {username: ['virk', 'aman', 'nikk'], email: ['virk@foo.com', 'aman@foo.com', 'nikk@foo.com']} - const contacts = request.collect('name', 'email') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({contacts}), 'utf8') - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.contacts).deep.equal([{name: null, email: 'virk@foo.com'}, {name: null, email: 'aman@foo.com'}, {name: null, email: 'nikk@foo.com'}]) - }) - - it('should group and return null for fields not present inside the object in mix order', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {username: ['virk', 'aman', 'nikk'], email: ['virk@foo.com', 'aman@foo.com', 'nikk@foo.com'], password: ['vi', 'am', 'ni']} - const contacts = request.collect('password', 'name', 'username') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({contacts}), 'utf8') - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.contacts).deep.equal([{password: 'vi', name: null, username: 'virk'}, {password: 'am', name: null, username: 'aman'}, {password: 'ni', name: null, username: 'nikk'}]) - }) - - it('should return all values expect defined keys', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {age: 22} - const all = request.except('age') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({all}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.all).deep.equal({name: 'foo'}) - }) - - it('should return all values expect defined keys when defined as an array', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {age: 22} - const all = request.except(['age']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({all}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.all).deep.equal({name: 'foo'}) - }) - - it('should not return key/value pair for key that does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {age: 22} - const all = request.except(['foo']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({all}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.all).deep.equal({name: 'foo', age: 22}) - }) - - it('should return all values for only defined keys', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {age: 22} - const all = request.only('age') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({all}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.all).deep.equal({age: 22}) - }) - - it('should return all values for only defined keys when keys are defined as array', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {age: 22} - const all = request.only(['age']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({all}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.all).deep.equal({age: 22}) - }) - - it('should not return key/value pair for key that does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._body = {age: 22} - const all = request.only(['foo']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({all}), 'utf8') - }) - - const res = yield supertest(server).get('/?name=foo').expect(200).end() - expect(res.body.all).deep.equal({}) - }) - - it('should return all headers for a given request', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const headers = request.headers() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({headers}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('username', 'admin').expect(200).end() - expect(res.body.headers).to.have.property('username') - expect(res.body.headers.username).to.equal('admin') - }) - - it('should return header value for a given key', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const username = request.header('username') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({username}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('username', 'admin').expect(200).end() - expect(res.body.username).to.equal('admin') - }) - - it('should check for request freshness', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const fresh = request.fresh() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({fresh}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('if-none-match', '*').expect(200).end() - expect(res.body.fresh).to.equal(true) - }) - - it('should tell whether request is stale or not', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const stale = request.stale() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({stale}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('if-none-match', '*').expect(200).end() - expect(res.body.stale).to.equal(false) - }) - - it('should return best match for request ip address', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const ip = request.ip() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({ip}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.ip).to.match(/127\.0\.0\.1/) - }) - - it('should return all ip addresses from a given request', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const ips = request.ips() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({ips}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.ips).to.be.an('array') - }) - - it('should tell whether request is https or not', function (done) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' - - pem.createCertificate({days: 1, selfSigned: true}, function (err, keys) { - if (err) { done(err) } - const server = https.createServer({key: keys.serviceKey, cert: keys.certificate}, function (req, res) { - const request = new Request(req, res, Config) - const secure = request.secure() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({secure}), 'utf8') - }) - - supertest(server) - .get('/') - .expect(200) - .end(function (err, res) { - if (err) { - done(err) - return - } - expect(res.body.secure).to.equal(true) - server.close() - done() - }) - }) - }) - - it('should return request subdomains', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const subdomains = request.subdomains() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({subdomains}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('X-Forwarded-Host', 'virk.adonisjs.com').expect(200).end() - expect(res.body.subdomains).deep.equal(['virk']) - }) - - it('should tell whether request is ajax or not', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const ajax = request.ajax() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({ajax}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('X-Requested-With', 'xmlhttprequest').expect(200).end() - expect(res.body.ajax).to.equal(true) - }) - - it('should tell whether request is pjax or not', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const pjax = request.pjax() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({pjax}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('X-PJAX', true).expect(200).end() - expect(res.body.pjax).to.equal(true) - }) - - it('should return request host name', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const hostname = request.hostname() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({hostname}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.hostname).to.equal('127.0.0.1') - }) - - it('should return request url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const url = request.url() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({url}), 'utf8') - }) - - const res = yield supertest(server).get('/?query=string').expect(200).end() - expect(res.body.url).to.equal('/') - }) - - it('should return request original url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const originalUrl = request.originalUrl() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({originalUrl}), 'utf8') - }) - - const res = yield supertest(server).get('/?query=string').expect(200).end() - expect(res.body.originalUrl).to.equal('/?query=string') - }) - - it('should return request method', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const method = request.method() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({method}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.method).to.equal('GET') - }) - - it('should tell whether request is of certain type', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const isHtml = request.is('html') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({isHtml}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Content-type', 'text/html').expect(200).end() - expect(res.body.isHtml).to.equal(true) - }) - - it('should tell whether request is of certain type when an array of options have been passed', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const isHtml = request.is(['json', 'html']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({isHtml}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Content-type', 'text/html').expect(200).end() - expect(res.body.isHtml).to.equal(true) - }) - - it('should tell whether request is of certain type when multiple arguments have been passed', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const isHtml = request.is('json', 'javascript', 'html') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({isHtml}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Content-type', 'text/html').expect(200).end() - expect(res.body.isHtml).to.equal(true) - }) - - it('should tell best response type request will accept', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const html = request.accepts('html') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({html}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Accept', 'text/html').expect(200).end() - expect(res.body.html).to.equal('html') - }) - - it('should tell best response type request will accept when an array of options have been passed', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const html = request.accepts(['json', 'html']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({html}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Accept', 'text/html').expect(200).end() - expect(res.body.html).to.equal('html') - }) - - it('should tell best response type request will accept when multiple arguments have been passed', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const html = request.accepts('json', 'javascript', 'html') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({html}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Accept', 'text/html').expect(200).end() - expect(res.body.html).to.equal('html') - }) - - it('should return request cookies', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const cookies = request.cookies() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({cookies}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Cookie', ['name=foo']).expect(200).end() - expect(res.body.cookies).deep.equal({name: 'foo'}) - }) - - it('should not reparse cookies after calling cookies method multiple times', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request.cookies() - request.cookiesObject.age = 22 - const cookiesAgain = request.cookies() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({cookies: cookiesAgain}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Cookie', ['name=foo']).expect(200).end() - expect(res.body.cookies).deep.equal({name: 'foo', age: 22}) - }) - - it('should return cookie value for a given key', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const name = request.cookie('name') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({name}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Cookie', ['name=foo']).expect(200).end() - expect(res.body.name).to.equal('foo') - }) - - it('should return null when cookie value for a given key does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const age = request.cookie('age') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({age}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Cookie', ['name=foo']).expect(200).end() - expect(res.body.age).to.equal(null) - }) - - it('should return default value when cookie value for a given key does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const age = request.cookie('age', 18) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({age}), 'utf8') - }) - - const res = yield supertest(server).get('/').set('Cookie', ['name=foo']).expect(200).end() - expect(res.body.age).to.equal(18) - }) - - it('should return route params', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._params = {id: 1} - const params = request.params() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({params}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.params).deep.equal({id: 1}) - }) - - it('should return empty object when request params does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const params = request.params() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({params}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.params).deep.equal({}) - }) - - it('should return request param value for a given key', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._params = {id: 1} - const id = request.param('id') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({id}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.id).to.equal(1) - }) - - it('should return null when param value for a given key does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._params = {id: 1} - const name = request.param('name') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({name}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.name).to.equal(null) - }) - - it('should return default value when param value for a given key does not exists', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._params = {id: 1} - const name = request.param('name', 'bar') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({name}), 'utf8') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.name).to.equal('bar') - }) - - it('should return an uploaded file as an instance of File object', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm() - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const file = request.file('logo') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({file: file instanceof File}), 'utf8') - }) - }) - const res = yield supertest(server).get('/').attach('logo', path.join(__dirname, '/uploads/npm-logo.svg')).expect(200).end() - expect(res.body.file).to.equal(true) - }) - - it('should return an array of uploaded files instances when multiple files are uploaded', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm({multiples: true}) - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const logos = request.file('logo[]') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({logo1: logos[0] instanceof File, logo2: logos[1] instanceof File}), 'utf8') - }) - }) - const res = yield supertest(server).get('/') - .attach('logo[]', path.join(__dirname, '/uploads/npm-logo.svg')) - .attach('logo[]', path.join(__dirname, '/uploads/npm-logo.svg')) - .expect(200) - .end() - - expect(res.body.logo1).to.equal(true) - expect(res.body.logo2).to.equal(true) - }) - - it('should be able to define max size for a given file', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm({multiples: true}) - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const logo = request.file('logo', {maxSize: '1kb'}) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8') - }) - }) - - const res = yield supertest(server).get('/') - .attach('logo', path.join(__dirname, '/uploads/npm-logo.svg')) - .expect(200) - .end() - expect(res.body.logo.maxSize).to.equal(1024) - }) - - it('should be able to define allowed extensions for a given file', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm({multiples: true}) - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const logo = request.file('logo', {allowedExtensions: ['jpg']}) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8') - }) - }) - - const res = yield supertest(server).get('/') - .attach('logo', path.join(__dirname, '/uploads/npm-logo.svg')) - .expect(200) - .end() - expect(res.body.logo.allowedExtensions).deep.equal(['jpg']) - }) - - it('should return error when trying to move a file of larger size', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm({multiples: true}) - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const logo = request.file('logo', {maxSize: '100b'}) - logo - .move() - .then(() => { - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8') - }) - }) - }) - - const res = yield supertest(server).get('/') - .attach('logo', path.join(__dirname, '/uploads/npm-logo.svg')) - .expect(200) - .end() - expect(res.body.logo.error).to.equal('Uploaded file size 235B exceeds the limit of 100B') - }) - - it('should return error when trying to move a file of invalid extension', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm({multiples: true}) - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const logo = request.file('logo', {allowedExtensions: ['jpg']}) - logo - .move() - .then(() => { - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({logo: logo.toJSON()}), 'utf8') - }) - }) - }) - - const res = yield supertest(server).get('/') - .attach('logo', path.join(__dirname, '/uploads/npm-logo.svg')) - .expect(200) - .end() - expect(res.body.logo.error).to.equal('Uploaded file extension svg is not valid') - }) - - it('should return true when a pattern matches the current route url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const matches = request.match('/user/:id/profile') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({matches}), 'utf8') - }) - - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.matches).to.equal(true) - }) - - it('should return false when a pattern does not matches the current route url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const matches = request.match('/user') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({matches}), 'utf8') - }) - - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.matches).to.equal(false) - }) - - it('should return true when any of the paths inside array matches the current route url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const matches = request.match(['/user', '/user/1/profile']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({matches}), 'utf8') - }) - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.matches).to.equal(true) - }) - - it('should return false when none of the paths inside array matches the current route url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const matches = request.match(['/user', '/user/1', '/1/profile']) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({matches}), 'utf8') - }) - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.matches).to.equal(false) - }) - - it('should return true when any of the paths from any of the arguments matches the current route url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const matches = request.match('/user', '/user/1/profile') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({matches}), 'utf8') - }) - - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.matches).to.equal(true) - }) - - it('should return false when any of the paths from any of the arguments does not matches the current route url', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const matches = request.match('/user', '/user/1', '/user/profile') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({matches}), 'utf8') - }) - - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.matches).to.equal(false) - }) - - it('should return false when request does not have body', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const hasBody = request.hasBody() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({hasBody}), 'utf8') - }) - - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.hasBody).to.equal(false) - }) - - it('should return true when request has body', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - const hasBody = request.hasBody() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({hasBody}), 'utf8') - }) - - const res = yield supertest(server).get('/user/1/profile').send('name', 'doe').expect(200).end() - expect(res.body.hasBody).to.equal(true) - }) - - it('should return request format using format method', function * () { - const server = http.createServer(function (req, res) { - const request = new Request(req, res, Config) - request._params = {format: '.json'} - const format = request.format() - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({format}), 'utf8') - }) - - const res = yield supertest(server).get('/user/1/profile').expect(200).end() - expect(res.body.format).to.equal('json') - }) - - it('should return an null when file is not uploaded', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm() - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const file = request.file('logo') - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({file}), 'utf8') - }) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.file).to.equal(null) - }) - - it('should return all uploaded file as an instance of File object', function * () { - const server = http.createServer(function (req, res) { - var form = new formidable.IncomingForm() - const request = new Request(req, res, Config) - form.parse(req, function (err, fields, files) { - if (err) { - res.writeHead(500, {'Content-type': 'application/json'}) - res.send(JSON.stringify({error: err.message})) - return - } - request._files = files - const allFiles = request.files() - const isInstances = [] - allFiles.forEach(function (file) { - isInstances.push(file instanceof File) - }) - res.writeHead(200, {'Content-type': 'application/json'}) - res.end(JSON.stringify({isInstances}), 'utf8') - }) - }) - const res = yield supertest(server).get('/').attach('logo', path.join(__dirname, '/uploads/npm-logo.svg')).attach('favicon', path.join(__dirname, '/public/favicon.ico')).expect(200).end() - expect(res.body.isInstances).deep.equal([true, true]) - }) - - it('should be able to add macro to the request prototype', function () { - Request.macro('foo', function () { - return 'foo' - }) - const request = new Request({}, {}, {get: function () {}}) - expect(request.foo()).to.equal('foo') - }) - - it('should have access to instance inside the callback', function * () { - Request.macro('foo', function () { - return this.request.name - }) - const request = new Request({name: 'bar'}, {}, {get: function () {}}) - expect(request.foo()).to.equal('bar') - }) -}) diff --git a/test/unit/response.spec.js b/test/unit/response.spec.js deleted file mode 100644 index 4620659c..00000000 --- a/test/unit/response.spec.js +++ /dev/null @@ -1,404 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const chai = require('chai') -const expect = chai.expect -const Request = require('../../src/Request') -const ResponseBuilder = require('../../src/Response') -const Route = require('../../src/Route') -const View = require('../../src/View') -const http = require('http') -const path = require('path') -const co = require('co') -const supertest = require('co-supertest') - -require('co-mocha') - -const Config = { - get: function (key) { - switch (key) { - case 'app.views.cache': - return true - case 'app.http.jsonpCallback': - return 'callback' - case 'app.http.setPoweredBy': - return true - default: - true - } - } -} - -describe('Response', function () { - before(function () { - const Helpers = { - viewsPath: function () { - return path.join(__dirname, './app/views') - } - } - - const view = new View(Helpers, Config, Route) - this.Response = new ResponseBuilder(view, Route, Config) - }) - - beforeEach(function () { - Route.new() - }) - - it('should respond to a request using send method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.send('Hello world') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.text).to.equal('Hello world') - }) - - it('should make use of descriptive methods exposed by nodeRes', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.ok('Hello world') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.text).to.equal('Hello world') - }) - - it('should return 401 using unauthorized method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.unauthorized('Login first') - }) - - const res = yield supertest(server).get('/').expect(401).end() - expect(res.text).to.equal('Login first') - }) - - it('should return 500 using internalServerError method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.internalServerError('Error first') - }) - const res = yield supertest(server).get('/').expect(500).end() - expect(res.text).to.equal('Error first') - }) - - it('should set header on response', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.header('country', 'India').send('') - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers.country).to.equal('India') - }) - - it('should remove existing from request', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.removeHeader('country', 'India').send('') - }) - - const res = yield supertest(server).get('/').set('country', 'India').expect(200).end() - expect(res.headers.country).to.equal(undefined) - }) - - it('should make json response using json method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.json({name: 'foo'}) - }) - - const res = yield supertest(server).get('/').expect(200).expect('Content-type', /json/).end() - expect(res.body).deep.equal({name: 'foo'}) - }) - - it('should make jsonp response using jsonp method with correct callback', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.jsonp({name: 'foo'}) - }) - - const res = yield supertest(server).get('/?callback=angular').expect(200).expect('Content-type', /javascript/).end() - expect(res.text).to.match(/typeof angular/) - }) - - it('should make jsonp response using jsonp default callback when callback is missing in query string', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.jsonp({name: 'foo'}) - }) - - const res = yield supertest(server).get('/').expect(200).expect('Content-type', /javascript/).end() - expect(res.text).to.match(/typeof callback/) - }) - - it('should set request status', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.status(304).json({name: 'foo'}) - }) - yield supertest(server).get('/').expect(304).end() - }) - - it('should download a given file using its path', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.download(path.join(__dirname, './public/style.css')) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.text).to.match(/(?:\s*\S+\s*{[^}]*})+/g) - }) - - it('should force download a given file using its path and by setting content-disposition header', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.attachment(path.join(__dirname, './public/style.css')) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers['content-disposition']).to.equal('attachment; filename="style.css"') - }) - - it('should force download a given file using its path but with different name', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.attachment(path.join(__dirname, './public/style.css'), 'production.css') - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers['content-disposition']).to.equal('attachment; filename="production.css"') - }) - - it('should set location header on response', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.location('http://amanvirk.me/').send('') - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers.location).to.equal('http://amanvirk.me/') - }) - - it('should set location header to referrer on response', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.location('back').send('') - }) - const res = yield supertest(server).get('/').set('Referrer', '/foo').expect(200).end() - expect(res.headers.location).to.equal('/foo') - }) - - it('should set location header to / when there is no referrer on request', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.location('back').send('') - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers.location).to.equal('/') - }) - - it('should set location header on response using redirect method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.redirect('http://amanvirk.me/') - }) - const res = yield supertest(server).get('/').expect(302).end() - expect(res.headers.location).to.equal('http://amanvirk.me/') - }) - - it('should set location header to referrer when using back with redirect method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.redirect('back') - }) - const res = yield supertest(server).get('/').set('Referrer', '/bar').expect(302).end() - expect(res.headers.location).to.equal('/bar') - }) - - it('should set location header to / when there is no referrer defined using redirect method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.redirect('back') - }) - const res = yield supertest(server).get('/').expect(302).end() - expect(res.headers.location).to.equal('/') - }) - - it('should redirect to a given route using route method', function * () { - Route.get('/user/:id', function * () {}).as('profile') - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.route('profile', {id: 1}) - }) - const res = yield supertest(server).get('/').expect(302).end() - expect(res.headers.location).to.equal('/user/1') - }) - - it('should redirect to a given route using route method when it is under a domain', function * () { - Route.group('g', function () { - Route.get('/user/:id', function * () {}).as('profile') - }).domain('virk.adonisjs.com') - - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.route('profile', {id: 1}) - }) - const res = yield supertest(server).get('/').expect(302).end() - expect(res.headers.location).to.equal('virk.adonisjs.com/user/1') - }) - - it('should add vary field to response headers', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.vary('Accepts').send('') - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers.vary).to.equal('Accepts') - }) - - it('should set response cookie using cookie method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.cookie('name', 'virk').end() - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers['set-cookie']).deep.equal(['name=virk']) - }) - - it('should make a view using response view method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - co(function * () { - return yield response.view('index') - }).then(function (responseView) { - response.send(responseView) - }).catch(function (err) { - response.status(200).send(err) - }) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.text.trim()).to.equal('

Hello world

') - }) - - it('should immediately send a view using response sendView method', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - co(function * () { - yield response.sendView('index') - }).catch(function (err) { - response.status(200).send(err) - }) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.text.trim()).to.equal('

Hello world

') - }) - - it('should set X-Powered-By when enabled inside app.http config', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const response = new this.Response(request, res) - response.send() - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers).to.have.property('x-powered-by') - }) - - it('should not set X-Powered-By when not enabled inside app.http config', function * () { - const server = http.createServer((req, res) => { - const Config = { - get: function () { - return false - } - } - const request = new Request(req, res, Config) - const Response = new ResponseBuilder({}, Route, Config) - const response = new Response(request, res) - response.send() - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.headers).not.have.property('x-powered-by') - }) - - it('should be able to add macro to the response prototype', function () { - const Response = new ResponseBuilder({}, Route, Config) - Response.macro('foo', function () { - return 'foo' - }) - const response = new Response({}, {setHeader: function () {}}) - expect(response.foo()).to.equal('foo') - }) - - it('should have access to instance inside the callback', function * () { - const Response = new ResponseBuilder({}, Route, Config) - Response.macro('foo', function () { - return this.request.name - }) - const response = new Response({name: 'bar'}, {setHeader: function () {}}) - expect(response.foo()).to.equal('bar') - }) - - it('should return true for isPending when request has not been ended', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const Response = new ResponseBuilder({}, Route, Config) - const response = new Response(request, res) - const isPending = response.isPending - response.send({isPending}) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.isPending).to.equal(true) - }) - - it('should return false for finished when request has not been ended', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const Response = new ResponseBuilder({}, Route, Config) - const response = new Response(request, res) - const finished = response.finished - response.send({finished}) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.finished).to.equal(false) - }) - - it('should return false for headersSent when request has not been ended', function * () { - const server = http.createServer((req, res) => { - const request = new Request(req, res, Config) - const Response = new ResponseBuilder({}, Route, Config) - const response = new Response(request, res) - const headersSent = response.headersSent - response.send({headersSent}) - }) - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.headersSent).to.equal(false) - }) -}) diff --git a/test/unit/route.spec.js b/test/unit/route.spec.js deleted file mode 100644 index e479a1ea..00000000 --- a/test/unit/route.spec.js +++ /dev/null @@ -1,1178 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const Route = require('../../src/Route') -const chai = require('chai') -const _ = require('lodash') -const expect = chai.expect -const stderr = require('test-console').stderr -require('co-mocha') - -describe('Route', function () { - beforeEach(function () { - Route.new() - }) - - context('Register', function () { - it('should register a route with GET verb', function () { - Route.get('/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['GET', 'HEAD']) - }) - - it('should register a route with POST verb', function () { - Route.post('/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['POST']) - }) - - it('should add / when route defination does not have one', function () { - Route.post('admin', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].route).to.equal('/admin') - }) - - it('should register a route with PUT verb', function () { - Route.put('/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['PUT']) - }) - - it('should register a route with DELETE verb', function () { - Route.delete('/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['DELETE']) - }) - - it('should register a route with PATCH verb', function () { - Route.patch('/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['PATCH']) - }) - - it('should register a route with OPTIONS verb', function () { - Route.options('/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['OPTIONS']) - }) - - it('should register a route with multiple verbs using match method', function () { - Route.match(['get', 'post'], '/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['GET', 'POST']) - }) - - it('should register a route for all verbs using any method', function () { - Route.any('/', 'SomeController.method') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].handler).to.equal('SomeController.method') - expect(routes[0].verb).deep.equal(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) - }) - - it('should throw an error when handler binded to resource is not a controller', function () { - const fn = function () { - Route.resource('/', function * () {}) - } - expect(fn).to.throw('InvalidArgumentException: E_INVALID_PARAMETER: You can only bind controllers to resources') - }) - - it('should log warning when trying to bind resource to the base route', function () { - const inspect = stderr.inspect() - Route.resource('/', 'HomeController') - inspect.restore() - expect(inspect.output[inspect.output.length - 2].trim()).to.match(/You are registering a resource for \/ path, which is not a good practice/) - }) - - it("should be able to get a route with it's name", function () { - Route.get('/user/:id', 'UsersController.show').as('user.show') - const route = Route.getRoute({name: 'user.show'}) - expect(route).to.be.an('object') - expect(route.handler).to.equal('UsersController.show') - }) - - it("should be able to get a route with it's handler name", function () { - Route.get('/user/:id', 'UsersController.show').as('user.show') - const route = Route.getRoute({handler: 'UsersController.show'}) - expect(route).to.be.an('object') - expect(route.name).to.equal('user.show') - }) - - it('should register resourceful routes', function () { - Route.resource('/', 'SomeController') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(7) - expect(verbs['/-GET/HEAD']).to.equal('SomeController.index') - expect(verbs['/create-GET/HEAD']).to.equal('SomeController.create') - expect(verbs['/-POST']).to.equal('SomeController.store') - expect(verbs['/:id-GET/HEAD']).to.equal('SomeController.show') - expect(verbs['/:id/edit-GET/HEAD']).to.equal('SomeController.edit') - expect(verbs['/:id-PUT/PATCH']).to.equal('SomeController.update') - expect(verbs['/:id-DELETE']).to.equal('SomeController.destroy') - }) - - it('should not have / in resourceful routes', function () { - Route.resource('/', 'SomeController') - const routes = Route.routes() - const names = _.map(routes, function (route) { - return route.name - }) - expect(routes.length).to.equal(7) - expect(names).deep.equal(['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']) - }) - - it('should register resourceful routes when base route is not /', function () { - Route.resource('/admin', 'SomeController') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(7) - expect(verbs['/admin-GET/HEAD']).to.equal('SomeController.index') - expect(verbs['/admin/create-GET/HEAD']).to.equal('SomeController.create') - expect(verbs['/admin-POST']).to.equal('SomeController.store') - expect(verbs['/admin/:id-GET/HEAD']).to.equal('SomeController.show') - expect(verbs['/admin/:id/edit-GET/HEAD']).to.equal('SomeController.edit') - expect(verbs['/admin/:id-DELETE']).to.equal('SomeController.destroy') - }) - - it('should not have / in resourceful routes when resourceful is not /', function () { - Route.resource('/admin', 'SomeController') - const routes = Route.routes() - const names = _.map(routes, function (route) { - return route.name - }) - expect(routes.length).to.equal(7) - expect(names).deep.equal(['admin.index', 'admin.create', 'admin.store', 'admin.show', 'admin.edit', 'admin.update', 'admin.destroy']) - }) - - it('should not have / in resourceful routes when resourceful does not starts with /', function () { - Route.resource('admin', 'SomeController') - const routes = Route.routes() - const names = _.map(routes, function (route) { - return route.name - }) - expect(routes.length).to.equal(7) - expect(names).deep.equal(['admin.index', 'admin.create', 'admin.store', 'admin.show', 'admin.edit', 'admin.update', 'admin.destroy']) - }) - - it('should be able to name routes', function () { - Route.any('/', 'SomeController.method').as('home') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].name).to.equal('home') - }) - - it('should be able to attach middlewares to a given route', function () { - Route.any('/', 'SomeController.method').middlewares(['auth']) - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].middlewares).deep.equal(['auth']) - }) - - it('should be able to attach middlewares as multiple parameters', function () { - Route.any('/', 'SomeController.method').middlewares('auth', 'web') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].middlewares).deep.equal(['auth', 'web']) - }) - - it('should be able to attach middlewares using middleware method', function () { - Route.any('/', 'SomeController.method').middleware('auth', 'web') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].middlewares).deep.equal(['auth', 'web']) - }) - - it('should be able to group routes', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }) - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].group).to.equal('admin') - }) - - it('should be able to attach middleware to group routes', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).middlewares(['auth']) - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].group).to.equal('admin') - expect(routes[0].middlewares).deep.equal(['auth']) - }) - - it('should be able to attach middlewares as multiple parameters on a group', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).middlewares('auth', 'web') - - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].middlewares).deep.equal(['auth', 'web']) - }) - - it('should be able to attach middlewares using middleware method', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).middleware('auth', 'web') - - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].middlewares).deep.equal(['auth', 'web']) - }) - - it('should be able to attach middleware to group routes and isolated middleware to routes inside group', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - Route.get('/cors', 'SomeController.method').middlewares(['cors']) - }).middlewares(['auth']) - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].group).to.equal('admin') - expect(routes[0].middlewares).deep.equal(['auth']) - expect(routes[1].group).to.equal('admin') - expect(routes[1].middlewares).deep.equal(['cors', 'auth']) - }) - - it('should be able to prefix routes inside a group', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).prefix('/v1') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].route).to.equal('/v1') - }) - - it('should prefix all resourceful routes under a group', function () { - Route.group('v1', function () { - Route.resource('admin', 'SomeController') - }).prefix('/v1') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(7) - expect(verbs['/v1/admin-GET/HEAD']).to.equal('SomeController.index') - expect(verbs['/v1/admin/create-GET/HEAD']).to.equal('SomeController.create') - expect(verbs['/v1/admin-POST']).to.equal('SomeController.store') - expect(verbs['/v1/admin/:id-GET/HEAD']).to.equal('SomeController.show') - expect(verbs['/v1/admin/:id/edit-GET/HEAD']).to.equal('SomeController.edit') - expect(verbs['/v1/admin/:id-PUT/PATCH']).to.equal('SomeController.update') - expect(verbs['/v1/admin/:id-DELETE']).to.equal('SomeController.destroy') - }) - - it('should be able to create nested resources seperated with dots', function () { - Route.resource('user.posts', 'PostController') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(7) - expect(verbs['/user/:user_id/posts-GET/HEAD']).to.equal('PostController.index') - expect(verbs['/user/:user_id/posts/create-GET/HEAD']).to.equal('PostController.create') - expect(verbs['/user/:user_id/posts-POST']).to.equal('PostController.store') - expect(verbs['/user/:user_id/posts/:id-GET/HEAD']).to.equal('PostController.show') - expect(verbs['/user/:user_id/posts/:id/edit-GET/HEAD']).to.equal('PostController.edit') - expect(verbs['/user/:user_id/posts/:id-PUT/PATCH']).to.equal('PostController.update') - expect(verbs['/user/:user_id/posts/:id-DELETE']).to.equal('PostController.destroy') - }) - - it('should create proper names for nested routes', function () { - Route.resource('user.posts', 'SomeController') - const routes = Route.routes() - const names = _.map(routes, function (route) { - return route.name - }) - expect(routes.length).to.equal(7) - expect(names).deep.equal(['user.posts.index', 'user.posts.create', 'user.posts.store', 'user.posts.show', 'user.posts.edit', 'user.posts.update', 'user.posts.destroy']) - }) - - it('should be able to create end number of nested resources seperated with dots', function () { - Route.resource('user.post.comments', 'CommentsController') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(7) - expect(verbs['/user/:user_id/post/:post_id/comments-GET/HEAD']).to.equal('CommentsController.index') - expect(verbs['/user/:user_id/post/:post_id/comments/create-GET/HEAD']).to.equal('CommentsController.create') - expect(verbs['/user/:user_id/post/:post_id/comments-POST']).to.equal('CommentsController.store') - expect(verbs['/user/:user_id/post/:post_id/comments/:id-GET/HEAD']).to.equal('CommentsController.show') - expect(verbs['/user/:user_id/post/:post_id/comments/:id/edit-GET/HEAD']).to.equal('CommentsController.edit') - expect(verbs['/user/:user_id/post/:post_id/comments/:id-PUT/PATCH']).to.equal('CommentsController.update') - expect(verbs['/user/:user_id/post/:post_id/comments/:id-DELETE']).to.equal('CommentsController.destroy') - }) - - it('should be define same resource under a group and without a group', function () { - Route.resource('users', 'UsersController') - Route.group('v', function () { - Route.resource('users', 'V1UsersController') - }).prefix('/v1') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(14) - expect(verbs['/users-GET/HEAD']).to.equal('UsersController.index') - expect(verbs['/v1/users-GET/HEAD']).to.equal('V1UsersController.index') - - expect(verbs['/users/create-GET/HEAD']).to.equal('UsersController.create') - expect(verbs['/v1/users/create-GET/HEAD']).to.equal('V1UsersController.create') - - expect(verbs['/users-POST']).to.equal('UsersController.store') - expect(verbs['/v1/users-POST']).to.equal('V1UsersController.store') - - expect(verbs['/users/:id-GET/HEAD']).to.equal('UsersController.show') - expect(verbs['/v1/users/:id-GET/HEAD']).to.equal('V1UsersController.show') - - expect(verbs['/users/:id/edit-GET/HEAD']).to.equal('UsersController.edit') - expect(verbs['/v1/users/:id/edit-GET/HEAD']).to.equal('V1UsersController.edit') - - expect(verbs['/users/:id-DELETE']).to.equal('UsersController.destroy') - expect(verbs['/v1/users/:id-DELETE']).to.equal('V1UsersController.destroy') - }) - - it('should be define same resource under a group and without a group binded to same controller', function () { - Route.resource('users', 'UsersController') - Route.group('v', function () { - Route.resource('users', 'UsersController') - }).prefix('/v1') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(14) - expect(verbs['/users-GET/HEAD']).to.equal('UsersController.index') - expect(verbs['/v1/users-GET/HEAD']).to.equal('UsersController.index') - - expect(verbs['/users/create-GET/HEAD']).to.equal('UsersController.create') - expect(verbs['/v1/users/create-GET/HEAD']).to.equal('UsersController.create') - - expect(verbs['/users-POST']).to.equal('UsersController.store') - expect(verbs['/v1/users-POST']).to.equal('UsersController.store') - - expect(verbs['/users/:id-GET/HEAD']).to.equal('UsersController.show') - expect(verbs['/v1/users/:id-GET/HEAD']).to.equal('UsersController.show') - - expect(verbs['/users/:id/edit-GET/HEAD']).to.equal('UsersController.edit') - expect(verbs['/v1/users/:id/edit-GET/HEAD']).to.equal('UsersController.edit') - - expect(verbs['/users/:id-PUT/PATCH']).to.equal('UsersController.update') - - expect(verbs['/users/:id-DELETE']).to.equal('UsersController.destroy') - expect(verbs['/v1/users/:id-DELETE']).to.equal('UsersController.destroy') - }) - - it('all route resources should have a name', function () { - Route.resource('user.posts', 'PostController') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - expect(routes.length).to.equal(7) - expect(verbs['/user/:user_id/posts-GET/HEAD']).to.equal('user.posts.index') - expect(verbs['/user/:user_id/posts/create-GET/HEAD']).to.equal('user.posts.create') - expect(verbs['/user/:user_id/posts-POST']).to.equal('user.posts.store') - expect(verbs['/user/:user_id/posts/:id-GET/HEAD']).to.equal('user.posts.show') - expect(verbs['/user/:user_id/posts/:id/edit-GET/HEAD']).to.equal('user.posts.edit') - expect(verbs['/user/:user_id/posts/:id-PUT/PATCH']).to.equal('user.posts.update') - expect(verbs['/user/:user_id/posts/:id-DELETE']).to.equal('user.posts.destroy') - }) - - it('should be able to override route resource names', function () { - Route.resource('user.posts', 'PostController').as({ - edit: 'post.showEdit', - destroy: 'post.remove' - }) - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - expect(routes.length).to.equal(7) - expect(verbs['/user/:user_id/posts-GET/HEAD']).to.equal('user.posts.index') - expect(verbs['/user/:user_id/posts/create-GET/HEAD']).to.equal('user.posts.create') - expect(verbs['/user/:user_id/posts-POST']).to.equal('user.posts.store') - expect(verbs['/user/:user_id/posts/:id-GET/HEAD']).to.equal('user.posts.show') - expect(verbs['/user/:user_id/posts/:id/edit-GET/HEAD']).to.equal('post.showEdit') - expect(verbs['/user/:user_id/posts/:id-PUT/PATCH']).to.equal('user.posts.update') - expect(verbs['/user/:user_id/posts/:id-DELETE']).to.equal('post.remove') - }) - - it('should be able to define route required routes for a resource', function () { - Route.resource('user.posts', 'PostController').only(['create', 'store', 'index']) - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - expect(routes.length).to.equal(3) - expect(verbs['/user/:user_id/posts-GET/HEAD']).to.equal('user.posts.index') - expect(verbs['/user/:user_id/posts/create-GET/HEAD']).to.equal('user.posts.create') - expect(verbs['/user/:user_id/posts-POST']).to.equal('user.posts.store') - expect(verbs['/user/:user_id/posts/:id-GET/HEAD']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/:id/edit-GET/HEAD']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/:id-PUT/PATCH']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/:id-DELETE']).to.equal(undefined) - }) - - it('should be able to define route required routes for a resource as multiple parameters', function () { - Route.resource('user.posts', 'PostController').only('create', 'store', 'index') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - expect(routes.length).to.equal(3) - expect(verbs['/user/:user_id/posts-GET/HEAD']).to.equal('user.posts.index') - expect(verbs['/user/:user_id/posts/create-GET/HEAD']).to.equal('user.posts.create') - expect(verbs['/user/:user_id/posts-POST']).to.equal('user.posts.store') - expect(verbs['/user/:user_id/posts/:id-GET/HEAD']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/:id/edit-GET/HEAD']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/:id-PUT/PATCH']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/:id-DELETE']).to.equal(undefined) - }) - - it('should be able to define route actions not required when creating resources', function () { - Route.resource('user.posts', 'PostController').except(['create', 'store', 'index']) - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - expect(routes.length).to.equal(4) - expect(verbs['/user/:user_id/posts-GET/HEAD']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/create-GET/HEAD']).to.equal(undefined) - expect(verbs['/user/:user_id/posts-POST']).to.equal(undefined) - expect(verbs['/user/:user_id/posts/:id-GET/HEAD']).to.equal('user.posts.show') - expect(verbs['/user/:user_id/posts/:id/edit-GET/HEAD']).to.equal('user.posts.edit') - expect(verbs['/user/:user_id/posts/:id-PUT/PATCH']).to.equal('user.posts.update') - expect(verbs['/user/:user_id/posts/:id-DELETE']).to.equal('user.posts.destroy') - }) - - it('should filter routes from resource routes copy also when using except', function () { - Route - .resource('user.posts', 'PostController') - .except(['create', 'store', 'index']) - .as({ - store: 'users.save' - }) - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - expect(verbs['/user/:user_id/posts-POST']).to.equal(undefined) - }) - - it('should be able to define domain for a given route', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).domain('v1.example.org') - const routes = Route.routes() - expect(routes[0]).to.be.an('object') - expect(routes[0].domain).to.equal('v1.example.org') - }) - - it('should be able to define formats on routes', function () { - Route.get('/', 'HomeController.index').formats(['json']) - const routes = Route.routes() - expect(routes[0].route).to.equal('/:format(.json)?') - }) - - it('should be able to define multiple formats on routes', function () { - Route.get('/', 'HomeController.index').formats(['json', 'xml']) - const routes = Route.routes() - expect(routes[0].route).to.equal('/:format(.json|.xml)?') - }) - - it('should be able to define formats on routes and make format strict', function () { - Route.get('/', 'HomeController.index').formats(['json'], true) - const routes = Route.routes() - expect(routes[0].route).to.equal('/:format(.json)') - }) - - it('should be able to define formats on group of routes', function () { - Route.group('v2', function () { - Route.get('/users', 'UsersController.index') - Route.get('/posts', 'PostController.index') - }).formats(['json']) - const routes = Route.routes() - expect(routes[0].route).to.equal('/users:format(.json)?') - expect(routes[1].route).to.equal('/posts:format(.json)?') - }) - - it('should be able to define strict formats on group of routes', function () { - Route.group('v2', function () { - Route.get('/users', 'UsersController.index') - Route.get('/posts', 'PostController.index') - }).formats(['json'], true) - const routes = Route.routes() - expect(routes[0].route).to.equal('/users:format(.json)') - expect(routes[1].route).to.equal('/posts:format(.json)') - }) - - it('should be able to define formats on resources', function () { - Route.resource('users', 'UsersController').formats(['json']) - const routes = Route.routes() - const routePairs = _.map(routes, function (route) { - return route.route - }) - expect(routePairs.length).to.equal(7) - routePairs.forEach(function (item) { - expect(item).to.match(/:format(json)?/g) - }) - }) - - it('should register resourceful routes with member paths', function () { - Route - .resource('/tasks', 'SomeController') - .addMember('completed', ['GET', 'HEAD']) - .addMember('mark_as', 'POST') - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(9) - expect(verbs['/tasks-GET/HEAD']).to.equal('SomeController.index') - expect(verbs['/tasks/create-GET/HEAD']).to.equal('SomeController.create') - expect(verbs['/tasks-POST']).to.equal('SomeController.store') - expect(verbs['/tasks/:id-GET/HEAD']).to.equal('SomeController.show') - expect(verbs['/tasks/:id/edit-GET/HEAD']).to.equal('SomeController.edit') - expect(verbs['/tasks/:id-PUT/PATCH']).to.equal('SomeController.update') - expect(verbs['/tasks/:id-DELETE']).to.equal('SomeController.destroy') - expect(verbs['/tasks/:id/completed-GET/HEAD']).to.equal('SomeController.completed') - expect(verbs['/tasks/:id/mark_as-POST']).to.equal('SomeController.mark_as') - }) - - it('should be able to add member paths to nested resources', function () { - Route - .resource('user.tasks', 'SomeController') - .addMember('completed', 'PUT') - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(verbs['/user/:user_id/tasks/:id/completed-PUT']).to.equal('SomeController.completed') - }) - - it('should make use of GET and HEAD verbs when no verbs are defined with addMember', function () { - Route - .resource('/tasks', 'SomeController') - .addMember('completed') - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(verbs['/tasks/:id/completed-GET/HEAD']).to.equal('SomeController.completed') - }) - - it('should throw an error when the action is not present for member route', function () { - const fn = function () { - Route.resource('/tasks', 'SomeController').addMember() - } - expect(fn).to.throw('InvalidArgumentException: E_INVALID_PARAMETER: Resource.addMember expects a route') - }) - - it('should update controller binding for added member', function () { - Route - .resource('/tasks', 'SomeController') - .addMember('completed', ['GET', 'HEAD'], function (member) { - member.bindAction('SomeController.getCompleted') - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(routes.length).to.equal(8) - expect(verbs['/tasks-GET/HEAD']).to.equal('SomeController.index') - expect(verbs['/tasks/create-GET/HEAD']).to.equal('SomeController.create') - expect(verbs['/tasks-POST']).to.equal('SomeController.store') - expect(verbs['/tasks/:id-GET/HEAD']).to.equal('SomeController.show') - expect(verbs['/tasks/:id/edit-GET/HEAD']).to.equal('SomeController.edit') - expect(verbs['/tasks/:id-PUT/PATCH']).to.equal('SomeController.update') - expect(verbs['/tasks/:id-DELETE']).to.equal('SomeController.destroy') - expect(verbs['/tasks/:id/completed-GET/HEAD']).to.equal('SomeController.getCompleted') - }) - - it('should be able to assign middleware to the member', function () { - Route - .resource('/tasks', 'SomeController') - .addMember('completed', ['GET', 'HEAD'], function (member) { - member.bindAction('SomeController.getCompleted').middleware('auth') - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.middlewares] - })) - expect(routes.length).to.equal(8) - expect(verbs['/tasks/:id/completed-GET/HEAD']).deep.equal(['auth']) - }) - - it('should be able to update member route name', function () { - Route - .resource('/tasks', 'SomeController') - .addMember('completed', ['GET', 'HEAD'], function (member) { - member.bindAction('SomeController.getCompleted').as('getCompletedTasks') - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - expect(routes.length).to.equal(8) - expect(verbs['/tasks/:id/completed-GET/HEAD']).deep.equal('getCompletedTasks') - }) - - it('should return route defination using toJSON method', function () { - let resourceMember = {} - Route - .resource('/tasks', 'SomeController') - .addMember('completed', ['GET', 'HEAD'], function (member) { - resourceMember = member.toJSON() - }) - expect(resourceMember).to.be.an('object') - expect(resourceMember.verb).deep.equal(['GET', 'HEAD']) - expect(resourceMember.handler).to.equal('SomeController.completed') - expect(resourceMember.middlewares).deep.equal([]) - expect(resourceMember.name).to.equal('tasks.completed') - }) - - it('should register resourceful routes with collection paths', function () { - Route - .resource('/tasks', 'SomeController') - .addCollection('completed', ['GET', 'HEAD']) - .addCollection('mark_as', 'POST') - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - - expect(routes.length).to.equal(9) - expect(verbs['/tasks-GET/HEAD']).to.equal('SomeController.index') - expect(verbs['/tasks/create-GET/HEAD']).to.equal('SomeController.create') - expect(verbs['/tasks-POST']).to.equal('SomeController.store') - expect(verbs['/tasks/:id-GET/HEAD']).to.equal('SomeController.show') - expect(verbs['/tasks/:id/edit-GET/HEAD']).to.equal('SomeController.edit') - expect(verbs['/tasks/:id-PUT/PATCH']).to.equal('SomeController.update') - expect(verbs['/tasks/:id-DELETE']).to.equal('SomeController.destroy') - expect(verbs['/tasks/completed-GET/HEAD']).to.equal('SomeController.completed') - expect(verbs['/tasks/mark_as-POST']).to.equal('SomeController.mark_as') - }) - - it('should be able to add collection paths to nested resources', function () { - Route - .resource('user.tasks', 'SomeController') - .addCollection('completed', ['GET', 'HEAD']) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(verbs['/user/:user_id/tasks/completed-GET/HEAD']).to.equal('SomeController.completed') - }) - - it('should make use of GET and HEAD verbs when no verbs are defined with addCollection', function () { - Route - .resource('/tasks', 'SomeController') - .addCollection('completed') - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - expect(verbs['/tasks/completed-GET/HEAD']).to.equal('SomeController.completed') - }) - - it('should throw an error when the action is not present for collection route', function () { - const fn = function () { - Route - .resource('/tasks', 'SomeController') - .addCollection() - } - expect(fn).to.throw('InvalidArgumentException: E_INVALID_PARAMETER: Resource.addCollection expects a route') - }) - - it('should be able to bind controller action to the resource collection', function () { - Route - .resource('/tasks', 'SomeController') - .addCollection('completed', ['GET', 'HEAD'], function (collection) { - collection.bindAction('SomeController.getCompletedTasks') - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.handler] - })) - - expect(routes.length).to.equal(8) - expect(verbs['/tasks-GET/HEAD']).to.equal('SomeController.index') - expect(verbs['/tasks/create-GET/HEAD']).to.equal('SomeController.create') - expect(verbs['/tasks-POST']).to.equal('SomeController.store') - expect(verbs['/tasks/:id-GET/HEAD']).to.equal('SomeController.show') - expect(verbs['/tasks/:id/edit-GET/HEAD']).to.equal('SomeController.edit') - expect(verbs['/tasks/:id-PUT/PATCH']).to.equal('SomeController.update') - expect(verbs['/tasks/:id-DELETE']).to.equal('SomeController.destroy') - expect(verbs['/tasks/completed-GET/HEAD']).to.equal('SomeController.getCompletedTasks') - }) - - it('should be able to assign middleware to the resource collection', function () { - Route - .resource('/tasks', 'SomeController') - .addCollection('completed', ['GET', 'HEAD'], function (collection) { - collection.middleware('auth') - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.middlewares] - })) - - expect(routes.length).to.equal(8) - expect(verbs['/tasks/completed-GET/HEAD']).deep.equal(['auth']) - }) - - it('should be able to change route name for the resource collection', function () { - Route - .resource('/tasks', 'SomeController') - .addCollection('completed', ['GET', 'HEAD'], function (collection) { - collection.as('tasks.getCompleted') - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - - expect(routes.length).to.equal(8) - expect(verbs['/tasks/completed-GET/HEAD']).deep.equal('tasks.getCompleted') - }) - - it('should be able to override collection route name', function () { - Route - .resource('/posts', 'PostController') - .addCollection('thrending', ['GET', 'HEAD']) - .as({ - thrending: 'posts.threndingPosts' - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - - expect(routes.length).to.equal(8) - expect(verbs['/posts-GET/HEAD']).to.equal('posts.index') - expect(verbs['/posts/create-GET/HEAD']).to.equal('posts.create') - expect(verbs['/posts-POST']).to.equal('posts.store') - expect(verbs['/posts/:id-GET/HEAD']).to.equal('posts.show') - expect(verbs['/posts/:id/edit-GET/HEAD']).to.equal('posts.edit') - expect(verbs['/posts/:id-PUT/PATCH']).to.equal('posts.update') - expect(verbs['/posts/:id-DELETE']).to.equal('posts.destroy') - expect(verbs['/posts/thrending-GET/HEAD']).to.equal('posts.threndingPosts') - }) - - it('should be able to override member route name', function () { - Route - .resource('/posts', 'PostController') - .addMember('preview', ['GET', 'HEAD']) - .as({ - preview: 'posts.previewPost' - }) - - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.name] - })) - - expect(routes.length).to.equal(8) - expect(verbs['/posts-GET/HEAD']).to.equal('posts.index') - expect(verbs['/posts/create-GET/HEAD']).to.equal('posts.create') - expect(verbs['/posts-POST']).to.equal('posts.store') - expect(verbs['/posts/:id-GET/HEAD']).to.equal('posts.show') - expect(verbs['/posts/:id/edit-GET/HEAD']).to.equal('posts.edit') - expect(verbs['/posts/:id-PUT/PATCH']).to.equal('posts.update') - expect(verbs['/posts/:id-DELETE']).to.equal('posts.destroy') - expect(verbs['/posts/:id/preview-GET/HEAD']).to.equal('posts.previewPost') - }) - }) - - context('Resolve', function () { - it('should return an empty object when unable to resolve route', function () { - const home = Route.resolve('/', 'GET') - expect(home).deep.equal({}) - }) - - it('should resolve a given route', function () { - Route.get('/', 'SomeController.method') - const home = Route.resolve('/', 'GET') - expect(home.route).to.equal('/') - expect(home.matchedVerb).to.equal('GET') - expect(home.handler).to.equal('SomeController.method') - expect(home.group).to.equal(null) - expect(home.middlewares).deep.equal([]) - expect(home.domain).to.equal(null) - expect(home.params).deep.equal({}) - }) - - it('should return route arguments', function () { - Route.get('/:id', 'SomeController.method') - const home = Route.resolve('/1', 'GET') - expect(home.params).deep.equal({id: '1'}) - }) - - it('should resolve a route prefixed via group', function () { - Route.group('v1', function () { - Route.get('/', 'SomeController.method') - }).prefix('/v1') - - const home = Route.resolve('/v1', 'GET') - expect(home.route).to.equal('/v1') - expect(home.matchedVerb).to.equal('GET') - expect(home.handler).to.equal('SomeController.method') - expect(home.group).to.equal('v1') - expect(home.middlewares).deep.equal([]) - expect(home.domain).to.equal(null) - expect(home.params).deep.equal({}) - }) - - it('should resolve a route registered with multiple verbs', function () { - Route.match(['get', 'post'], '/', 'SomeController.method') - const home = Route.resolve('/', 'GET') - expect(home.route).to.equal('/') - expect(home.verb).deep.equal(['GET', 'POST']) - expect(home.matchedVerb).to.equal('GET') - expect(home.handler).to.equal('SomeController.method') - expect(home.group).to.equal(null) - expect(home.middlewares).deep.equal([]) - expect(home.domain).to.equal(null) - expect(home.params).deep.equal({}) - }) - - it('should return route middlewares if registered with route', function () { - Route.get('/', 'SomeController.method').middlewares(['auth']) - const home = Route.resolve('/', 'GET') - expect(home.middlewares).deep.equal(['auth']) - }) - - it('should return route middlewares registered on group', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).middlewares(['auth']) - const home = Route.resolve('/', 'GET') - expect(home.middlewares).deep.equal(['auth']) - }) - - it('should return route middlewares registered on group and on route as well', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method').middlewares(['cors']) - }).middlewares(['auth']) - const home = Route.resolve('/', 'GET') - expect(home.middlewares).deep.equal(['cors', 'auth']) - }) - - it('should resolve routes with domains', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).domain('virk.me') - const home = Route.resolve('/', 'GET', 'virk.me') - expect(home.route).to.equal('/') - expect(home.handler).to.equal('SomeController.method') - }) - - it('should not resolve paths defined inside domain without host', function () { - Route.group('admin', function () { - Route.get('/', 'SomeController.method') - }).domain('virk.me') - const home = Route.resolve('/', 'GET') - expect(home).deep.equal({}) - }) - - it('should resolve routes registered as resource', function () { - Route.resource('users', 'UsersController') - const usersIndex = Route.resolve('/users', 'GET') - expect(usersIndex.name).to.equal('users.index') - expect(usersIndex.handler).to.equal('UsersController.index') - }) - - it('should resolve routes registered as nested resource', function () { - Route.resource('users.posts', 'PostController') - const postsIndex = Route.resolve('/users/1/posts', 'GET') - expect(postsIndex.name).to.equal('users.posts.index') - expect(postsIndex.handler).to.equal('PostController.index') - }) - - it('should resolve routes registered as resource under a group', function () { - Route.group('v1', function () { - Route.resource('users.posts', 'PostController') - }).prefix('/v1') - const postsIndex = Route.resolve('/v1/users/1/posts', 'GET') - expect(postsIndex.name).to.equal('users.posts.index') - expect(postsIndex.handler).to.equal('PostController.index') - }) - - it('should resolve routes registered as resource under a group and with a group', function () { - Route.resource('users.posts', 'PostController') - Route.group('v1', function () { - Route.resource('users.posts', 'V1PostController') - }).prefix('/v1') - const V1postsIndex = Route.resolve('/v1/users/1/posts', 'GET') - expect(V1postsIndex.name).to.equal('users.posts.index') - expect(V1postsIndex.handler).to.equal('V1PostController.index') - - const postsIndex = Route.resolve('/users/1/posts', 'GET') - expect(postsIndex.name).to.equal('users.posts.index') - expect(postsIndex.handler).to.equal('PostController.index') - }) - - it('should be able to define and resolve routes using formats', function () { - Route.get('/users', 'UsersController.index').formats(['json', 'xml']) - const userIndex = Route.resolve('/users', 'GET') - const withJson = Route.resolve('/users.json', 'GET') - const withXml = Route.resolve('/users.xml', 'GET') - expect(userIndex.handler).to.equal(withJson.handler) - expect(userIndex.handler).to.equal(withXml.handler) - }) - - it('should be able to define and resolve routes using formats from groups', function () { - Route.group('v2', function () { - Route.get('/users', 'UsersController.index') - }).prefix('/v2').formats(['json', 'xml']) - const userIndex = Route.resolve('/v2/users', 'GET') - const withJson = Route.resolve('/v2/users.json', 'GET') - const withXml = Route.resolve('/v2/users.xml', 'GET') - expect(userIndex.handler).to.equal(withJson.handler) - expect(userIndex.handler).to.equal(withXml.handler) - }) - - it('should be able to define formats on single routes and then groups too', function () { - Route.group('v2', function () { - Route.get('/users', 'UsersController.index').formats(['html']) - }).prefix('/v2').formats(['json', 'xml']) - const userIndex = Route.resolve('/v2/users', 'GET') - const withJson = Route.resolve('/v2/users.json', 'GET') - const withXml = Route.resolve('/v2/users.xml', 'GET') - const withHtml = Route.resolve('/v2/users.html', 'GET') - expect(userIndex.handler).deep.equal(withJson.handler) - expect(userIndex.handler).deep.equal(withXml.handler) - expect(userIndex.handler).deep.equal(withHtml.handler) - }) - - it('should be to define able formats on route resources', function () { - Route.resource('users', 'UsersController').formats(['json', 'html']) - const userIndex = Route.resolve('/users', 'GET') - const withJson = Route.resolve('/users.json', 'GET') - const withHtml = Route.resolve('/users.html', 'GET') - expect(userIndex.handler).deep.equal(withJson.handler) - expect(userIndex.handler).deep.equal(withHtml.handler) - }) - - it('should return format as params when using formats', function () { - Route.resource('users', 'UsersController').formats(['json', 'html']) - const userIndex = Route.resolve('/users', 'GET') - const withJson = Route.resolve('/users.json', 'GET') - const withHtml = Route.resolve('/users.html', 'GET') - expect(userIndex.params.format).to.equal(undefined) - expect(withJson.params.format).to.equal('.json') - expect(withHtml.params.format).to.equal('.html') - }) - }) - - context('Building Url', function () { - it('should make url for any url , even if not registered inside routes', function () { - const url = Route.url('http://amanvirk.me/:post',%20%7Bpost:%20'hello-world'%7D) - expect(url).to.equal('http://amanvirk.me/hello-world') - }) - - it('should make for any given route', function () { - Route.get('/:post', 'SomeController.index') - const url = Route.url('http://github.com/:post',%20%7Bpost:%20'hello-world'%7D) - expect(url).to.equal('/hello-world') - }) - - it('should make url for a named route', function () { - Route.get('/:post', 'SomeController.index').as('post') - const url = Route.url('http://github.com/post',%20%7Bpost:%20'hello-world'%7D) - expect(url).to.equal('/hello-world') - }) - - it('should make url for a prefixed named route', function () { - Route.group('v1', function () { - Route.get('/:post', 'SomeController.index').as('post') - }).prefix('/v1') - const url = Route.url('http://github.com/post',%20%7Bpost:%20'hello-world'%7D) - expect(url).to.equal('/v1/hello-world') - }) - - it('should make url for route registered inside domain', function () { - Route.group('v1', function () { - Route.get('/:post', 'SomeController.index').as('post') - }).domain('amanvirk.me') - const url = Route.url('http://github.com/post',%20%7Bpost:%20'hello-world'%7D) - expect(url).to.equal('amanvirk.me/hello-world') - }) - - it('should make url for route registered as a resource', function () { - Route.resource('users', 'UsersController') - const url = Route.url('http://github.com/users.index') - const createUrl = Route.url('http://github.com/users.create') - const updateUrl = Route.url('http://github.com/users.update',%20%7Bid:%201%7D) - expect(url).to.equal('/users') - expect(createUrl).to.equal('/users/create') - expect(updateUrl).to.equal('/users/1') - }) - - it('should be able to define a get route using .on method', function () { - Route.on('/signup') - const routes = Route.routes() - expect(routes[0].handler).to.equal(null) - expect(routes[0].route).to.equal('/signup') - }) - - it('should bind a custom callback handler to the render method', function () { - Route.on('/signup').render('signup') - const routes = Route.routes() - expect(typeof (routes[0].handler)).to.equal('function') - expect(routes[0].route).to.equal('/signup') - }) - - it('should call sendView method on response when handler is invoked', function * () { - let viewToRender = null - const res = { - sendView: function * (view) { - viewToRender = view - } - } - Route.on('/signup').render('signup') - const routes = Route.routes() - yield routes[0].handler({}, res) - expect(viewToRender).to.equal('signup') - }) - - it('should pass request object to the sendView method', function * () { - let requestPassed = null - const res = { - sendView: function * (view, data) { - requestPassed = data.request - } - } - Route.on('/signup').render('signup') - const routes = Route.routes() - yield routes[0].handler({foo: 'bar'}, res) - expect(requestPassed).deep.equal({foo: 'bar'}) - }) - - it('should be able to bind middleware to the resource', function () { - Route.resource('tasks', 'TaskController').middleware('auth') - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.middlewares] - })) - expect(routes).to.have.length(7) - expect(verbs['/tasks-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks/create-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks-POST']).deep.equal(['auth']) - expect(verbs['/tasks/:id-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks/:id/edit-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks/:id-PUT/PATCH']).deep.equal(['auth']) - expect(verbs['/tasks/:id-DELETE']).deep.equal(['auth']) - }) - - it('should be able to bind middleware on selected actions', function () { - Route.resource('tasks', 'TaskController').middleware({ - auth: ['store'] - }) - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.middlewares] - })) - expect(routes).to.have.length(7) - expect(verbs['/tasks-GET/HEAD']).deep.equal([]) - expect(verbs['/tasks/create-GET/HEAD']).deep.equal([]) - expect(verbs['/tasks-POST']).deep.equal(['auth']) - expect(verbs['/tasks/:id-GET/HEAD']).deep.equal([]) - expect(verbs['/tasks/:id/edit-GET/HEAD']).deep.equal([]) - expect(verbs['/tasks/:id-PUT/PATCH']).deep.equal([]) - expect(verbs['/tasks/:id-DELETE']).deep.equal([]) - }) - - it('should be able to bind multiple middleware on selected actions', function () { - Route.resource('tasks', 'TaskController').middleware({ - auth: ['store'], - web: ['index', 'show'] - }) - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.middlewares] - })) - expect(routes).to.have.length(7) - expect(verbs['/tasks-GET/HEAD']).deep.equal(['web']) - expect(verbs['/tasks/create-GET/HEAD']).deep.equal([]) - expect(verbs['/tasks-POST']).deep.equal(['auth']) - expect(verbs['/tasks/:id-GET/HEAD']).deep.equal(['web']) - expect(verbs['/tasks/:id/edit-GET/HEAD']).deep.equal([]) - expect(verbs['/tasks/:id-PUT/PATCH']).deep.equal([]) - expect(verbs['/tasks/:id-DELETE']).deep.equal([]) - }) - - it('should throw an error when actions are not defined as array', function () { - const fn = () => Route.resource('tasks', 'TaskController').middleware({auth: 'store'}) - expect(fn).to.throw('InvalidArgumentException: E_INVALID_PARAMETER: Resource route methods must be defined as an array') - }) - - it('should be able to bind an array middleware to the resource', function () { - Route.resource('tasks', 'TaskController').middleware(['auth']) - const routes = Route.routes() - const verbs = _.fromPairs(_.map(routes, function (route) { - return [route.route + '-' + route.verb.join('/'), route.middlewares] - })) - expect(routes).to.have.length(7) - expect(verbs['/tasks-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks/create-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks-POST']).deep.equal(['auth']) - expect(verbs['/tasks/:id-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks/:id/edit-GET/HEAD']).deep.equal(['auth']) - expect(verbs['/tasks/:id-PUT/PATCH']).deep.equal(['auth']) - expect(verbs['/tasks/:id-DELETE']).deep.equal(['auth']) - }) - - it('should return an array of routes for a given resource', function () { - const resourceRoutes = Route - .resource('tasks', 'TaskController') - .only(['store', 'update']) - .middleware(['auth']) - .toJSON() - expect(resourceRoutes).to.have.length(2) - expect(resourceRoutes[0].route).to.equal('/tasks') - expect(resourceRoutes[1].route).to.equal('/tasks/:id') - expect(resourceRoutes[0].middlewares).deep.equal(['auth']) - expect(resourceRoutes[1].middlewares).deep.equal(['auth']) - }) - }) -}) diff --git a/test/unit/server.spec.js b/test/unit/server.spec.js deleted file mode 100644 index 1bc6b557..00000000 --- a/test/unit/server.spec.js +++ /dev/null @@ -1,295 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const Server = require('../../src/Server') -const Request = require('../../src/Request') -const ResponseBuilder = require('../../src/Response') -const Static = require('../../src/Static') -const Route = require('../../src/Route') -const Middleware = require('../../src/Middleware') -const chai = require('chai') -const Ioc = require('adonis-fold').Ioc -const supertest = require('co-supertest') -const expect = chai.expect -const http = require('http') -const EventProvider = require('../../src/Event') -const path = require('path') -const stderr = require('test-console').stderr - -class Session { - -} -const Config = { - get: function (key) { - switch (key) { - case 'app.static': - return {} - case 'event': - return { - wildcard: true, - delimiter: ':' - } - case 'app.http.allowMethodSpoofing': - return true - default: - return 2 - } - } -} - -const Helpers = { - publicPath: function () { - return path.join(__dirname, './public') - }, - makeNameSpace: function (base, toPath) { - return `App/${base}/${toPath}` - } -} - -const Event = new EventProvider(Config) - -require('co-mocha') - -describe('Server', function () { - before(function () { - Ioc.autoload('App', path.join(__dirname, './app')) - const staticServer = new Static(Helpers, Config) - const Response = new ResponseBuilder({}, {}, Config) - this.server = new Server(Request, Response, Route, Helpers, Middleware, staticServer, Session, Config, Event) - }) - - beforeEach(function () { - Route.new() - Middleware.new() - }) - - it('should serve static resource from a given directory', function * () { - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/style.css').expect('Content-type', /css/).expect(200).end() - expect(res.text).to.match(/(?:\s*\S+\s*{[^}]*})+/g) - }) - - it('should serve favicon when request is for favicon', function * () { - const testServer = http.createServer(this.server.handle.bind(this.server)) - yield supertest(testServer).get('/favicon.ico').expect('Content-type', /x-icon/).expect(200).end() - }) - - it('should make 404 error when unable to find static resource', function * () { - const testServer = http.createServer(this.server.handle.bind(this.server)) - yield supertest(testServer).get('/foo.css').expect(404).end() - }) - - it('should not serve static resources with route is not GET or HEAD', function * () { - const testServer = http.createServer(this.server.handle.bind(this.server)) - yield supertest(testServer).post('/style.css').expect(404).end() - }) - - it('should serve static resource even if route is defined', function * () { - Route.get('/favicon.ico', function * (request, response) { - response.send({rendered: true}) - }) - const testServer = http.createServer(this.server.handle.bind(this.server)) - yield supertest(testServer).get('/favicon.ico').expect('Content-type', /x-icon/).expect(200).end() - }) - - it('should call route action if defined', function * () { - Route.get('/', function * (request, response) { - response.send({rendered: true}) - }) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(200).end() - expect(res.body.rendered).to.equal(true) - }) - - it('should invoke route for verb defined using _method', function * () { - Route.put('/', function * (request, response) { - response.send({rendered: true}) - }) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/?_method=PUT').expect(200).end() - expect(res.body.rendered).to.equal(true) - }) - - it('should not spoof request method when allowMethodSpoofing is not turned on', function * () { - Route.put('/', function * (request, response) { - response.send({rendered: true}) - }) - const staticServer = new Static(Helpers, Config) - const Response = new ResponseBuilder({}, {}, Config) - const customConfig = { - get: function (key) { - if (key === 'app.http.allowMethodSpoofing') { - return false - } - return Config.get(key) - } - } - const server = new Server(Request, Response, Route, Helpers, Middleware, staticServer, Session, customConfig, Event) - const testServer = http.createServer(server.handle.bind(server)) - yield supertest(testServer).get('/?_method=PUT').expect(404).end() - }) - - it('should not log warning when allowMethodSpoofing is not turned on but trying to spoof method', function * () { - Route.put('/', function * (request, response) { - response.send({rendered: true}) - }) - const inspect = stderr.inspect() - const staticServer = new Static(Helpers, Config) - const Response = new ResponseBuilder({}, {}, Config) - const customConfig = { - get: function (key) { - if (key === 'app.http.allowMethodSpoofing') { - return false - } - return Config.get(key) - } - } - const server = new Server(Request, Response, Route, Helpers, Middleware, staticServer, Session, customConfig, Event) - const testServer = http.createServer(server.handle.bind(server)) - yield supertest(testServer).get('/?_method=PUT').expect(404).end() - inspect.restore() - expect(inspect.output.join('')).to.match(/You are making use of method spoofing/) - }) - - it('should call route action via controller method', function * () { - Route.get('/', 'HomeController.index') - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(200).end() - expect(res.body.rendered).to.equal(true) - }) - - it('should return error when route handler is not of a valid type', function * () { - Route.get('/', {}) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(500).end() - expect(res.error.text).to.match(/InvalidArgumentException: E_INVALID_IOC_BINDING: Handler must point to a valid namespace or a closure/) - }) - - it('should return error when unable to find controller', function * () { - Route.get('/', 'FooController.index') - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(500).end() - expect(res.error.text).to.match(/Cannot find module/) - }) - - it('should return error when unable to find controller method', function * () { - Route.get('/', 'HomeController.foo') - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(500).end() - expect(res.error.text).to.match(/foo does not exists/) - }) - - it('should call all global middleware before reaching the route handler', function * () { - Middleware.global(['App/Http/Middleware/Global']) - Route.get('/', 'UserController.index') - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(200).end() - expect(res.text).to.equal('2') - }) - - it('should catch errors created by global middleware', function * () { - Middleware.global(['App/Http/Middleware/GlobalCatch']) - Route.get('/', 'UserController.index') - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(401).end() - expect(res.error.text).to.equal('Login') - }) - - it('should catch errors thrown by global middleware', function * () { - Middleware.global(['App/Http/Middleware/GlobalThrow']) - Route.get('/', 'UserController.index') - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(401).end() - expect(res.error.text).to.match(/Error: Login/) - }) - - it('should return error when unable to resolve middleware', function * () { - Middleware.register('auth', ['App/Auth']) - Route.get('/', 'HomeController.index').middlewares(['auth']) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(500).end() - expect(res.error.text).to.match(/Cannot find module/) - }) - - it('should return error when unable to find handle method on middleware', function * () { - Middleware.register('auth', ['App/Http/Middleware/NoHandle']) - Route.get('/', 'HomeController.index').middlewares(['auth']) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(500).end() - expect(res.error.text).to.match(/handle does not exists/) - }) - - it('should handle call middleware attached to a route', function * () { - Middleware.register('parser', 'App/Http/Middleware/Parser') - Middleware.register('cycle', 'App/Http/Middleware/Cycle2') - Route.get('/', 'UserController.index').middlewares(['parser', 'cycle']) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(200).end() - expect(res.text).to.equal('1') - }) - - it('should call middlewares attached on route with closure as handler', function * () { - Middleware.register('parser', 'App/Http/Middleware/Parser') - Middleware.register('cycle', 'App/Http/Middleware/Cycle2') - Route.get('/', function * (request, response) { - response.send(request.count) - }).middlewares(['parser', 'cycle']) - - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(200).end() - expect(res.text).to.equal('1') - }) - - it('should report error thrown my route closure', function * () { - Route.get('/', function * () { - throw new Error('Unable to login') - }) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(500).end() - expect(res.error.text).to.match(/Unable to login/) - }) - - it('should show default error stack when error itself does not have any message', function * () { - Route.get('/', function * () { - throw new Error() - }) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(500).end() - expect(res.error.text).to.match(/Error/) - }) - - it('should emit error event when there are listeners attach to error', function * () { - Event.when('Http:error:*', function (error, request, response) { - response.status(401).send(error.message) - }) - Route.get('/', function * () { - throw new Error('Forbidden') - }) - const testServer = http.createServer(this.server.handle.bind(this.server)) - const res = yield supertest(testServer).get('/').expect(401).end() - expect(res.error.text).to.equal('Forbidden') - }) - - it('should server instance is null', function * () { - expect(this.server.httpInstance).to.be.null - }) - - it('should server instance is http.Server', function * () { - const httpServer = this.server.getInstance() - expect(httpServer).to.be.instanceOf(http.Server) - expect(this.server.httpInstance).to.be.instanceOf(http.Server) - }) - - it('should listen to server on a given port and host using listen method', function * () { - Route.get('/', 'HomeController.index') - this.server.listen('0.0.0.0', 8000) - const testServer = supertest.agent('http://127.0.0.1:8000/') - const res = yield testServer.get('/').expect(200).end() - expect(res.body).deep.equal({rendered: true}) - }) -}) diff --git a/test/unit/sessions.spec.js b/test/unit/sessions.spec.js deleted file mode 100644 index a3fa7061..00000000 --- a/test/unit/sessions.spec.js +++ /dev/null @@ -1,1069 +0,0 @@ -'use strict' - -/** - * adonis-framework - * Copyright(c) 2015-2016 Harminder Virk - * MIT Licensed -*/ - -const chai = require('chai') -const http = require('http') -const supertest = require('co-supertest') -const co = require('co') -const Ioc = require('adonis-fold').Ioc -const expect = chai.expect -const _ = require('lodash') -const fs = require('co-fs-extra') -const path = require('path') -const Session = require('../../src/Session') -const Drivers = require('../../src/Session/Drivers') -const CookieDriver = Drivers.cookie -const FileDriver = Drivers.file -const RedisDriver = Drivers.redis -const RedisFactory = require('adonis-redis/src/RedisFactory') -const Store = require('../../src/Session/Store') -const SessionManager = require('../../src/Session/SessionManager') -const querystring = require('querystring') -require('co-mocha') - -const parseCookies = function (cookies) { - return _.reduce(cookies, (map, cookie) => { - const tokens = cookie.split('=') - const value = querystring.unescape(_.tail(tokens).join('=')) - map[tokens[0]] = value.startsWith('j:') ? JSON.parse(removeCookieAttribures(value.replace('j:', ''))) : value - return map - }, {}) -} - -const removeCookieAttribures = function (cookie) { - return cookie.replace(/;.*/, '') -} - -const makeConfigObject = function (appKey, sessionObject) { - sessionObject = sessionObject || {} - const session = _.merge({ - driver: 'cookie', - cookie: 'adonis-session', - domain: 'localhost', - path: '/', - clearWithBrowser: true, - age: 10, - secure: false, - redis: { - host: '127.0.0.1', - port: '6379', - keyPrefix: 'session:' - } - }, sessionObject) - return { - app: { appKey: appKey || null }, - session - } -} - -class Config { - constructor (appKey, sessionObject) { - this.config = makeConfigObject(appKey, sessionObject) - } - get (key) { - return _.get(this.config, key) - } -} - -describe('Session', function () { - beforeEach(function () { - Session.driver = {} - Session.config = {} - }) - - context('Session Builder', function () { - it('should throw an error when unable to locate driver', function * () { - const fn = () => new SessionManager({get: function () { return 'mongo' }}) - expect(fn).to.throw('RuntimeException: E_INVALID_SESSION_DRIVER: Unable to locate mongo session driver') - }) - - it('should extend session drivers using extend method', function * () { - class Redis { - } - SessionManager.extend('my-redis', new Redis()) - const config = new Config(null, {driver: 'my-redis'}) - const session = new SessionManager(config) - expect(session.driver instanceof Redis).to.equal(true) - }) - - it('should make an instance of pre existing drivers using make method', function * () { - let Config = {} - Config.get = function () { - return 'file' - } - let Helpers = {} - Helpers.storagePath = function () { - return '' - } - Ioc.bind('Adonis/Src/Config', function () { - return Config - }) - Ioc.bind('Adonis/Src/Helpers', function () { - return Helpers - }) - const session = new SessionManager(Config) - expect(session.driver.sessionPath).to.equal('') - }) - }) - - context('Session class @session', function () { - it('should throw an exception when values passed to put method are not valid', function * () { - Session.config = new Config() - const session = new Session({once: function () {}}) - try { - yield session.put('key') - } catch (e) { - expect(e.message).to.equal('E_INVALID_PARAMETER: Session.put expects a key/value pair or an object of keys and values') - } - }) - - it('should not call driver read method multiple times if read operations are more than one', function * () { - const config = new Config() - let readCounts = 0 - class FakeDriver { - * read () { - readCounts++ - } - } - Session.driver = new FakeDriver(config) - Session.config = config - const session = new Session({headers: {}, once: function () {}}, {}) - yield session.all() - yield session.get('foo') - expect(readCounts).to.equal(1) - }) - - it('should return a clone of session values to keep the actual session safe from mutability', function * () { - const config = new Config() - class FakeDriver { - * read () { - return {name: {d: 'virk', t: 'String'}} - } - } - Session.driver = new FakeDriver(config) - Session.config = config - const session = new Session({headers: {}, once: function () {}}, {}) - const values = yield session.all() - expect(values).deep.equal({name: 'virk'}) - values.name = 'foo' - const reFetchSession = yield session.all() - expect(reFetchSession).deep.equal({name: 'virk'}) - }) - - it('should property of session from the internal payload, instead of the cloned copy', function * () { - const config = new Config() - class FakeDriver { - * read () { - return {name: {d: 'virk', t: 'String'}} - } - } - Session.driver = new FakeDriver(config) - Session.config = config - const session = new Session({headers: {}, once: function () {}}, {}) - const values = yield session.all() - expect(values).deep.equal({name: 'virk'}) - values.name = 'foo' - const name = yield session.get('name') - expect(name).deep.equal('virk') - }) - - it('should deep property of session using get method', function * () { - const config = new Config() - class FakeDriver { - * read () { - return {profile: {d: {name: 'virk'}, t: 'Object'}} - } - } - Session.driver = new FakeDriver(config) - Session.config = config - const session = new Session({headers: {}, once: function () {}}, {}) - const name = yield session.get('profile.name') - expect(name).deep.equal('virk') - }) - - it('should be able to pull deeply property of session using get method', function * () { - const config = new Config() - class FakeDriver { - * read () { - return {profile: {d: {name: 'virk', age: 22}, t: 'Object'}} - } - - * write () { - } - } - Session.driver = new FakeDriver(config) - Session.config = config - const session = new Session({headers: {}, once: function () {}}, {getHeader: function () {}, setHeader: function () {}}) - const name = yield session.pull('profile.name') - expect(name).deep.equal('virk') - const all = yield session.all() - expect(all).to.deep.equal({profile: {age: 22, name: null}}) - }) - }) - - context('Session Store', function () { - it('should convert object to string representation', function () { - const value = {name: 'virk'} - const key = 'profile' - const body = Store.guardPair(key, value) - expect(body).to.deep.equal({d: JSON.stringify(value), t: 'Object'}) - }) - - it('should convert date to string representation', function () { - const value = new Date() - const key = 'time' - const body = Store.guardPair(key, value) - expect(body).to.deep.equal({d: String(value), t: 'Date'}) - }) - - it('should convert number to string representation', function () { - const value = 22 - const key = 'age' - const body = Store.guardPair(key, value) - expect(body).to.deep.equal({d: String(value), t: 'Number'}) - }) - - it('should convert array to string representation', function () { - const value = [22, 42] - const key = 'marks' - const body = Store.guardPair(key, value) - expect(body).to.deep.equal({d: JSON.stringify(value), t: 'Array'}) - }) - - it('should convert boolean to string representation', function () { - const value = true - const key = 'admin' - const body = Store.guardPair(key, value) - expect(body).to.deep.equal({d: String(value), t: 'Boolean'}) - }) - - it('should convert return null when value is a function', function () { - const value = function () {} - const key = 'admin' - const body = Store.guardPair(key, value) - expect(body).to.equal(null) - }) - - it('should convert return null when value is a regex', function () { - const value = /12/ - const key = 'admin' - const body = Store.guardPair(key, value) - expect(body).to.equal(null) - }) - - it('should convert return null when value is an error', function () { - const value = new Error() - const key = 'admin' - const body = Store.guardPair(key, value) - expect(body).to.equal(null) - }) - - it("should convert body with object to it's original value", function () { - const value = {name: 'virk'} - const body = { - d: JSON.stringify(value), - t: 'Object' - } - const convertedValue = Store.unGuardPair(body) - expect(convertedValue).deep.equal(value) - }) - - it("should convert body with number to it's original value", function () { - const value = 22 - const body = { - d: String(value), - t: 'Number' - } - const convertedValue = Store.unGuardPair(body) - expect(convertedValue).to.equal(value) - }) - - it("should convert body with Array to it's original value", function () { - Session.config = new Config() - const value = [22, 42] - const body = { - d: JSON.stringify(value), - t: 'Array' - } - const convertedValue = Store.unGuardPair(body) - expect(convertedValue).deep.equal(value) - }) - - it("should convert body with negative boolean to it's original value", function () { - const value = false - const body = { - d: String(value), - t: 'Boolean' - } - const convertedValue = Store.unGuardPair(body) - expect(convertedValue).to.equal(value) - }) - - it("should convert body with positive boolean to it's original value", function () { - const value = true - const body = { - d: String(value), - t: 'Boolean' - } - const convertedValue = Store.unGuardPair(body) - expect(convertedValue).to.equal(true) - }) - }) - - context('Cookie Driver @cookie', function () { - it('should set session on cookies when active driver is cookie', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'virk') - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies).to.have.property('adonis-session') - expect(cookies).to.have.property('adonis-session-value') - expect(cookies['adonis-session-value'].sessionId).to.equal(removeCookieAttribures(cookies['adonis-session'])) - expect(cookies['adonis-session-value'].data).deep.equal({name: Store.guardPair('name', 'virk')}) - }) - - it('should set multiple session values on cookies using object', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put({name: 'virk', age: 22}) - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies).to.have.property('adonis-session') - expect(cookies).to.have.property('adonis-session-value') - expect(cookies['adonis-session-value'].sessionId).to.equal(removeCookieAttribures(cookies['adonis-session'])) - const data = {} - data.name = Store.guardPair('name', 'virk') - data.age = Store.guardPair('age', 22) - expect(cookies['adonis-session-value'].data).deep.equal(data) - }) - - it('should set json as value for a given key', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('profile', {name: 'virk', age: 22}) - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies).to.have.property('adonis-session') - expect(cookies).to.have.property('adonis-session-value') - expect(cookies['adonis-session-value'].sessionId).to.equal(removeCookieAttribures(cookies['adonis-session'])) - const data = {} - data.profile = Store.guardPair('profile', {name: 'virk', age: 22}) - expect(cookies['adonis-session-value'].data).deep.equal(data) - }) - - it('should not set key/value pair on session when value is not of a valid type', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put({ name: 'virk', age: function () {} }) - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies).to.have.property('adonis-session') - expect(cookies).to.have.property('adonis-session-value') - expect(cookies['adonis-session-value'].sessionId).to.equal(removeCookieAttribures(cookies['adonis-session'])) - let data = {} - data.name = Store.guardPair('name', 'virk') - expect(cookies['adonis-session-value'].data).deep.equal(data) - }) - - it('should make use of existing session id if defined', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'virk') - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=11']).expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies['adonis-session']).to.equal('11; Domain=localhost; Path=/') - }) - - it('should be able to remove existing session value', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'virk') - yield session.forget('name') - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: 11, - data: {} - } - body.data.name = Store.guardPair('name', 'virk') - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=11']).expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies).to.have.property('adonis-session') - expect(cookies['adonis-session']).to.equal('11; Domain=localhost; Path=/') - expect(cookies).to.have.property('adonis-session-value') - expect(cookies['adonis-session-value'].sessionId).to.equal(removeCookieAttribures(cookies['adonis-session'])) - expect(cookies['adonis-session-value'].data).deep.equal({}) - }) - - it('should return empty object when current sessionId does not equals the values session id', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.all() - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: 11, - data: {} - } - body.data.name = Store.guardPair('name', 'virk') - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session-value=j:' + JSON.stringify(body)]).expect(200).end() - expect(res.body.name).deep.equal({}) - }) - - it('should return all session values sent along the request', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.all() - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: '11', - data: {} - } - body.data.name = Store.guardPair('name', 'virk') - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=11; adonis-session-value=j:' + JSON.stringify(body) + ';']).expect(200).end() - expect(res.body.name).deep.equal({name: 'virk'}) - }) - - it('should be able to read existing session values from request', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.get('name') - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: '20', - data: {} - } - body.data.name = Store.guardPair('name', 'virk') - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=20; adonis-session-value=j:' + JSON.stringify(body)]).expect(200).end() - expect(res.body.name).to.equal('virk') - }) - - it('should return default value when existing session value does not exists', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.get('name', 'foo') - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - expect(res.body.name).to.equal('foo') - }) - - it('should return session value and delete it from session using pull method', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.pull('name') - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: '40', - data: {} - } - body.data.name = Store.guardPair('name', 'virk') - body.data.age = Store.guardPair('age', 22) - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=40; adonis-session-value=j:' + JSON.stringify(body)]).expect(200).end() - expect(res.body.name).to.equal('virk') - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies['adonis-session']).to.equal('40; Domain=localhost; Path=/') - expect(cookies['adonis-session-value'].data).deep.equal({age: body.data.age}) - }) - - it('should return empty object when pull is called on the last session value', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.pull('name') - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: '40', - data: {} - } - - body.data.name = Store.guardPair('name', 'virk') - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=40; adonis-session-value=j:' + JSON.stringify(body)]).expect(200).end() - expect(res.body.name).to.equal('virk') - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies['adonis-session']).to.equal('40; Domain=localhost; Path=/') - expect(cookies['adonis-session-value'].data).deep.equal({}) - }) - - it('should clear the session id from the cookie when flush has been called', function * () { - const config = new Config() - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('age', 22) - return yield session.flush() - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: '40', - data: {} - } - - body.data.name = Store.guardPair('name', 'virk') - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=40; adonis-session-value=j:' + JSON.stringify(body)]).expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies['adonis-session']).to.equal('; Expires=Thu, 01 Jan 1970 00:00:00 GMT') - expect(cookies['adonis-session-value']).to.equal('; Expires=Thu, 01 Jan 1970 00:00:00 GMT') - }) - - it('should set expiry on cookie when session clearWithBrowser is set to false', function * () { - const config = new Config(null, {clearWithBrowser: false}) - Session.driver = new CookieDriver(config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('age', 22) - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - let body = { - sessionId: '40', - data: {} - } - - body.data.name = Store.guardPair('name', 'virk') - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies['adonis-session']).to.match(/Expires=\w{3},\s*\d{1,}\s*\w{3}\s*\d{4}/) - }) - }) - - context('File Driver @file', function () { - before(function () { - this.Helpers = { - storagePath: function () { - return path.join(__dirname, './storage/sessions') - } - } - }) - - beforeEach(function * () { - yield fs.emptyDir(this.Helpers.storagePath()) - }) - - it('should set session using file driver', function * () { - const config = new Config() - Session.driver = new FileDriver(this.Helpers, config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'virk') - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies).to.have.property('adonis-session') - const sessionId = removeCookieAttribures(cookies['adonis-session']) - const sessionValues = yield fs.readJson(path.join(__dirname, './storage/sessions', sessionId)) - expect(sessionValues).deep.equal({name: Store.guardPair('name', 'virk')}) - }) - - it('should update session values when session already exists', function * () { - const config = new Config() - Session.driver = new FileDriver(this.Helpers, config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.put('name', 'updated name') - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '122002' - yield fs.writeJson(path.join(this.Helpers.storagePath(), sessionId), {name: Store.guardPair('name', 'foo')}) - const res = yield supertest(server).get('/').set('Cookie', 'adonis-session=' + sessionId).expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies['adonis-session']).to.equal('122002; Domain=localhost; Path=/') - const sessionValues = yield fs.readJson(path.join(this.Helpers.storagePath(), sessionId)) - expect(sessionValues).deep.equal({name: Store.guardPair('name', 'updated name')}) - }) - - it('should read value for a given key from session using file driver', function * () { - const config = new Config() - Session.driver = new FileDriver(this.Helpers, config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.get('name') - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '122002' - yield fs.writeJson(path.join(this.Helpers.storagePath(), sessionId), {name: Store.guardPair('name', 'virk')}) - const res = yield supertest(server).get('/').set('Cookie', 'adonis-session=' + sessionId).expect(200).end() - expect(res.body.name).to.equal('virk') - }) - - it('should be able to put values to session store multiple times in a single request', function * () { - const config = new Config() - Session.driver = new FileDriver(this.Helpers, config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'foo') - yield session.flush() - yield session.put('age', 22) - }).then(function () { - res.writeHead(200, {'content-type': 'application/json'}) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - const sessionId = removeCookieAttribures(cookies['adonis-session']) - const sessionValues = yield fs.readJson(path.join(this.Helpers.storagePath(), sessionId)) - expect(sessionValues).deep.equal({age: Store.guardPair('age', 22)}) - }) - - it('should be remove session file on flush', function * () { - const config = new Config() - Session.driver = new FileDriver(this.Helpers, config) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'foo') - yield session.flush() - }).then(function () { - res.writeHead(200, {'content-type': 'application/json'}) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '39000' - yield supertest(server).get('/').set('Cookie', ['adonis-session=' + sessionId]).expect(200).end() - const sessionFileExists = yield fs.exists(path.join(this.Helpers.storagePath(), sessionId)) - expect(sessionFileExists).to.equal(false) - }) - }) - - context('Redis Driver @redis', function () { - before(function () { - this.Helpers = {} - this.redis = new RedisFactory(new Config().get('session.redis'), this.Helpers, false) - }) - - afterEach(function * () { - const all = yield this.redis.keys('session:*') - const pipeline = this.redis.multi() - all.forEach((key) => { - pipeline.del(key) - }) - yield pipeline.exec() - }) - - it('should set session using redis driver', function * () { - const config = new Config() - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'virk') - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - console.log(err) - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies).to.have.property('adonis-session') - const sessionId = removeCookieAttribures(cookies['adonis-session']) - const sessionValues = yield this.redis.get(sessionId) - expect(JSON.parse(sessionValues)).deep.equal({name: Store.guardPair('name', 'virk')}) - }) - - it('should update session values when session already exists', function * () { - const config = new Config() - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.put('name', 'updated name') - }).then(function () { - res.writeHead(200) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '122002' - yield this.redis.set(sessionId, JSON.stringify({name: Store.guardPair('name', 'foo')})) - const res = yield supertest(server).get('/').set('Cookie', 'adonis-session=' + sessionId).expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - expect(cookies['adonis-session']).to.equal('122002; Domain=localhost; Path=/') - const sessionValues = yield this.redis.get(sessionId) - expect(JSON.parse(sessionValues)).deep.equal({name: Store.guardPair('name', 'updated name')}) - }) - - it('should read value for a given key from session using redis driver', function * () { - const config = new Config() - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.get('name') - }).then(function (name) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({name})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '122002' - yield this.redis.set(sessionId, JSON.stringify({name: Store.guardPair('name', 'foo')})) - const res = yield supertest(server).get('/').set('Cookie', 'adonis-session=' + sessionId).expect(200).end() - expect(res.body.name).to.equal('foo') - }) - - it('should be able to put values to session store multiple times in a single request', function * () { - const config = new Config() - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'foo') - yield session.flush() - yield session.put('age', 22) - }).then(function () { - res.writeHead(200, {'content-type': 'application/json'}) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const res = yield supertest(server).get('/').expect(200).end() - const cookies = parseCookies(res.headers['set-cookie']) - const sessionId = removeCookieAttribures(cookies['adonis-session']) - const sessionValues = yield this.redis.get(sessionId) - expect(JSON.parse(sessionValues)).deep.equal({age: Store.guardPair('age', 22)}) - }) - - it('should remove session key from redis on flush', function * () { - const config = new Config() - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'foo') - yield session.flush() - }).then(function () { - res.writeHead(200, {'content-type': 'application/json'}) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '102010' - yield supertest(server).get('/').set('Cookie', ['adonis-session=' + sessionId]).expect(200).end() - const sessionValues = yield this.redis.get(sessionId) - expect(sessionValues).to.equal(null) - }) - - it('should set proper ttl on the session id', function * () { - const config = new Config(null, {age: 120}) - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - yield session.put('name', 'foo') - }).then(function () { - res.writeHead(200, {'content-type': 'application/json'}) - res.end() - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '102010' - yield supertest(server).get('/').set('Cookie', ['adonis-session=' + sessionId]).expect(200).end() - const sessionTTL = yield this.redis.ttl(sessionId) - expect(sessionTTL).to.equal(120) - }) - - it('should return null when value does not exists in the session store', function * () { - const config = new Config(null, {age: 120}) - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.get('age') - }).then(function (age) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({age})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = '102010' - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=' + sessionId]).expect(200).end() - expect(res.body.age).to.equal(null) - }) - - it('should return null when the sessionId does not exists in the session store', function * () { - const config = new Config(null, {age: 120}) - Session.driver = new RedisDriver(this.Helpers, config, RedisFactory) - Session.config = config - - const server = http.createServer(function (req, res) { - const session = new Session(req, res) - co(function * () { - return yield session.get('age') - }).then(function (age) { - res.writeHead(200, {'content-type': 'application/json'}) - res.end(JSON.stringify({age})) - }).catch(function (err) { - res.writeHead(500, {'content-type': 'application/json'}) - res.end(JSON.stringify(err)) - }) - }) - - const sessionId = new Date().getTime() - const res = yield supertest(server).get('/').set('Cookie', ['adonis-session=' + sessionId]).expect(200).end() - expect(res.body.age).to.equal(null) - }) - }) -}) diff --git a/test/unit/uploads/npm-logo.svg b/test/unit/uploads/npm-logo.svg deleted file mode 100644 index 4330983d..00000000 --- a/test/unit/uploads/npm-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test/unit/view.form.spec.js b/test/unit/view.form.spec.js deleted file mode 100644 index 1c397b0c..00000000 --- a/test/unit/view.form.spec.js +++ /dev/null @@ -1,356 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const chai = require('chai') -const expect = chai.expect -const Form = require('../../src/View/Form') -const View = require('../../src/View') -const Route = require('../../src/Route') -const path = require('path') -const Helpers = { - viewsPath: function () { - return path.join(__dirname, './app/views') - } -} - -const Config = { - get: function () { - return false - } -} - -const form = new Form(new View(Helpers, Config, Route).viewsEnv, Route) - -describe('Form Helper', function () { - it('should be able to create a form opening tag using open method', function () { - const formTag = form.open({url: '/user', method: 'POST'}) - expect(formTag.val).to.equal('
') - }) - - it('should be able to set enctype to multiple form data when files are set to true', function () { - const formTag = form.open({url: '/user', method: 'POST', files: true}) - expect(formTag.val).to.equal('') - }) - - it('should be able to define additional attributes on form tag', function () { - const formTag = form.open({url: '/user', method: 'POST', files: true, novalidate: true}) - expect(formTag.val).to.equal('') - }) - - it('should be able to define multiple classes from formtag', function () { - const formTag = form.open({url: '/user', method: 'POST', class: 'form form--small'}) - expect(formTag.val).to.equal('') - }) - - it('should be able to define method other than POST', function () { - const formTag = form.open({url: '/', method: 'put'}) - expect(formTag.val).to.equal('') - }) - - it('should be able to define method other than POST and should respect existing query string values', function () { - const formTag = form.open({url: '/?page=1', method: 'put'}) - expect(formTag.val).to.equal('') - }) - - it('should be able to make url using route name', function () { - Route.post('/users', 'UserController.store').as('storeUser') - const formTag = form.open({route: 'storeUser', method: 'POST'}) - expect(formTag.val).to.equal('') - }) - - it("should throw an exception when you make url using a route name that doesn't exists", function () { - Route.post('/users', 'UserController.store').as('users.store') - const fn = () => form.open({ route: 'user.store', method: 'POST' }) - expect(fn).to.throw('RuntimeException: E_MISSING_ROUTE: The route user.store has not been found') - }) - - it("should throw an exception when you make url using a route handler that doesn't exists", function () { - const fn = () => form.open({ action: 'UserController.foo', method: 'POST' }) - expect(fn).to.throw('RuntimeException: E_MISSING_ROUTE_ACTION: The action UserController.foo has not been found') - }) - - it('should be able to make url using route name with params', function () { - Route.delete('/user/:id', 'UserController.delete').as('deleteUser') - const formTag = form.open({route: 'deleteUser', params: {id: 1}}) - expect(formTag.val).to.equal('') - }) - - it('should be able to make url using controller.action binding', function () { - Route.put('/users', 'UserController.update').as('getUsers') - const formTag = form.open({action: 'UserController.update'}) - expect(formTag.val).to.equal('') - }) - - it('should be able to create label', function () { - const label = form.label('email', 'Enter your email address') - expect(label.val).to.equal('') - }) - - it('should be able to define extra attributes with label', function () { - const label = form.label('email', 'Enter your email address', {class: 'flat'}) - expect(label.val).to.equal('') - }) - - it('should create an input box using text method', function () { - const label = form.text('email') - expect(label.val).to.equal('') - }) - - it('should be able to define extra attributes on input box', function () { - const label = form.text('email', null, {class: 'small'}) - expect(label.val).to.equal('') - }) - - it('should be able to define set placeholder attribute on input box', function () { - const label = form.text('email', null, {placeholder: 'Enter your email address'}) - expect(label.val).to.equal('') - }) - - it('should be able to define override id attribute on input box', function () { - const label = form.text('email', null, {id: 'my-email'}) - expect(label.val).to.equal('') - }) - - it('should create a password input using password method', function () { - const label = form.password('password') - expect(label.val).to.equal('') - }) - - it('should create an email input using password method', function () { - const label = form.email('email_address') - expect(label.val).to.equal('') - }) - - it('should create a file input using file method', function () { - const label = form.file('profile') - expect(label.val).to.equal('') - }) - - it('should create a date input using date method', function () { - const label = form.date('dob') - expect(label.val).to.equal('') - }) - - it('should create a color input using color method', function () { - const label = form.color('themecolor') - expect(label.val).to.equal('') - }) - - it('should be able to use old values for the input', function () { - const view = new View(Helpers, Config, Route) - view.global('old', function () { - return 'some value' - }) - const formNew = new Form(view.viewsEnv, Route) - const label = formNew.text('username') - expect(label.val).to.equal('') - }) - - it('should not use old value when avoidOld has been passed', function () { - const view = new View(Helpers, Config, Route) - view.global('old', function () { - return 'some value' - }) - const formNew = new Form(view.viewsEnv, Route) - const input = formNew.text('username', null, {avoidOld: true}) - expect(input.val).to.equal('') - }) - - it('should create a url input using url method', function () { - const label = form.url('http://github.com/blogLink') - expect(label.val).to.equal('') - }) - - it('should create a search input using search method', function () { - const label = form.search('search') - expect(label.val).to.equal('') - }) - - it('should create a hidden input using hidden method', function () { - const label = form.hidden('token') - expect(label.val).to.equal('') - }) - - it('should create a textarea using hidden method', function () { - const label = form.textarea('description') - expect(label.val).to.equal('') - }) - - it('should create be able to attach attributes to textarea', function () { - const label = form.textarea('description', null, {class: 'big'}) - expect(label.val).to.equal('') - }) - - it('should be able to define value for textarea', function () { - const label = form.textarea('description', 'Enter description') - expect(label.val).to.equal('') - }) - - it('should be able to use old values for textarea', function () { - const label = form.textarea('description', 'Enter description') - expect(label.val).to.equal('') - }) - - it('should create a radio button using radio method', function () { - const label = form.radio('sex', 'male') - expect(label.val).to.equal('') - }) - - it('should be to able to select radio button', function () { - const label = form.radio('sex', 'male', true) - expect(label.val).to.equal('') - }) - - it('should create a checkbox using checkbox method', function () { - const label = form.checkbox('admin', 'yes') - expect(label.val).to.equal('') - }) - - it('should be to able to select checkbox', function () { - const label = form.checkbox('admin', 'yes', true) - expect(label.val).to.equal('') - }) - - it('should be able to create a selectbox', function () { - const label = form.select('countries', ['India', 'Usa', 'Brazil']) - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define diffrent keys and values for options', function () { - const label = form.select('countries', {'ind': 'India', 'us': 'Usa'}) - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define attributes on select box', function () { - const label = form.select('countries', {'ind': 'India', 'us': 'Usa'}, null, null, {multiple: true}) - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define selected option on select box', function () { - const label = form.select('countries', {'ind': 'India', 'us': 'Usa'}, 'ind') - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define selected option on select box when using array', function () { - const label = form.select('countries', ['India', 'Usa', 'Brazil'], 'India') - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define multiple selected option on select box', function () { - const label = form.select('countries', {'ind': 'India', 'us': 'Usa'}, ['ind', 'us']) - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define empty input as first option', function () { - const label = form.select('countries', {'ind': 'India', 'us': 'Usa'}, null, 'Select Country') - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define range inside select box', function () { - const label = form.selectRange('number', 1, 4) - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should be able to define opposite range inside select box', function () { - const label = form.selectRange('number', 3, 0) - const expected = - `` - expect(label.val).to.equal(expected) - }) - - it('should create a submit button using submit method', function () { - const label = form.submit('Submit') - expect(label.val).to.equal('') - }) - - it('should be able to define extra attributes on submit button', function () { - const label = form.submit('Submit', null, {class: 'small'}) - expect(label.val).to.equal('') - }) - - it('should be able to define name of submit button', function () { - const label = form.submit('Create Account', 'create') - expect(label.val).to.equal('') - }) - - it('should create a button using button method', function () { - const label = form.button('Submit') - expect(label.val).to.equal('') - }) - - it('should be able to define extra attributes on button', function () { - const label = form.button('Submit', null, {class: 'big'}) - expect(label.val).to.equal('') - }) - - it('should be able to define a different name for button', function () { - const label = form.button('Create Account', 'create') - expect(label.val).to.equal('') - }) - - it('should create a reset button using resetButton method', function () { - const label = form.resetButton('Clear Form') - expect(label.val).to.equal('') - }) - - it('should create a reset button by passing type to reset inside button method', function () { - const label = form.button('Clear Form', 'clear', {type: 'reset'}) - expect(label.val).to.equal('') - }) -}) diff --git a/test/unit/view.spec.js b/test/unit/view.spec.js deleted file mode 100644 index 25ef0354..00000000 --- a/test/unit/view.spec.js +++ /dev/null @@ -1,202 +0,0 @@ -'use strict' - -/** - * adonis-framework - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. -*/ - -const View = require('../../src/View') -const Route = require('../../src/Route') -const chai = require('chai') -const path = require('path') -const expect = chai.expect -require('co-mocha') - -const Helpers = { - viewsPath: function () { - return path.join(__dirname, './app/views') - } -} -const Config = { - get: function () { - return true - } -} - -describe('View', function () { - before(function () { - this.view = new View(Helpers, Config, Route) - }) - - beforeEach(function () { - Route.new() - }) - - it('should throw an error when unable to find view', function * () { - try { - yield this.view.make('foo.html') - expect(true).to.be.false - } catch (e) { - expect(e.message).to.match(/template not found/) - } - }) - - it("should make a view using it's path", function * () { - const index = yield this.view.make('index.njk') - expect(index.trim()).to.equal('

Hello world

') - }) - - it("should make a view using it's path without .njk extension", function * () { - const index = yield this.view.make('index') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should make a nested view using a /', function * () { - const index = yield this.view.make('subviews/index') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should make a nested view using a / and the extension', function * () { - const index = yield this.view.make('subviews/index.njk') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should make a nested view using a .', function * () { - const index = yield this.view.make('subviews.index') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should make a nested view using a . and the extension', function * () { - const index = yield this.view.make('subviews.index.njk') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should include a view using a .', function * () { - const index = yield this.view.make('include') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should include a view by going a directory back', function * () { - const index = yield this.view.make('subviews.internal') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should extends a view using a .', function * () { - const index = yield this.view.make('extends') - expect(index.trim()).to.equal('

Hello world

') - }) - - it('should make use of route filter inside views', function * () { - Route.get('/:id', 'ProfileController.show').as('profile') - const profile = yield this.view.make('profile', {id: 1}) - expect(profile.trim()).to.equal('/1') - }) - - it('should make use of action filter inside views', function * () { - Route.get('/:id', 'ProfileController.show').as('profile') - const profile = yield this.view.make('profileAction', {id: 1}) - expect(profile.trim()).to.equal('/1') - }) - - it('should throw exception when unable to find route action inside route filter', function * () { - const fn = () => this.view.makeString('{{ "ProfileController.show" | action({id:1}) }}') - expect(fn).to.throw(/RuntimeException: E_MISSING_ROUTE_ACTION: The action ProfileController\.show has not been found/) - }) - - it('should make an anchor link to a given route', function * () { - Route.get('/:id', 'ProfileController.show').as('profile') - const viewString = this.view.makeString('{{ linkTo("profile", "View Profile", {id: 1}) }}') - expect(viewString.trim()).to.equal(' View Profile ') - }) - - it('should make an anchor link with defined target to a given route', function * () { - Route.get('/users', 'ProfileController.show').as('listUsers') - const viewString = this.view.makeString('{{ linkTo("listUsers", "View Profile", {}, "_blank") }}') - expect(viewString.trim()).to.equal(' View Profile ') - }) - - it('should make an anchor link to a given action', function * () { - Route.get('profile/:id', 'ProfileController.show') - const viewString = this.view.makeString('{{ linkToAction("ProfileController.show", "View Profile", {id: 1}) }}') - expect(viewString.trim()).to.equal(' View Profile ') - }) - - it('should make an anchor link with defined target to a given action', function * () { - Route.get('profile/:id', 'ProfileController.show') - const viewString = this.view.makeString('{{ linkToAction("ProfileController.show", "View Profile", {id: 1}, "_blank") }}') - expect(viewString.trim()).to.equal(' View Profile ') - }) - - it('should stringify json', function * () { - const jsonView = yield this.view.make('json', {profile: {name: 'virk'}}) - expect(jsonView.trim()).to.equal(JSON.stringify({name: 'virk'}, null, 2)) - }) - - it('should be able to make use of yield keyword inside view', function * () { - const profile = { - get: function * () { - return 'virk' - } - } - const asyncView = yield this.view.make('async', {profile}) - expect(asyncView.trim()).to.equal('virk') - }) - - it('should return error thrown by yield method', function * () { - const profile = { - get: function * () { - throw new Error('What you want?') - } - } - try { - yield this.view.make('async', {profile}) - } catch (e) { - expect(e.message).to.match(/What you/) - } - }) - - it('should add a filter using filter method', function * () { - this.view.filter('mycase', function (text) { - return text.toUpperCase() - }) - const view = yield this.view.make('filter') - expect(view.trim()).to.equal('VIRK') - }) - - it('should add a global using global method', function * () { - const time = new Date().getTime() - this.view.global('time', function () { - return time - }) - const view = yield this.view.make('global') - expect(view.trim()).to.equal(time.toString()) - }) - - it('should be able to make use of use method when injectServices is true', function * () { - new Date().getTime() - this.view.global('typeof', function (value) { - return typeof (value) - }) - const view = yield this.view.make('services') - expect(view.trim()).to.equal('function') - }) - - it('should not be able to make use of use method when injectServices is false', function * () { - new Date().getTime() - const customConfig = { - get: function () { - return false - } - } - const view = new View(Helpers, customConfig, Route) - view.global('typeof', function (value) { - return typeof (value) - }) - const compiledView = yield view.make('services') - expect(compiledView.trim()).to.equal('undefined') - }) -}) diff --git a/tests/ace/base_command.spec.ts b/tests/ace/base_command.spec.ts new file mode 100644 index 00000000..e61972ea --- /dev/null +++ b/tests/ace/base_command.spec.ts @@ -0,0 +1,291 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import sinon from 'sinon' +import { test } from '@japa/runner' +import { BaseCommand } from '../../modules/ace/main.js' +import { ListCommand } from '../../modules/ace/commands.js' +import { IgnitorFactory } from '../../factories/core/ignitor.js' +import { createAceKernel } from '../../modules/ace/create_kernel.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Base command', () => { + test('infer staysAlive and startApp flags from command options', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static options = { + startApp: true, + staysAlive: true, + } + } + + const kernel = createAceKernel(app) + const command = await kernel.create(MakeController, []) + const listCommand = await kernel.create(ListCommand, []) + + assert.isTrue(command.startApp) + assert.isTrue(command.staysAlive) + + assert.isUndefined(listCommand.startApp) + assert.isUndefined(listCommand.staysAlive) + }) + + test('execute command template methods', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + + stack: string[] = [] + async prepare() { + this.stack.push('prepare') + } + + async interact() { + this.stack.push('interact') + } + + async run() { + this.stack.push('run') + } + + async completed() { + this.stack.push('completed') + } + } + + const kernel = createAceKernel(app) + const command = await kernel.create(MakeController, []) + await command.exec() + + assert.deepEqual(command.stack, ['prepare', 'interact', 'run', 'completed']) + }) + + test('do not run template methods when do not exists', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + stack: string[] = [] + + async run() { + this.stack.push('run') + } + } + + const kernel = createAceKernel(app) + const command = await kernel.create(MakeController, []) + await command.exec() + + assert.deepEqual(command.stack, ['run']) + }) + + test('fail when prepare method raises exception', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async prepare() { + throw new Error('prepare failed') + } + } + + const kernel = createAceKernel(app) + kernel.ui.switchMode('raw') + kernel.errorHandler.render = async function (error: Error) { + command.logger.fatal(error) + } + + const command = await kernel.create(MakeController, []) + await command.exec() + + assert.equal(command.error.message, 'prepare failed') + assert.equal(command.exitCode, 1) + assert.lengthOf(command.logger.getLogs(), 1) + assert.equal(command.logger.getLogs()[0].stream, 'stderr') + }) + + test('fail when interact method raises exception', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async interact() { + throw new Error('interact failed') + } + } + + const kernel = createAceKernel(app) + kernel.ui.switchMode('raw') + kernel.errorHandler.render = async function (error: Error) { + command.logger.fatal(error) + } + + const command = await kernel.create(MakeController, []) + await command.exec() + + assert.equal(command.error.message, 'interact failed') + assert.equal(command.exitCode, 1) + assert.lengthOf(command.logger.getLogs(), 1) + assert.equal(command.logger.getLogs()[0].stream, 'stderr') + }) + + test('fail when run method raises exception', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async run() { + throw new Error('run failed') + } + } + + const kernel = createAceKernel(app) + kernel.ui.switchMode('raw') + kernel.errorHandler.render = async function (error: Error) { + command.logger.fatal(error) + } + + const command = await kernel.create(MakeController, []) + await command.exec() + + assert.equal(command.error.message, 'run failed') + assert.equal(command.exitCode, 1) + assert.lengthOf(command.logger.getLogs(), 1) + assert.equal(command.logger.getLogs()[0].stream, 'stderr') + }) + + test('do not print errors when completed method handles exception', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async run() { + throw new Error('run failed') + } + + async completed() { + return true + } + } + + const kernel = createAceKernel(app) + kernel.ui.switchMode('raw') + + const command = await kernel.create(MakeController, []) + await command.exec() + + assert.equal(command.error.message, 'run failed') + assert.equal(command.exitCode, 1) + assert.lengthOf(command.logger.getLogs(), 0) + }) + + test('print error when completed method does not handles exception', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async run() { + throw new Error('run failed') + } + + async completed() { + return false + } + } + + const kernel = createAceKernel(app) + kernel.ui.switchMode('raw') + kernel.errorHandler.render = async function (error: Error) { + command.logger.fatal(error) + } + + const command = await kernel.create(MakeController, []) + await command.exec() + + assert.equal(command.error.message, 'run failed') + assert.equal(command.exitCode, 1) + assert.lengthOf(command.logger.getLogs(), 1) + assert.equal(command.logger.getLogs()[0].stream, 'stderr') + }) + + test('throw exception when completed method raises exception', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static commandName: string = 'make:controller' + async completed() { + throw new Error('completed failed') + } + } + + const kernel = createAceKernel(app) + const command = await kernel.create(MakeController, []) + + await assert.rejects(() => command.exec(), 'completed failed') + }) + + test('call app terminate when main command terminate method is called', async () => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + class MakeController extends BaseCommand { + static options = { + startApp: true, + staysAlive: true, + } + } + + const kernel = createAceKernel(app) + const command = await kernel.create(MakeController, []) + const listCommand = await kernel.create(ListCommand, []) + + const appMock = sinon.mock(app) + appMock.expects('terminate').twice() + + kernel.getMainCommand = () => command + await command.terminate() + + kernel.getMainCommand = () => listCommand + await listCommand.terminate() + + appMock.verify() + }) +}) diff --git a/tests/ace/codemods.spec.ts b/tests/ace/codemods.spec.ts new file mode 100644 index 00000000..1aae37d9 --- /dev/null +++ b/tests/ace/codemods.spec.ts @@ -0,0 +1,305 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Codemods } from '../../modules/ace/codemods.js' +import { AceFactory } from '../../factories/core/ace.js' + +test.group('Codemods', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('get ts morph project', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + + const codemods = new Codemods(ace.app, ace.ui.logger) + const project = await codemods.getTsMorphProject() + + assert.exists(project) + }) + + test('reuse the same CodeTransformer instance', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + + const codemods = new Codemods(ace.app, ace.ui.logger) + const project1 = await codemods.getTsMorphProject() + const project2 = await codemods.getTsMorphProject() + + assert.deepEqual(project1, project2) + }) +}) + +test.group('Codemods | environment variables', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('define env variables', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + /** + * Creating .env file so that we can update it. + */ + await fs.create('.env', '') + + const codemods = new Codemods(ace.app, ace.ui.logger) + await codemods.defineEnvVariables({ CORS_MODE: 'strict', CORS_ENABLED: true }) + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update .env file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('.env', 'CORS_MODE=strict') + await assert.fileContains('.env', 'CORS_ENABLED=true') + }) + + test('do not insert env value in .env.example if specified', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + /** + * Creating .env file so that we can update it. + */ + await fs.create('.env', '') + await fs.create('.env.example', '') + + const codemods = new Codemods(ace.app, ace.ui.logger) + await codemods.defineEnvVariables( + { SECRET_VALUE: 'secret' }, + { omitFromExample: ['SECRET_VALUE'] } + ) + await assert.fileContains('.env', 'SECRET_VALUE=secret') + await assert.fileContains('.env.example', 'SECRET_VALUE=') + }) + + test('do not define env variables when file does not exists', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const codemods = new Codemods(ace.app, ace.ui.logger) + + await codemods.defineEnvVariables({ CORS_MODE: 'strict', CORS_ENABLED: true }) + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update .env file', + stream: 'stdout', + }, + ]) + + await assert.fileNotExists('.env') + }) + + test('define env variables validations', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + + /** + * Creating .env file so that we can update it. + */ + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + + const codemods = new Codemods(ace.app, ace.ui.logger) + + await codemods.defineEnvValidations({ + variables: { + CORS_MODE: 'Env.schema.string()', + }, + }) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update start/env.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('start/env.ts', 'CORS_MODE: Env.schema.string()') + }) +}) + +test.group('Codemods | rcFile', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('update rcfile', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', 'export default defineConfig({})') + + const codemods = new Codemods(ace.app, ace.ui.logger) + + await codemods.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/core') + rcFile.addCommand('@adonisjs/core/commands') + }) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('adonisrc.ts', '@adonisjs/core') + await assert.fileContains('adonisrc.ts', '@adonisjs/core/commands') + }) +}) + +test.group('Codemods | registerMiddleware', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('register middleware', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('start/kernel.ts', 'router.use([])') + + const codemods = new Codemods(ace.app, ace.ui.logger) + await codemods.registerMiddleware('router', [{ path: '@adonisjs/core/bodyparser_middleware' }]) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update start/kernel.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('start/kernel.ts', '@adonisjs/core/bodyparser_middleware') + }) +}) + +test.group('Codemods | registerPolicies', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('register bouncer policies', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('app/policies/main.ts', 'export const policies = {}') + + const codemods = new Codemods(ace.app, ace.ui.logger) + await codemods.registerPolicies([{ name: 'PostPolicy', path: '#policies/post_policy' }]) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update app/policies/main.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('app/policies/main.ts', '#policies/post_policy') + }) +}) + +test.group('Codemods | registerVitePlugin', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('register vite plugin', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.createJson('package.json', {}) + await fs.create('vite.config.ts', 'export default { plugins: [] }') + + const codemods = new Codemods(ace.app, ace.ui.logger) + await codemods.registerVitePlugin('vue()', [ + { identifier: 'vue', module: '@vitejs/plugin-vue', isNamed: false }, + ]) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update vite.config.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('vite.config.ts', 'vue()') + }) +}) + +test.group('Codemods | registerJapaPlugin', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('register japa plugin', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.createJson('package.json', {}) + await fs.create('tests/bootstrap.ts', 'export const plugins = []') + + const codemods = new Codemods(ace.app, ace.ui.logger) + await codemods.registerJapaPlugin('apiClient()', [ + { identifier: 'apiClient', module: '@japa/api-client', isNamed: true }, + ]) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update tests/bootstrap.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('tests/bootstrap.ts', 'apiClient()') + }) +}) + +test.group('Codemods | install packages', (group) => { + group.tap((t) => t.timeout(60 * 1000)) + + test('install packages', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + + await fs.createJson('tsconfig.json', {}) + await fs.createJson('package.json', {}) + await fs.create('app/policies/main.ts', 'export const policies = {}') + + const codemods = new Codemods(ace.app, ace.ui.logger) + await codemods.installPackages([{ name: '@adonisjs/assembler@next', isDevDependency: true }]) + + await assert.dirExists('node_modules/@adonisjs/assembler') + }) + + test('install packages in verbose mode', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + + await fs.createJson('tsconfig.json', {}) + await fs.createJson('package.json', {}) + await fs.create('app/policies/main.ts', 'export const policies = {}') + + const codemods = new Codemods(ace.app, ace.ui.logger) + codemods.verboseInstallOutput = true + await codemods.installPackages([{ name: '@adonisjs/assembler@next', isDevDependency: true }]) + + await assert.dirExists('node_modules/@adonisjs/assembler') + }) +}) diff --git a/tests/ace/kernel.spec.ts b/tests/ace/kernel.spec.ts new file mode 100644 index 00000000..13983a9e --- /dev/null +++ b/tests/ace/kernel.spec.ts @@ -0,0 +1,131 @@ +/* + * @adonisjs/ace + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import sinon from 'sinon' +import { test } from '@japa/runner' +import { HelpCommand } from '../../modules/ace/main.js' +import { IgnitorFactory } from '../../factories/core/ignitor.js' +import { createAceKernel } from '../../modules/ace/create_kernel.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Kernel', () => { + test('create kernel instance with global flags', async ({ assert }) => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + const kernel = createAceKernel(app) + assert.deepEqual(kernel.flags, [ + { + name: 'ansi', + flagName: 'ansi', + required: false, + type: 'boolean', + showNegatedVariantInHelp: true, + description: 'Force enable or disable colorful output', + }, + { + name: 'help', + flagName: 'help', + required: false, + type: 'boolean', + description: 'View help for a given command', + }, + ]) + }) + + test('turn off colors when --no-ansi flag is mentioned', async () => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + const kernel = createAceKernel(app) + const switchMode = sinon.spy(kernel.ui.switchMode) + + await kernel.handle(['--no-ansi']) + switchMode.calledWith('silent') + }) + + test('turn off colors when --no-ansi flag is mentioned', async () => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + const kernel = createAceKernel(app) + const switchMode = sinon.spy(kernel.ui.switchMode) + + await kernel.handle(['--no-ansi']) + switchMode.calledWith('silent') + }) + + test('turn on colors when --ansi flag is mentioned', async () => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + const kernel = createAceKernel(app) + const switchMode = sinon.spy(kernel.ui.switchMode) + + await kernel.handle(['--ansi']) + switchMode.calledWith('normal') + }) + + test('display command help when --help flag is mentioned', async () => { + const ignitor = new IgnitorFactory().withCoreConfig().create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + + const kernel = createAceKernel(app) + const execMock = sinon.mock(HelpCommand.prototype) + execMock.expects('exec') + + await kernel.handle(['--help']) + execMock.verify() + }) + + test('load commands from a module identifier', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + commands: ['../commands'], + }, + }) + .create(BASE_URL, { + importer: (filePath) => { + if (filePath === '../commands') { + return { + async getMetaData() { + return [ + { + commandName: 'make:controller', + aliases: [], + }, + ] + }, + } + } + import(filePath) + }, + }) + + const app = ignitor.createApp('console') + await app.init() + + const kernel = createAceKernel(app) + await kernel.boot() + assert.exists(kernel.getCommand('make:controller')) + }) +}) diff --git a/tests/bindings/edge.spec.ts b/tests/bindings/edge.spec.ts new file mode 100644 index 00000000..fcc934de --- /dev/null +++ b/tests/bindings/edge.spec.ts @@ -0,0 +1,81 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import edge from 'edge.js' +import { test } from '@japa/runner' + +import '../../providers/edge_provider.js' +import { HttpContextFactory } from '../../factories/http.js' +import { IgnitorFactory } from '../../factories/core/ignitor.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Bindings | Edge', () => { + test('register edge globals', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [ + () => import('../../providers/app_provider.js'), + () => import('../../providers/edge_provider.js'), + ], + }, + }) + .withCoreConfig() + .create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + + assert.equal(edge.globals.config('app.appKey'), 'averylongrandomsecretkey') + assert.isTrue(edge.globals.config.has('app.appKey')) + assert.isFalse(edge.globals.config.has('foobar')) + assert.strictEqual(edge.globals.app, app) + + const router = await app.container.make('router') + router.get('/users/:id', () => {}) + router.commit() + + assert.equal(edge.globals.route('/users/:id', [1]), '/users/1') + assert.match(edge.globals.signedRoute('/users/:id', [1]), /\/users\/1\?signature=/) + }) + + test('render template using router', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [ + () => import('../../providers/app_provider.js'), + () => import('../../providers/edge_provider.js'), + ], + }, + }) + .withCoreConfig() + .create(BASE_URL) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + + edge.registerTemplate('welcome', { + template: `Hello {{ username }}`, + }) + + const router = await app.container.make('router') + router.on('/').render('welcome', { username: 'virk' }) + router.commit() + + const route = router.match('/', 'GET') + const ctx = new HttpContextFactory().create() + + await route?.route.execute(route.route, app.container.createResolver(), ctx, () => {}) + assert.equal(ctx.response.getBody(), 'Hello virk') + }) +}) diff --git a/tests/bindings/repl.spec.ts b/tests/bindings/repl.spec.ts new file mode 100644 index 00000000..7b966136 --- /dev/null +++ b/tests/bindings/repl.spec.ts @@ -0,0 +1,99 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import is from '../../src/helpers/is.js' +import stringHelpers from '../../src/helpers/string.js' +import { IgnitorFactory } from '../../factories/core/ignitor.js' +import AppServiceProvider from '../../providers/app_provider.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Bindings | Repl', () => { + test('load services to REPL context', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [ + () => import('../../providers/app_provider.js'), + () => import('../../providers/hash_provider.js'), + () => import('../../providers/repl_provider.js'), + ], + }, + }) + .withCoreConfig() + .create(BASE_URL, { + importer(filePath: string) { + return import(new URL(filePath, new URL('../', import.meta.url)).href) + }, + }) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + app.makeURL() + + /** + * Setting up REPL with fake server + * and context + */ + const repl = await app.container.make('repl') + repl.server = { + context: {}, + displayPrompt() {}, + } as any + + /** + * Define REPL bindings + */ + // await new ReplServiceProvider(app).boot() + const methods = repl.getMethods() + + await methods.loadEncryption.handler(repl) + assert.deepEqual(repl.server!.context.encryption, await app.container.make('encryption')) + + await methods.loadApp.handler(repl) + assert.deepEqual(repl.server!.context.app, await app.container.make('app')) + + await methods.loadHash.handler(repl) + assert.deepEqual(repl.server!.context.hash, await app.container.make('hash')) + + await methods.loadRouter.handler(repl) + assert.deepEqual(repl.server!.context.router, await app.container.make('router')) + + await methods.loadConfig.handler(repl) + assert.deepEqual(repl.server!.context.config, await app.container.make('config')) + + await methods.loadTestUtils.handler(repl) + assert.deepEqual(repl.server!.context.testUtils, await app.container.make('testUtils')) + + await methods.loadHelpers.handler(repl) + assert.deepEqual(repl.server!.context.helpers.string, stringHelpers) + assert.deepEqual(repl.server!.context.helpers.is, is) + + const output = await methods.importDefault.handler(repl, '../providers/app_provider.js') + assert.deepEqual(output, AppServiceProvider) + + const router = await methods.make.handler(repl, 'router') + assert.deepEqual(router, await app.container.make('router')) + + const exportedMods = await methods.importAll.handler(repl, '../../../factories') + assert.properties(exportedMods, [ + 'core', + 'app', + 'bodyparser', + 'encryption', + 'events', + 'hash', + 'logger', + 'http', + ]) + }) +}) diff --git a/tests/bindings/vinejs.spec.ts b/tests/bindings/vinejs.spec.ts new file mode 100644 index 00000000..15798477 --- /dev/null +++ b/tests/bindings/vinejs.spec.ts @@ -0,0 +1,156 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import vine from '@vinejs/vine' +import { test } from '@japa/runner' + +import { MultipartFileFactory } from '../../factories/bodyparser.js' +import '../../providers/vinejs_provider.js' + +test.group('Bindings | VineJS', () => { + test('clone schema type', async ({ assert }) => { + const file = vine.file() + assert.notStrictEqual(file, file.clone()) + }) + + test('raise error when value is not a file', async ({ assert }) => { + const validator = vine.compile( + vine.object({ + avatar: vine.file(), + }) + ) + + try { + await validator.validate({ + avatar: 'foo', + }) + } catch (error) { + assert.deepEqual(error.messages, [ + { + field: 'avatar', + message: 'The avatar must be a file', + rule: 'file', + }, + ]) + } + }) + + test('raise error when file size is greater than the allowed size', async ({ assert }) => { + const validator = vine.compile( + vine.object({ + avatar: vine.file({ size: '2mb' }), + }) + ) + + try { + await validator.validate({ + avatar: new MultipartFileFactory() + .merge({ + size: 4000000, + }) + .create(), + }) + } catch (error) { + assert.deepEqual(error.messages, [ + { + field: 'avatar', + message: 'File size should be less than 2MB', + rule: 'file.size', + meta: { + size: '2mb', + }, + }, + ]) + } + }) + + test('raise error when file extension is not allowed', async ({ assert }) => { + const validator = vine.compile( + vine.object({ + avatar: vine.file({ extnames: ['jpg'] }), + }) + ) + + try { + await validator.validate({ + avatar: new MultipartFileFactory() + .merge({ + size: 4000000, + extname: 'png', + }) + .create(), + }) + } catch (error) { + assert.deepEqual(error.messages, [ + { + field: 'avatar', + message: 'Invalid file extension png. Only jpg is allowed', + rule: 'file.extname', + meta: { + extnames: ['jpg'], + }, + }, + ]) + } + }) + + test('compute file options lazily', async ({ assert }) => { + const validator = vine.compile( + vine.object({ + avatar: vine.file(() => { + return { extnames: ['jpg'] } + }), + }) + ) + + try { + await validator.validate({ + avatar: new MultipartFileFactory() + .merge({ + size: 4000000, + extname: 'png', + }) + .create(), + }) + } catch (error) { + assert.deepEqual(error.messages, [ + { + field: 'avatar', + message: 'Invalid file extension png. Only jpg is allowed', + rule: 'file.extname', + meta: { + extnames: ['jpg'], + }, + }, + ]) + } + }) + + test('pass validation when file is valid', async ({ assert }) => { + const validator = vine.compile( + vine.object({ + avatar: vine.file(() => { + return { extnames: ['jpg'] } + }), + }) + ) + + const { avatar } = await validator.validate({ + avatar: new MultipartFileFactory() + .merge({ + size: 4000000, + extname: 'jpg', + }) + .create(), + }) + + assert.equal(avatar.size, 4000000) + assert.lengthOf(avatar.errors, 0) + }) +}) diff --git a/tests/cli_formatters/routes_list.spec.ts b/tests/cli_formatters/routes_list.spec.ts new file mode 100644 index 00000000..06359b27 --- /dev/null +++ b/tests/cli_formatters/routes_list.spec.ts @@ -0,0 +1,1024 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import type { ApplicationService } from '../../src/types.js' +import { IgnitorFactory } from '../../factories/core/ignitor.js' +import { createAceKernel } from '../../modules/ace/create_kernel.js' +import { RoutesListFormatter } from '../../src/cli_formatters/routes_list.js' + +/** + * Registers routes for testing + */ +async function registerRoutes(app: ApplicationService) { + class AboutController { + async handle() {} + } + class UsersController { + async handle() {} + } + class AuthMiddleware { + async handle() {} + } + const ContactController = () => import('#controllers/contacts_controller' as any) + + const router = await app.container.make('router') + const middleware = router.named({ + auth: async () => { + return { + default: AuthMiddleware, + } + }, + throttle: async () => { + return { + default: class ThrottleMiddleware { + async handle() {} + }, + } + }, + signed: async () => { + return { + default: class SignedMiddleware { + async handle() {} + }, + } + }, + acl: async () => import('#middleware/acl_middleware' as any), + }) + + router.use([ + async () => { + return { + default: class BodyParserMiddleware { + async handle() {} + }, + } + }, + ]) + + router.get('/', () => {}) + router.get('/files/:directory/*', () => {}) + router.get('/home', '#controllers/home_controller').as('home') + router + .get('/about', [AboutController]) + .as('about') + .use(() => {}) + + router.post('/contact', [ContactController, 'store']).as('contact.store') + router.get('/contact', [ContactController, 'create']).as('contact.create') + + router + .get('users', [UsersController, 'handle']) + .use(middleware.auth()) + .use(function canViewUsers() {}) + .use(() => {}) + + router + .get('payments', [() => import('#controllers/payments_controller' as any), 'index']) + .use(middleware.auth()) + .use(middleware.acl()) + .use(middleware.signed()) + .use(middleware.throttle()) + + router + .get('/articles', [() => import('#controllers/articles_controller' as any), 'index']) + .as('articles') + .domain('blog.adonisjs.com') + + router + .get('/articles/:id/:slug?', [() => import('#controllers/articles_controller' as any), 'show']) + .as('articles.show') + .domain('blog.adonisjs.com') + + router.on('/blog').redirect('/articles') +} + +test.group('Formatters | List routes | toJSON', () => { + test('format routes as JSON', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter(router, createAceKernel(app).ui, {}, {}) + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: '', + pattern: '/', + methods: ['GET'], + handler: { + type: 'closure', + name: 'closure', + args: undefined, + }, + middleware: [], + }, + { + name: '', + pattern: '/files/:directory/*', + methods: ['GET'], + handler: { + type: 'closure', + name: 'closure', + args: undefined, + }, + middleware: [], + }, + { + name: 'home', + pattern: '/home', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/home_controller', + method: 'handle', + }, + middleware: [], + }, + { + name: 'about', + pattern: '/about', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: 'AboutController', + method: 'handle', + }, + middleware: ['closure'], + }, + { + name: 'contact.store', + pattern: '/contact', + methods: ['POST'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'store', + }, + middleware: [], + }, + { + name: 'contact.create', + pattern: '/contact', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'create', + }, + middleware: [], + }, + { + name: '', + pattern: '/users', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: 'UsersController', + method: 'handle', + }, + middleware: ['auth', 'canViewUsers', 'closure'], + }, + { + name: '', + pattern: '/payments', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/payments_controller', + method: 'index', + }, + middleware: ['auth', 'acl', 'signed', 'throttle'], + }, + { + handler: { + args: '/articles', + name: 'redirectsToRoute', + type: 'closure', + }, + methods: ['GET'], + middleware: [], + name: '', + pattern: '/blog', + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [ + { + pattern: '/articles', + name: 'articles', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/articles_controller', + method: 'index', + }, + middleware: [], + }, + { + pattern: '/articles/:id/:slug?', + name: 'articles.show', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/articles_controller', + method: 'show', + }, + middleware: [], + }, + ], + }, + ]) + }) + + test('show HEAD routes', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + { displayHeadRoutes: true }, + {} + ) + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: '', + pattern: '/', + methods: ['GET', 'HEAD'], + handler: { + type: 'closure', + name: 'closure', + args: undefined, + }, + middleware: [], + }, + { + name: '', + pattern: '/files/:directory/*', + methods: ['GET', 'HEAD'], + handler: { + type: 'closure', + name: 'closure', + args: undefined, + }, + middleware: [], + }, + { + name: 'home', + pattern: '/home', + methods: ['GET', 'HEAD'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/home_controller', + method: 'handle', + }, + middleware: [], + }, + { + name: 'about', + pattern: '/about', + methods: ['GET', 'HEAD'], + handler: { + type: 'controller', + moduleNameOrPath: 'AboutController', + method: 'handle', + }, + middleware: ['closure'], + }, + { + name: 'contact.store', + pattern: '/contact', + methods: ['POST'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'store', + }, + middleware: [], + }, + { + name: 'contact.create', + pattern: '/contact', + methods: ['GET', 'HEAD'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'create', + }, + middleware: [], + }, + { + name: '', + pattern: '/users', + methods: ['GET', 'HEAD'], + handler: { + type: 'controller', + moduleNameOrPath: 'UsersController', + method: 'handle', + }, + middleware: ['auth', 'canViewUsers', 'closure'], + }, + { + name: '', + pattern: '/payments', + methods: ['GET', 'HEAD'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/payments_controller', + method: 'index', + }, + middleware: ['auth', 'acl', 'signed', 'throttle'], + }, + { + handler: { + args: '/articles', + name: 'redirectsToRoute', + type: 'closure', + }, + methods: ['GET', 'HEAD'], + middleware: [], + name: '', + pattern: '/blog', + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [ + { + pattern: '/articles', + name: 'articles', + methods: ['GET', 'HEAD'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/articles_controller', + method: 'index', + }, + middleware: [], + }, + { + pattern: '/articles/:id/:slug?', + name: 'articles.show', + methods: ['GET', 'HEAD'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/articles_controller', + method: 'show', + }, + middleware: [], + }, + ], + }, + ]) + }) + + test('format routes as ANSI list', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const cliUi = createAceKernel(app).ui + cliUi.switchMode('silent') + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + cliUi, + { + maxPrettyPrintWidth: 100, + }, + {} + ) + + assert.deepEqual(await formatter.formatAsAnsiList(), [ + { + heading: '', + rows: [ + `METHOD ROUTE ................................................... HANDLER MIDDLEWARE`, + `GET / ....................................................... closure `, + `GET /files/:directory/* ..................................... closure `, + `GET /home (home) ................ #controllers/home_controller.handle `, + `GET /about (about) ........................... AboutController.handle closure`, + `POST /contact (contact.store) . #controllers/contacts_controller.store `, + `GET /contact (contact.create) #controllers/contacts_controller.crea… `, + `GET /users ................................... UsersController.handle auth, canViewUsers, closure`, + `GET /payments ................ #controllers/payments_controller.index auth, acl, and 2 more`, + `GET /blog .............................. redirectsToRoute(/articles) `, + ], + }, + { + heading: + '.. blog.adonisjs.com ...............................................................................', + rows: [ + `METHOD ROUTE .................................................................... HANDLER MIDDLEWARE`, + `GET /articles (articles) ...................... #controllers/articles_controller.index `, + `GET /articles/:id/:slug? (articles.show) ....... #controllers/articles_controller.show `, + ], + }, + ]) + }) + + test('format routes as ANSI table', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const cliUi = createAceKernel(app).ui + cliUi.switchMode('raw') + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + cliUi, + { + maxPrettyPrintWidth: 100, + }, + {} + ) + + const tables = await formatter.formatAsAnsiTable() + tables[0].table.render() + + assert.deepEqual(cliUi.logger.getLogs(), [ + { + message: 'dim(METHOD)|dim(ROUTE)|dim(HANDLER)|dim(MIDDLEWARE)', + stream: 'stdout', + }, + { + message: `dim(GET)|/ | cyan(closure)|dim()`, + stream: 'stdout', + }, + { + message: `dim(GET)|/files/yellow(:directory)/red(*) | cyan(closure)|dim()`, + stream: 'stdout', + }, + { + message: `dim(GET)|/home dim((home)) | cyan(#controllers/home_controller).cyan(handle)|dim()`, + stream: 'stdout', + }, + { + message: `dim(GET)|/about dim((about)) | cyan(AboutController).cyan(handle)|dim(closure)`, + stream: 'stdout', + }, + { + message: `dim(POST)|/contact dim((contact.store)) | cyan(#controllers/contacts_controller).cyan(store)|dim()`, + stream: 'stdout', + }, + { + message: `dim(GET)|/contact dim((contact.create)) | cyan(#controllers/contacts_controller).cyan(create)|dim()`, + stream: 'stdout', + }, + { + message: `dim(GET)|/users | cyan(UsersController).cyan(handle)|dim(auth, canViewUsers, closure)`, + stream: 'stdout', + }, + { + message: `dim(GET)|/payments | cyan(#controllers/payments_controller).cyan(index)|dim(auth, acl, signed, throttle)`, + stream: 'stdout', + }, + { + message: `dim(GET)|/blog | cyan(redirectsToRoute)dim((/articles))|dim()`, + stream: 'stdout', + }, + ]) + }) +}) + +test.group('Formatters | List routes | filters', () => { + test('show routes that has one or more middleware', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + {}, + { + middleware: ['*'], + } + ) + + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: 'about', + pattern: '/about', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: 'AboutController', + method: 'handle', + }, + middleware: ['closure'], + }, + { + name: '', + pattern: '/users', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: 'UsersController', + method: 'handle', + }, + middleware: ['auth', 'canViewUsers', 'closure'], + }, + { + name: '', + pattern: '/payments', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/payments_controller', + method: 'index', + }, + middleware: ['auth', 'acl', 'signed', 'throttle'], + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [], + }, + ]) + }) + + test('show routes that has zero middleware', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + {}, + { + ignoreMiddleware: ['*'], + } + ) + + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: '', + pattern: '/', + methods: ['GET'], + handler: { + type: 'closure', + name: 'closure', + args: undefined, + }, + middleware: [], + }, + { + name: '', + pattern: '/files/:directory/*', + methods: ['GET'], + handler: { + type: 'closure', + name: 'closure', + args: undefined, + }, + middleware: [], + }, + { + name: 'home', + pattern: '/home', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/home_controller', + method: 'handle', + }, + middleware: [], + }, + { + name: 'contact.store', + pattern: '/contact', + methods: ['POST'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'store', + }, + middleware: [], + }, + { + name: 'contact.create', + pattern: '/contact', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'create', + }, + middleware: [], + }, + { + handler: { + args: '/articles', + name: 'redirectsToRoute', + type: 'closure', + }, + methods: ['GET'], + middleware: [], + name: '', + pattern: '/blog', + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [ + { + pattern: '/articles', + name: 'articles', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/articles_controller', + method: 'index', + }, + middleware: [], + }, + { + pattern: '/articles/:id/:slug?', + name: 'articles.show', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/articles_controller', + method: 'show', + }, + middleware: [], + }, + ], + }, + ]) + }) + + test('show routes that has specific middleware', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + {}, + { + middleware: ['auth'], + } + ) + + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: '', + pattern: '/users', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: 'UsersController', + method: 'handle', + }, + middleware: ['auth', 'canViewUsers', 'closure'], + }, + { + name: '', + pattern: '/payments', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/payments_controller', + method: 'index', + }, + middleware: ['auth', 'acl', 'signed', 'throttle'], + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [], + }, + ]) + }) + + test('combine middleware and ignoreMiddleware filters', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + {}, + { + middleware: ['auth'], + ignoreMiddleware: ['acl'], + } + ) + + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: '', + pattern: '/users', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: 'UsersController', + method: 'handle', + }, + middleware: ['auth', 'canViewUsers', 'closure'], + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [], + }, + ]) + }) + + test('show routes by controller name', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + {}, + { + middleware: ['auth'], + match: 'UsersController', + } + ) + + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: '', + pattern: '/users', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: 'UsersController', + method: 'handle', + }, + middleware: ['auth', 'canViewUsers', 'closure'], + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [], + }, + ]) + }) + + test('show routes by route name', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + {}, + { + middleware: ['auth'], + match: 'contact.', + } + ) + + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: 'contact.store', + pattern: '/contact', + methods: ['POST'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'store', + }, + middleware: [], + }, + { + name: 'contact.create', + pattern: '/contact', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'create', + }, + middleware: [], + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [], + }, + ]) + }) + + test('show routes by pattern name', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .create(fs.baseUrl) + + const app = ignitor.createApp('console') + await app.init() + await app.boot() + await registerRoutes(app) + + const router = await app.container.make('router') + const formatter = new RoutesListFormatter( + router, + createAceKernel(app).ui, + {}, + { + middleware: ['auth'], + match: '/contact', + } + ) + + assert.deepEqual(await formatter.formatAsJSON(), [ + { + domain: 'root', + routes: [ + { + name: 'contact.store', + pattern: '/contact', + methods: ['POST'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'store', + }, + middleware: [], + }, + { + name: 'contact.create', + pattern: '/contact', + methods: ['GET'], + handler: { + type: 'controller', + moduleNameOrPath: '#controllers/contacts_controller', + method: 'create', + }, + middleware: [], + }, + ], + }, + { + domain: 'blog.adonisjs.com', + routes: [], + }, + ]) + }) +}) diff --git a/tests/commands/add.spec.ts b/tests/commands/add.spec.ts new file mode 100644 index 00000000..b98c7066 --- /dev/null +++ b/tests/commands/add.spec.ts @@ -0,0 +1,331 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { test } from '@japa/runner' +import { ListLoader } from '@adonisjs/ace' +import type { FileSystem } from '@japa/file-system' + +import Add from '../../commands/add.js' +import Configure from '../../commands/configure.js' +import { AceFactory } from '../../factories/core/ace.js' + +const VERBOSE = !!process.env.CI + +/** + * Setup a fake adonis project in the file system + */ +async function setupProject(fs: FileSystem, pkgManager?: 'npm' | 'pnpm' | 'yarn' | 'yarn@berry') { + await fs.create( + 'package.json', + JSON.stringify({ type: 'module', name: 'test', dependencies: {} }) + ) + + if (pkgManager === 'pnpm') { + await fs.create('pnpm-lock.yaml', '') + } else if (pkgManager === 'yarn' || pkgManager === 'yarn@berry') { + await fs.create('yarn.lock', '') + } else { + await fs.create('package-lock.json', '') + } + + await fs.create('tsconfig.json', JSON.stringify({ compilerOptions: {} })) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('start/kernel.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('.env', '') +} + +/** + * Setup a fake package inside the node_modules directory + */ +async function setupPackage(fs: FileSystem, configureContent?: string) { + await fs.create( + 'node_modules/foo/package.json', + JSON.stringify({ type: 'module', name: 'test', main: 'index.js', dependencies: {} }) + ) + + await fs.create( + 'node_modules/foo/index.js', + `export const stubsRoot = './' + export async function configure(command) { ${configureContent} }` + ) +} + +test.group('Install', (group) => { + group.tap((t) => t.disableTimeout()) + + test('detect correct pkg manager ( npm )', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'npm') + await setupPackage(fs) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [new URL('node_modules/foo', fs.baseUrl).href]) + command.verbose = VERBOSE + + await command.exec() + + await assert.fileIsNotEmpty('package-lock.json') + }) + + test('detect correct pkg manager ( pnpm )', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'pnpm') + await setupPackage(fs) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [new URL('node_modules/foo', fs.baseUrl).href]) + command.verbose = VERBOSE + + await command.exec() + + await assert.fileIsNotEmpty('pnpm-lock.yaml') + }) + + test('use specific package manager', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'npm') + await setupPackage(fs) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [new URL('node_modules/foo', fs.baseUrl).href]) + command.verbose = VERBOSE + command.packageManager = 'pnpm' + + await command.exec() + + await assert.fileIsNotEmpty('pnpm-lock.yaml') + }) + + test('should install dependency', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'npm') + await setupPackage(fs) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [new URL('node_modules/foo', fs.baseUrl).href]) + command.verbose = VERBOSE + + await command.exec() + + await assert.fileContains('package.json', 'foo') + }) + + test('should install dev dependency', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'npm') + await setupPackage(fs) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [new URL('node_modules/foo', fs.baseUrl).href, '-D']) + command.verbose = VERBOSE + + await command.exec() + + const pkgJson = await fs.contentsJson('package.json') + assert.deepEqual(pkgJson.devDependencies, { test: 'file:node_modules/foo' }) + }) + + test('pass unknown args to configure', async ({ fs, assert }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'npm') + await setupPackage( + fs, + ` + command.logger.log(command.parsedFlags) + ` + ) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [ + new URL('node_modules/foo', fs.baseUrl).href, + '--foo', + '--auth=session', + '-x', + ]) + command.verbose = VERBOSE + + await command.exec() + + const logs = command.logger.getLogs() + + assert.deepInclude(logs, { + message: { foo: 'true', auth: 'session', x: 'true', ...(VERBOSE ? { verbose: true } : {}) }, + stream: 'stdout', + }) + }) + + test('should configure package', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'pnpm') + await setupPackage( + fs, + ` const codemods = await command.createCodemods() + await codemods.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/cache/cache_provider') + })` + ) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [new URL('node_modules/foo', fs.baseUrl).href]) + command.verbose = VERBOSE + + await command.exec() + + await assert.fileContains('adonisrc.ts', '@adonisjs/cache/cache_provider') + }) + + test('display error and stop if package install fail', async ({ fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'pnpm') + await setupPackage(fs) + + await ace.app.init() + + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [ + new URL('node_modules/inexistent', fs.baseUrl).toString(), + ]) + command.verbose = VERBOSE + + await command.exec() + + command.assertExitCode(1) + command.assertLogMatches(/Process exited with non-zero status/) + }) + + test('display error if configure fail', async ({ fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(join(filePath, `index.js?${Math.random()}`)), + }) + + await setupProject(fs, 'pnpm') + await setupPackage(fs, 'throw new Error("Invalid configure")') + + await ace.app.init() + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, [new URL('node_modules/foo', fs.baseUrl).href]) + command.verbose = VERBOSE + ace.errorHandler.render = async function (error: Error) { + command.logger.fatal(error) + } + + await command.exec() + + command.assertExitCode(1) + command.assertLogMatches(/Unable to configure/) + }) + + test('configure edge', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(filePath), + }) + + await setupProject(fs, 'pnpm') + + await ace.app.init() + ace.addLoader(new ListLoader([Configure])) + ace.ui.switchMode('raw') + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, ['edge']) + command.verbose = VERBOSE + + await command.exec() + + await assert.fileContains('package.json', 'edge.js') + await assert.fileContains('adonisrc.ts', '@adonisjs/core/providers/edge_provider') + }) + + test('configure vinejs', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(filePath), + }) + + await setupProject(fs, 'pnpm') + + await ace.app.init() + ace.addLoader(new ListLoader([Configure])) + ace.prompt.trap('install').accept() + + const command = await ace.create(Add, ['vinejs']) + command.verbose = VERBOSE + + await command.exec() + + await assert.fileContains('package.json', '@vinejs/vine') + await assert.fileContains('adonisrc.ts', '@adonisjs/core/providers/vinejs_provider') + }) +}) diff --git a/tests/commands/build.spec.ts b/tests/commands/build.spec.ts new file mode 100644 index 00000000..4b9bf92a --- /dev/null +++ b/tests/commands/build.spec.ts @@ -0,0 +1,404 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import ts from 'typescript' +import { execa } from 'execa' +import { test } from '@japa/runner' +import Build from '../../commands/build.js' +import { AceFactory } from '../../factories/core/ace.js' + +test.group('Build command', (group) => { + group.tap((t) => t.timeout(30 * 1000)) + + test('show error when assembler is not installed', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === '@adonisjs/assembler') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + await command.exec() + + assert.equal(command.exitCode, 1) + assert.lengthOf(ace.ui.logger.getLogs(), 1) + assert.equal(ace.ui.logger.getLogs()[0].stream, 'stderr') + assert.match(ace.ui.logger.getLogs()[0].message, /Cannot find package "@adonisjs\/assembler/) + }) + + test('show error when typescript is not installed', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === 'typescript') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + await command.exec() + + assert.equal(command.exitCode, 1) + assert.lengthOf(ace.ui.logger.getLogs(), 1) + assert.equal(ace.ui.logger.getLogs()[0].stream, 'stderr') + assert.match(ace.ui.logger.getLogs()[0].message, /Cannot find package "typescript/) + }) + + test('fail when tsconfig file is missing', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + await command.exec() + + assert.equal(command.exitCode, 1) + }) + + test('build project inside build directory', async ({ assert, fs }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + compilerOptions: { skipLibCheck: true }, + exclude: [], + }) + ) + + await fs.create('adonisrc.ts', `export default {}`) + await fs.create('index.ts', '') + await fs.create( + 'package.json', + JSON.stringify({ + name: 'app', + dependencies: { + typescript: ts.version, + }, + }) + ) + + await execa('npm', ['install'], { + cwd: fs.basePath, + stdio: 'inherit', + }) + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + await command.exec() + + assert.equal(command.exitCode, 0) + + await assert.fileExists('build/index.js') + await assert.fileExists('build/adonisrc.js') + }) + + test('do not output when typescript build has errors', async ({ assert, fs }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + compilerOptions: { + target: 'ESNext', + module: 'NodeNext', + lib: ['ESNext'], + strict: true, + noUnusedLocals: true, + }, + }) + ) + + await fs.create('adonisrc.ts', `export default {}`) + await fs.create('index.ts', 'const foo = `a`') + await fs.create( + 'package.json', + JSON.stringify({ + name: 'app', + dependencies: { + typescript: ts.version, + }, + }) + ) + + await execa('npm', ['install'], { + cwd: fs.basePath, + stdio: 'inherit', + }) + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + await command.exec() + + assert.equal(command.exitCode, 1) + await assert.fileNotExists('build/index.js') + }) + + test('output with --ignore-ts-errors flags when typescript build has errors', async ({ + assert, + fs, + }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + compilerOptions: { + target: 'ESNext', + module: 'NodeNext', + lib: ['ESNext'], + strict: true, + noUnusedLocals: true, + }, + }) + ) + + await fs.create('adonisrc.ts', `export default {}`) + await fs.create('index.ts', 'const foo = `a`') + await fs.create( + 'package.json', + JSON.stringify({ + name: 'app', + dependencies: { + typescript: ts.version, + }, + }) + ) + + await execa('npm', ['install'], { + cwd: fs.basePath, + stdio: 'inherit', + }) + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + command.ignoreTsErrors = true + await command.exec() + + assert.equal(command.exitCode, 0) + await assert.fileExists('build/index.js') + await assert.fileExists('build/adonisrc.js') + }) + + test('show error when configured assets bundler is missing', async ({ assert, fs }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + compilerOptions: { + target: 'ESNext', + module: 'NodeNext', + lib: ['ESNext'], + strict: true, + noUnusedLocals: true, + }, + }) + ) + + await fs.create('adonisrc.ts', `export default {}`) + await fs.create('index.ts', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.assetsBundler = { + name: 'vite', + devServer: { command: 'vite' }, + build: { command: 'vite build' }, + } + + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + await command.exec() + + assert.equal(command.exitCode, 1) + assert.exists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/compiling frontend assets/) + }) + ) + }) + + test('do not attempt to build assets when assets bundler is not configured', async ({ + assert, + fs, + }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + compilerOptions: { + target: 'ESNext', + module: 'NodeNext', + lib: ['ESNext'], + strict: true, + noUnusedLocals: true, + }, + }) + ) + + await fs.create('adonisrc.ts', `export default {}`) + await fs.create('index.ts', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + + const command = await ace.create(Build, []) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.notExists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/compiling frontend assets/) + }) + ) + }) + + test('do not attempt to build assets when --no-assets flag is used', async ({ assert, fs }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + compilerOptions: { + target: 'ESNext', + module: 'NodeNext', + lib: ['ESNext'], + strict: true, + noUnusedLocals: true, + }, + }) + ) + + await fs.create('adonisrc.ts', `export default {}`) + await fs.create('index.ts', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.assetsBundler = { + name: 'vite', + devServer: { command: 'vite' }, + build: { command: 'vite build' }, + } + + ace.ui.switchMode('normal') + + const command = await ace.create(Build, ['--no-assets']) + await command.exec() + + assert.equal(command.exitCode, 0) + assert.notExists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/compiling frontend assets/) + }) + ) + }) + + test('correctly pass hooks to the bundler', async ({ assert, fs }) => { + assert.plan(2) + + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + compilerOptions: { + target: 'ESNext', + module: 'NodeNext', + lib: ['ESNext'], + strict: true, + noUnusedLocals: true, + }, + }) + ) + + await fs.create('adonisrc.ts', `export default {}`) + await fs.create('index.ts', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.hooks = { + onBuildCompleted: [ + async () => ({ + default: async () => { + assert.isTrue(true) + }, + }), + ], + onBuildStarting: [ + async () => ({ + default: async () => { + assert.isTrue(true) + }, + }), + ], + } + + ace.ui.switchMode('normal') + + const command = await ace.create(Build, []) + await command.exec() + }) +}) diff --git a/tests/commands/configure.spec.ts b/tests/commands/configure.spec.ts new file mode 100644 index 00000000..af3425cd --- /dev/null +++ b/tests/commands/configure.spec.ts @@ -0,0 +1,450 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' +import Configure from '../../commands/configure.js' +import { AceFactory } from '../../factories/core/ace.js' + +const BASE_URL = new URL('./tmp/', import.meta.url) +const BASE_PATH = fileURLToPath(BASE_URL) + +test.group('Configure command | list dependencies', (group) => { + group.each.disableTimeout() + + test('list development dependencies to install', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Configure, ['../dummy-pkg.js']) + command.stubsRoot = join(fs.basePath, 'stubs') + + const codemods = await command.createCodemods() + await codemods.listPackagesToInstall([ + { + name: '@japa/runner', + isDevDependency: true, + }, + { + name: '@japa/preset-adonis', + isDevDependency: true, + }, + { + name: 'playwright', + isDevDependency: true, + }, + ]) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: ['Please install following packages'].join('\n'), + stream: 'stdout', + }, + { + message: ['yellow(npm i -D) @japa/runner @japa/preset-adonis playwright'].join('\n'), + stream: 'stdout', + }, + { + message: [''].join('\n'), + stream: 'stdout', + }, + ]) + }) + + test('list development and prod dependencies to install', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Configure, ['../dummy-pkg.js']) + command.stubsRoot = join(fs.basePath, 'stubs') + + const codemods = await command.createCodemods() + await codemods.listPackagesToInstall([ + { + name: '@japa/runner', + isDevDependency: true, + }, + { + name: '@japa/preset-adonis', + isDevDependency: true, + }, + { + name: 'playwright', + isDevDependency: false, + }, + ]) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: ['Please install following packages'].join('\n'), + stream: 'stdout', + }, + { + message: ['yellow(npm i -D) @japa/runner @japa/preset-adonis'].join('\n'), + stream: 'stdout', + }, + { + message: ['yellow(npm i) playwright'].join('\n'), + stream: 'stdout', + }, + ]) + }) + + test('list prod dependencies to install', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Configure, ['../dummy-pkg.js']) + command.stubsRoot = join(fs.basePath, 'stubs') + + const codemods = await command.createCodemods() + await codemods.listPackagesToInstall([ + { + name: 'playwright', + isDevDependency: false, + }, + ]) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: ['Please install following packages'].join('\n'), + stream: 'stdout', + }, + { + message: [].join('\n'), + stream: 'stdout', + }, + { + message: ['yellow(npm i) playwright'].join('\n'), + stream: 'stdout', + }, + ]) + }) +}) + +test.group('Configure command | run', (group) => { + group.each.setup(({ context }) => { + context.fs.baseUrl = BASE_URL + context.fs.basePath = BASE_PATH + }) + + group.each.disableTimeout() + + test('error when unable to import package', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: async (filePath) => { + await import(filePath) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Configure, ['./dummy-pkg.js']) + await command.exec() + + command.assertLog('[ red(error) ] Cannot find module "./dummy-pkg.js". Make sure to install it') + assert.equal(command.exitCode, 1) + }) + + test('error when package cannot be configured', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(new URL(filePath, fs.baseUrl).href) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.create('dummy-pkg.js', `export const stubsRoot = './'`) + + const command = await ace.create(Configure, ['./dummy-pkg.js?v=1']) + await command.exec() + + command.assertLog( + '[ red(error) ] Cannot configure module "./dummy-pkg.js?v=1". The module does not export the configure hook' + ) + assert.equal(command.exitCode, 1) + }) + + test('run package configure method', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(new URL(filePath, fs.baseUrl).href) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.create( + 'dummy-pkg.js', + ` + export const stubsRoot = './' + export function configure (command) { + command.result = 'configured' + } + ` + ) + + const command = await ace.create(Configure, ['./dummy-pkg.js?v=2']) + await command.exec() + assert.equal(command.result, 'configured') + }) + + test('install packages', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(new URL(filePath, fs.baseUrl).href) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('package.json', { type: 'module' }) + await fs.createJson('tsconfig.json', {}) + await fs.create( + 'dummy-pkg.js', + ` + export const stubsRoot = './' + export async function configure (command) { + const codemods = await command.createCodemods() + await codemods.installPackages([ + { name: 'is-odd@2.0.0', isDevDependency: true }, + { name: 'is-even@1.0.0', isDevDependency: false } + ]) + } + ` + ) + + const command = await ace.create(Configure, ['./dummy-pkg.js?v=3']) + command.verbose = true + await command.exec() + + assert.equal(command.exitCode, 0) + const packageJson = await fs.contentsJson('package.json') + assert.deepEqual(packageJson.dependencies, { 'is-even': '^1.0.0' }) + assert.deepEqual(packageJson.devDependencies, { 'is-odd': '^2.0.0' }) + }) + + test('install packages using pnpm when pnpm-lock file exists', async ({ fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(new URL(filePath, fs.baseUrl).href) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.create('pnpm-lock.yaml', '') + await fs.createJson('tsconfig.json', {}) + await fs.createJson('package.json', { type: 'module' }) + await fs.create( + 'dummy-pkg.js', + ` + export const stubsRoot = './' + export async function configure (command) { + const codemods = await command.createCodemods() + await codemods.installPackages([ + { name: 'is-odd@2.0.0', isDevDependency: true, }, + ]) + } + ` + ) + + const command = await ace.create(Configure, ['./dummy-pkg.js?v=4']) + await command.exec() + + command.assertSucceeded() + command.assertLog('[ cyan(wait) ] installing dependencies using pnpm . ') + }) + + test('install packages using npm when package-lock file exists', async ({ fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(new URL(filePath, fs.baseUrl).href) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('package-lock.json', {}) + await fs.createJson('tsconfig.json', {}) + await fs.createJson('package.json', { type: 'module' }) + await fs.create( + 'dummy-pkg.js', + ` + export const stubsRoot = './' + export async function configure (command) { + const codemods = await command.createCodemods() + await codemods.installPackages([ + { name: 'is-odd@2.0.0', isDevDependency: true, }, + ]) + } + ` + ) + + const command = await ace.create(Configure, ['./dummy-pkg.js?v=5']) + await command.exec() + + command.assertSucceeded() + command.assertLog('[ cyan(wait) ] installing dependencies using npm . ') + }) + + test('display error when installation fails', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(new URL(filePath, fs.baseUrl).href) + }, + }) + + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('package-lock.json', {}) + await fs.createJson('tsconfig.json', {}) + await fs.createJson('package.json', { type: 'module' }) + await fs.create( + 'dummy-pkg.js', + ` + export const stubsRoot = './' + export async function configure (command) { + const codemods = await command.createCodemods() + await codemods.installPackages([ + { name: 'is-odd@15.0.0', isDevDependency: true, }, + ]) + } + ` + ) + + const command = await ace.create(Configure, ['./dummy-pkg.js?v=6']) + await command.exec() + + command.assertFailed() + + const logs = ace.ui.logger.getLogs() + assert.deepInclude(logs, { + message: '[ cyan(wait) ] unable to install dependencies ...', + stream: 'stdout', + }) + + const lastLog = logs[logs.length - 1] + assert.equal(command.exitCode, 1) + console.log(lastLog.message) + assert.include(lastLog.message, '[ red(error) ] Process exited with non-zero status') + }) +}) + +test.group('Configure command | vinejs', (group) => { + group.each.disableTimeout() + + test('register vinejs provider', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', 'export default defineConfig({})') + + const command = await ace.create(Configure, ['vinejs']) + command.stubsRoot = join(fs.basePath, 'stubs') + + await command.run() + + assert.deepEqual(command.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('adonisrc.ts', '@adonisjs/core/providers/vinejs_provider') + }) +}) + +test.group('Configure command | edge', (group) => { + group.each.disableTimeout() + + test('register edge provider', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', 'export default defineConfig({})') + + const command = await ace.create(Configure, ['edge']) + command.stubsRoot = join(fs.basePath, 'stubs') + + await command.run() + + assert.deepEqual(command.ui.logger.getLogs(), [ + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('adonisrc.ts', '@adonisjs/core/providers/edge_provider') + await assert.fileContains( + 'adonisrc.ts', + `metaFiles: [{ + pattern: 'resources/views/**/*.edge', + reloadServer: false, + }]` + ) + }) +}) + +test.group('Configure command | health checks', (group) => { + group.each.disableTimeout() + + test('create start/health file with some default checks', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', 'export default defineConfig({})') + + const command = await ace.create(Configure, ['health_checks']) + + await command.run() + + assert.deepEqual(command.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create start/health.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) create app/controllers/health_checks_controller.ts', + stream: 'stdout', + }, + ]) + + await assert.fileContains('start/health.ts', [ + 'new DiskSpaceCheck()', + 'new MemoryHeapCheck()', + 'export const healthChecks = ', + ]) + await assert.fileContains('app/controllers/health_checks_controller.ts', [ + `import { healthChecks } from '#start/health'`, + 'const report = await healthChecks.run()', + 'export default class HealthChecksController', + ]) + }) +}) diff --git a/tests/commands/eject.spec.ts b/tests/commands/eject.spec.ts new file mode 100644 index 00000000..10ea0ba4 --- /dev/null +++ b/tests/commands/eject.spec.ts @@ -0,0 +1,73 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import EjectCommand from '../../commands/eject.js' +import { AceFactory } from '../../factories/core/ace.js' + +test.group('Eject', () => { + test('eject a single stub', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(filePath), + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(EjectCommand, [ + 'make/controller/main.stub', + '--pkg="../../index.js"', + ]) + await command.exec() + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: '[ green(success) ] eject stubs/make/controller/main.stub', + stream: 'stdout', + }, + ]) + + await assert.hasFiles(['stubs/make/controller/main.stub']) + }) + + test('eject a directory', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(filePath), + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(EjectCommand, ['make/controller', '--pkg="../../index.js"']) + await command.exec() + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: '[ green(success) ] eject stubs/make/controller/actions.stub', + stream: 'stdout', + }, + { + message: '[ green(success) ] eject stubs/make/controller/api.stub', + stream: 'stdout', + }, + { + message: '[ green(success) ] eject stubs/make/controller/main.stub', + stream: 'stdout', + }, + { + message: '[ green(success) ] eject stubs/make/controller/resource.stub', + stream: 'stdout', + }, + ]) + + await assert.hasFiles([ + 'stubs/make/controller/main.stub', + 'stubs/make/controller/api.stub', + 'stubs/make/controller/resource.stub', + ]) + }) +}) diff --git a/tests/commands/env_add.spec.ts b/tests/commands/env_add.spec.ts new file mode 100644 index 00000000..d6a64635 --- /dev/null +++ b/tests/commands/env_add.spec.ts @@ -0,0 +1,116 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import EnvAdd from '../../commands/env/add.js' +import { AceFactory } from '../../factories/core/ace.js' + +test.group('Env Add command', () => { + test('add new env variable to the different files', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('.env', '') + await fs.create('.env.example', '') + await fs.create( + './start/env.ts', + `import { Env } from '@adonisjs/core/env' + export default await Env.create(new URL('../', import.meta.url), {})` + ) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(EnvAdd, ['variable', 'value', '--type=string']) + await command.exec() + + await assert.fileContains('.env', 'VARIABLE=value') + await assert.fileContains('.env.example', 'VARIABLE=') + await assert.fileContains('./start/env.ts', 'VARIABLE: Env.schema.string()') + }) + + test('convert variable to screaming snake case', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('.env', '') + await fs.create('.env.example', '') + await fs.create( + './start/env.ts', + `import { Env } from '@adonisjs/core/env' + export default await Env.create(new URL('../', import.meta.url), {})` + ) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(EnvAdd, ['stripe_ApiKey', 'value', '--type=string']) + await command.exec() + + await assert.fileContains('.env', 'STRIPE_API_KEY=value') + await assert.fileContains('.env.example', 'STRIPE_API_KEY=') + await assert.fileContains('./start/env.ts', 'STRIPE_API_KEY: Env.schema.string()') + }) + + test('enum type with allowed values', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('.env', '') + await fs.create('.env.example', '') + await fs.create( + './start/env.ts', + `import { Env } from '@adonisjs/core/env' + export default await Env.create(new URL('../', import.meta.url), {})` + ) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(EnvAdd, [ + 'variable', + 'bar', + '--type=enum', + '--enum-values=foo', + '--enum-values=bar', + ]) + await command.exec() + + await assert.fileContains('.env', 'VARIABLE=bar') + await assert.fileContains('.env.example', 'VARIABLE=') + await assert.fileContains( + './start/env.ts', + "VARIABLE: Env.schema.enum(['foo', 'bar'] as const)" + ) + }) + + test('prompt when nothing is passed to the command', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('.env', '') + await fs.create('.env.example', '') + await fs.create( + './start/env.ts', + `import { Env } from '@adonisjs/core/env' + export default await Env.create(new URL('../', import.meta.url), {})` + ) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(EnvAdd, []) + + command.prompt.trap('Enter the variable name').replyWith('my_variable_name') + command.prompt.trap('Enter the variable value').replyWith('my_value') + command.prompt.trap('Select the variable type').replyWith('string') + + await command.exec() + + await assert.fileContains('.env', 'MY_VARIABLE_NAME=my_value') + await assert.fileContains('.env.example', 'MY_VARIABLE_NAME=') + await assert.fileContains('./start/env.ts', 'MY_VARIABLE_NAME: Env.schema.string()') + }) +}) diff --git a/tests/commands/generate_key.spec.ts b/tests/commands/generate_key.spec.ts new file mode 100644 index 00000000..d0af0010 --- /dev/null +++ b/tests/commands/generate_key.spec.ts @@ -0,0 +1,100 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import GenerateKey from '../../commands/generate_key.js' +import { AceFactory } from '../../factories/core/ace.js' + +test.group('Generate key', () => { + test('create key and write it to .env file', async ({ assert, fs }) => { + await fs.create('.env', '') + await fs.create('.env.example', '') + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(GenerateKey, []) + await command.exec() + + await assert.fileContains('.env', 'APP_KEY=') + await assert.fileContains('.env.example', 'APP_KEY=') + }) + + test('do not write to the file when --show flag is set', async ({ assert, fs }) => { + await fs.create('.env', '') + await fs.create('.env.example', '') + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(GenerateKey, ['--show']) + await command.exec() + + await assert.fileEquals('.env', '') + await assert.fileEquals('.env.example', '') + + assert.deepEqual(ace.ui.logger.getLogs()[0].stream, 'stdout') + assert.match(ace.ui.logger.getLogs()[0].message, /APP_KEY =/) + }) + + test('do not write to the file when in production envionment', async ({ + assert, + fs, + cleanup, + }) => { + await fs.create('.env', '') + await fs.create('.env.example', '') + + cleanup(() => { + delete process.env.NODE_ENV + }) + + process.env.NODE_ENV = 'production' + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(GenerateKey, []) + await command.exec() + + await assert.fileEquals('.env', '') + await assert.fileEquals('.env.example', '') + + assert.deepEqual(ace.ui.logger.getLogs()[0].stream, 'stdout') + assert.match(ace.ui.logger.getLogs()[0].message, /APP_KEY =/) + }) + + test('write to the file when in production envionment and --force flag is set', async ({ + assert, + fs, + cleanup, + }) => { + await fs.create('.env', '') + await fs.create('.env.example', '') + + cleanup(() => { + delete process.env.NODE_ENV + }) + + process.env.NODE_ENV = 'production' + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(GenerateKey, ['--force']) + await command.exec() + + await assert.fileContains('.env', 'APP_KEY=') + await assert.fileContains('.env.example', 'APP_KEY=') + }) +}) diff --git a/tests/commands/inspect_rcfile.spec.ts b/tests/commands/inspect_rcfile.spec.ts new file mode 100644 index 00000000..d3259dbc --- /dev/null +++ b/tests/commands/inspect_rcfile.spec.ts @@ -0,0 +1,54 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import InspectRCFile from '../../commands/inspect_rcfile.js' + +test.group('Inspect RCFile', () => { + test('inspect rcfile contents', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const inspect = await ace.create(InspectRCFile, []) + await inspect.exec() + + inspect.assertSucceeded() + + const { raw, providers, preloads, commands, ...rcContents } = ace.app.rcFile + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: JSON.stringify( + { + ...rcContents, + providers: providers.map((provider) => { + return { + ...provider, + file: provider.file.toString(), + } + }), + preloads: preloads.map((preload) => { + return { + ...preload, + file: preload.file.toString(), + } + }), + commands: commands.map((command) => { + return command.toString() + }), + }, + null, + 2 + ), + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_command.spec.ts b/tests/commands/make_command.spec.ts new file mode 100644 index 00000000..3f10b273 --- /dev/null +++ b/tests/commands/make_command.spec.ts @@ -0,0 +1,37 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import MakeCommand from '../../commands/make/command.js' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' + +test.group('Make command', () => { + test('create command class', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeCommand, ['listRoutes']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/command/main.stub', { + entity: ace.app.generators.createEntity('listRoutes'), + }) + + await assert.fileEquals('commands/list_routes.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create commands/list_routes.ts', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_controller.spec.ts b/tests/commands/make_controller.spec.ts new file mode 100644 index 00000000..9fde61f3 --- /dev/null +++ b/tests/commands/make_controller.spec.ts @@ -0,0 +1,337 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' +import MakeControllerCommand from '../../commands/make/controller.js' + +test.group('Make controller', () => { + test('create controller', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, ['user']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/main.stub', { + entity: ace.app.generators.createEntity('user'), + singular: false, + }) + + await assert.fileEquals('app/controllers/users_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/users_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('skip when controller already exists', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.create('app/controllers/users_controller.ts', `export default class {}`) + + const command = await ace.create(MakeControllerCommand, ['user']) + await command.exec() + + await assert.fileEquals('app/controllers/users_controller.ts', `export default class {}`) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: + 'cyan(SKIPPED:) create app/controllers/users_controller.ts dim((File already exists))', + stream: 'stdout', + }, + ]) + }) + + test('create resource controller', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, ['user', '--resource']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/resource.stub', { + entity: ace.app.generators.createEntity('user'), + singular: false, + }) + + await assert.fileEquals('app/controllers/users_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/users_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('create api controller', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, ['user', '--api']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/api.stub', { + entity: ace.app.generators.createEntity('user'), + singular: false, + }) + + await assert.fileEquals('app/controllers/users_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/users_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('create api controller when both api and resource flags are used', async ({ + assert, + fs, + }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, ['user', '--api', '--resource']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/api.stub', { + entity: ace.app.generators.createEntity('user'), + singular: false, + }) + + await assert.fileEquals('app/controllers/users_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: + '[ yellow(warn) ] --api and --resource flags cannot be used together. Ignoring --resource', + stream: 'stdout', + }, + { + message: 'green(DONE:) create app/controllers/users_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('create controller with actions', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, [ + 'user', + 'index', + 'show', + 'delete-profile', + ]) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/actions.stub', { + entity: ace.app.generators.createEntity('user'), + singular: false, + actions: ['index', 'show', 'deleteProfile'], + }) + + await assert.fileEquals('app/controllers/users_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/users_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('warn when using --resource flag with actions', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, [ + 'user', + 'index', + 'show', + 'delete-profile', + '--resource', + ]) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/actions.stub', { + entity: ace.app.generators.createEntity('user'), + singular: false, + actions: ['index', 'show', 'deleteProfile'], + }) + + await assert.fileEquals('app/controllers/users_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: '[ yellow(warn) ] Cannot use --resource flag with actions. Ignoring --resource', + stream: 'stdout', + }, + { + message: 'green(DONE:) create app/controllers/users_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('warn when using --resource and --api flag with actions', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, [ + 'user', + 'index', + 'show', + 'delete-profile', + '--resource', + '--api', + ]) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/actions.stub', { + entity: ace.app.generators.createEntity('user'), + singular: false, + actions: ['index', 'show', 'deleteProfile'], + }) + + await assert.fileEquals('app/controllers/users_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: '[ yellow(warn) ] Cannot use --resource flag with actions. Ignoring --resource', + stream: 'stdout', + }, + { + message: '[ yellow(warn) ] Cannot use --api flag with actions. Ignoring --api', + stream: 'stdout', + }, + { + message: 'green(DONE:) create app/controllers/users_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('create singular controller', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, ['user', '-s']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/main.stub', { + entity: ace.app.generators.createEntity('user'), + singular: true, + }) + + await assert.fileEquals('app/controllers/user_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/user_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('create singular resource controller', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, ['user', '--resource', '-s']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/resource.stub', { + entity: ace.app.generators.createEntity('user'), + singular: true, + }) + + await assert.fileEquals('app/controllers/user_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/user_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('create singular controller with actions', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, [ + 'user', + 'index', + 'show', + 'delete-profile', + '-s', + ]) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/actions.stub', { + entity: ace.app.generators.createEntity('user'), + actions: ['index', 'show', 'deleteProfile'], + singular: true, + }) + + await assert.fileEquals('app/controllers/user_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/user_controller.ts', + stream: 'stdout', + }, + ]) + }) + + test('create singular api controller', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeControllerCommand, ['user', '--api', '-s']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/controller/api.stub', { + entity: ace.app.generators.createEntity('user'), + singular: true, + }) + + await assert.fileEquals('app/controllers/user_controller.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/controllers/user_controller.ts', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_event.spec.ts b/tests/commands/make_event.spec.ts new file mode 100644 index 00000000..360486cd --- /dev/null +++ b/tests/commands/make_event.spec.ts @@ -0,0 +1,37 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' +import MakeEventCommand from '../../commands/make/event.js' + +test.group('Make event', () => { + test('create event class', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeEventCommand, ['orderShipped']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/event/main.stub', { + entity: ace.app.generators.createEntity('orderShipped'), + }) + + await assert.fileEquals('app/events/order_shipped.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/events/order_shipped.ts', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_exception.spec.ts b/tests/commands/make_exception.spec.ts new file mode 100644 index 00000000..79887e82 --- /dev/null +++ b/tests/commands/make_exception.spec.ts @@ -0,0 +1,37 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' +import MakeException from '../../commands/make/exception.js' + +test.group('Make exception command', () => { + test('create exception class', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeException, ['Unauthorized']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/exception/main.stub', { + entity: ace.app.generators.createEntity('Unauthorized'), + }) + + await assert.fileEquals('app/exceptions/unauthorized_exception.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/exceptions/unauthorized_exception.ts', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_listener.spec.ts b/tests/commands/make_listener.spec.ts new file mode 100644 index 00000000..25140e27 --- /dev/null +++ b/tests/commands/make_listener.spec.ts @@ -0,0 +1,73 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { ListLoader } from '../../modules/ace/main.js' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' +import MakeEventCommand from '../../commands/make/event.js' +import MakeListenerCommand from '../../commands/make/listener.js' + +test.group('Make listener', () => { + test('create listener class', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeListenerCommand, ['sendEmail']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/listener/main.stub', { + entity: ace.app.generators.createEntity('sendEmail'), + }) + + await assert.fileEquals('app/listeners/send_email.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/listeners/send_email.ts', + stream: 'stdout', + }, + ]) + }) + + test('create a listener with an event class', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + + ace.addLoader(new ListLoader([MakeEventCommand])) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeListenerCommand, ['sendEmail', '-e=orderShipped']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/listener/for_event.stub', { + entity: ace.app.generators.createEntity('sendEmail'), + event: ace.app.generators.createEntity('orderShipped'), + }) + + const { contents: eventContents } = await new StubsFactory().prepare('make/event/main.stub', { + entity: ace.app.generators.createEntity('orderShipped'), + }) + + await assert.fileEquals('app/listeners/send_email.ts', contents) + await assert.fileEquals('app/events/order_shipped.ts', eventContents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/events/order_shipped.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) create app/listeners/send_email.ts', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_middleware.spec.ts b/tests/commands/make_middleware.spec.ts new file mode 100644 index 00000000..f41d2b8d --- /dev/null +++ b/tests/commands/make_middleware.spec.ts @@ -0,0 +1,141 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' +import MakeMiddleware from '../../commands/make/middleware.js' + +test.group('Make middleware', (group) => { + group.tap((t) => t.disableTimeout()) + + test('create middleware class', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('start/kernel.ts', 'server.use([])') + + const command = await ace.create(MakeMiddleware, ['auth', '--stack=server']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/middleware/main.stub', { + entity: ace.app.generators.createEntity('auth'), + }) + + await assert.fileEquals('app/middleware/auth_middleware.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/middleware/auth_middleware.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update start/kernel.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains( + 'start/kernel.ts', + `server.use([() => import('#middleware/auth_middleware')])` + ) + }) + + test('register middleware under named stack', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('start/kernel.ts', 'export const middleware = router.named({})') + + const command = await ace.create(MakeMiddleware, ['auth', '--stack=named']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/middleware/main.stub', { + entity: ace.app.generators.createEntity('auth'), + }) + + await assert.fileEquals('app/middleware/auth_middleware.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/middleware/auth_middleware.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update start/kernel.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains( + 'start/kernel.ts', + `auth: () => import('#middleware/auth_middleware')` + ) + }) + + test('create nested middleware', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('start/kernel.ts', 'export const middleware = router.named({})') + + const command = await ace.create(MakeMiddleware, ['blog/auth', '--stack=named']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/middleware/main.stub', { + entity: ace.app.generators.createEntity('auth'), + }) + + await assert.fileEquals('app/middleware/blog/auth_middleware.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/middleware/blog/auth_middleware.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update start/kernel.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains( + 'start/kernel.ts', + `auth: () => import('#middleware/blog/auth_middleware')` + ) + }) + + test('show error when selected middleware stack is invalid', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + await fs.createJson('tsconfig.json', {}) + await fs.create('start/kernel.ts', 'export const middleware = router.named({})') + + const command = await ace.create(MakeMiddleware, ['auth', '--stack=foo']) + await command.exec() + await assert.fileNotExists('app/middleware/auth_middleware.ts') + + command.assertFailed() + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: + '[ red(error) ] Invalid middleware stack "foo". Select from "server, router, named"', + stream: 'stderr', + }, + ]) + }) +}) diff --git a/tests/commands/make_preload.spec.ts b/tests/commands/make_preload.spec.ts new file mode 100644 index 00000000..fadd876c --- /dev/null +++ b/tests/commands/make_preload.spec.ts @@ -0,0 +1,142 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { StubsFactory } from '../../factories/stubs.js' +import { AceFactory } from '../../factories/core/ace.js' +import MakePreload from '../../commands/make/preload.js' + +test.group('Make preload file', () => { + test('create a preload file for all environments', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakePreload, ['app']) + command.prompt.trap('Do you want to register the preload file in .adonisrc.ts file?').accept() + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/preload/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + await assert.fileEquals('start/app.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create start/app.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('adonisrc.ts', `() => import('#start/app')`) + }) + + test('do not prompt when --register flag is used', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakePreload, ['app', '--register']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/preload/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + await assert.fileEquals('start/app.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create start/app.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('adonisrc.ts', `() => import('#start/app')`) + }) + + test('do not register preload file when --no-register flag is used', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakePreload, ['app', '--no-register']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/preload/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + await assert.fileEquals('start/app.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create start/app.ts', + stream: 'stdout', + }, + ]) + + await assert.fileEquals('adonisrc.ts', `export default defineConfig({})`) + }) + + test('use environment flag to make preload file in a specific env', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakePreload, [ + 'app', + '--environments=web', + '--environments=repl', + ]) + command.prompt.trap('Do you want to register the preload file in .adonisrc.ts file?').accept() + await command.exec() + + await assert.fileContains('adonisrc.ts', [ + `() => import('#start/app')`, + `environment: ['web', 'repl']`, + ]) + }) + + test('display error when defined environment is not allowed', async ({ fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakePreload, ['app']) + command.environments = ['foo' as any] + command.prompt.trap('Do you want to register the preload file in .adonisrc.ts file?').accept() + await command.exec() + + command.assertLog( + '[ red(error) ] Invalid environment(s) "foo". Only "web,console,test,repl" are allowed' + ) + }) +}) diff --git a/tests/commands/make_provider.spec.ts b/tests/commands/make_provider.spec.ts new file mode 100644 index 00000000..d22ac82d --- /dev/null +++ b/tests/commands/make_provider.spec.ts @@ -0,0 +1,160 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import MakeProvider from '../../commands/make/provider.js' +import { StubsFactory } from '../../factories/stubs.js' + +test.group('Make provider', () => { + test('create provider class', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeProvider, ['app']) + command.prompt.trap('Do you want to register the provider in .adonisrc.ts file?').accept() + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/provider/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + + await assert.fileEquals('providers/app_provider.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create providers/app_provider.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('adonisrc.ts', `() => import('#providers/app_provider')`) + }) + + test('do not display prompt when --register flag is used', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeProvider, ['app', '--register']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/provider/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + + await assert.fileEquals('providers/app_provider.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create providers/app_provider.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileContains('adonisrc.ts', `() => import('#providers/app_provider')`) + }) + + test('do not register provider when --no-register flag is used', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeProvider, ['app', '--no-register']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/provider/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + + await assert.fileEquals('providers/app_provider.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create providers/app_provider.ts', + stream: 'stdout', + }, + ]) + + await assert.fileEquals('adonisrc.ts', `export default defineConfig({})`) + }) + + test('create provider class for a specific environment', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeProvider, ['app', '-e=web', '-e=repl']) + command.prompt.trap('Do you want to register the provider in .adonisrc.ts file?').accept() + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/provider/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create providers/app_provider.ts', + stream: 'stdout', + }, + { + message: 'green(DONE:) update adonisrc.ts file', + stream: 'stdout', + }, + ]) + + await assert.fileEquals('providers/app_provider.ts', contents) + await assert.fileContains('adonisrc.ts', [ + `() => import('#providers/app_provider')`, + `environment: ['web', 'repl']`, + ]) + }) + + test('show error when selected environment is invalid', async ({ assert, fs }) => { + await fs.createJson('tsconfig.json', {}) + await fs.create('adonisrc.ts', `export default defineConfig({})`) + + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeProvider, ['app', '--environments=foo']) + command.prompt.trap('Do you want to register the provider in .adonisrc.ts file?').accept() + await command.exec() + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: + '[ red(error) ] Invalid environment(s) "foo". Only "web,console,test,repl" are allowed', + stream: 'stderr', + }, + ]) + }) +}) diff --git a/tests/commands/make_service.spec.ts b/tests/commands/make_service.spec.ts new file mode 100644 index 00000000..4ff225ab --- /dev/null +++ b/tests/commands/make_service.spec.ts @@ -0,0 +1,37 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' +import MakeService from '../../commands/make/service.js' + +test.group('Make service', () => { + test('create service class', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeService, ['app']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/service/main.stub', { + entity: ace.app.generators.createEntity('app'), + }) + + await assert.fileEquals('app/services/app_service.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/services/app_service.ts', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_test.spec.ts b/tests/commands/make_test.spec.ts new file mode 100644 index 00000000..b14ec631 --- /dev/null +++ b/tests/commands/make_test.spec.ts @@ -0,0 +1,226 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import MakeTest from '../../commands/make/test.js' +import { StubsFactory } from '../../factories/stubs.js' +import { AceFactory } from '../../factories/core/ace.js' +import { IgnitorFactory } from '../../factories/core/ignitor.js' + +test.group('Make test', () => { + test('--suite flag: make inside suite directory', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + tests: { + suites: [ + { + name: 'functional', + files: ['tests/functional/**/*.spec.ts'], + }, + ], + }, + }, + }) + .create(fs.baseUrl) + + const ace = await new AceFactory().make(ignitor) + ace.ui.switchMode('raw') + + const command = await ace.create(MakeTest, ['posts/create', '--suite', 'functional']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/test/main.stub', { + entity: ace.app.generators.createEntity('posts/create'), + suite: { + directory: 'tests/functional', + }, + }) + + await assert.fileEquals('tests/functional/posts/create.spec.ts', contents) + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create tests/functional/posts/create.spec.ts', + stream: 'stdout', + }, + ]) + }) + + test('--suite flag: show error when mentioned suite does not exists', async ({ assert, fs }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + }, + }) + .withCoreConfig() + .create(fs.baseUrl) + + const ace = await new AceFactory().make(ignitor) + ace.ui.switchMode('raw') + + const command = await ace.create(MakeTest, ['posts/create', '--suite', 'functional']) + await command.exec() + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: + '[ red(error) ] The "functional" suite is not configured inside the "adonisrc.js" file', + stream: 'stderr', + }, + ]) + }) + + test('auto pick first suite when only one suite is configured in rcfile', async ({ + assert, + fs, + }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + tests: { + suites: [ + { + name: 'functional', + files: ['tests/functional/**/*.spec.ts'], + }, + ], + }, + }, + }) + .create(fs.baseUrl) + + const ace = await new AceFactory().make(ignitor) + ace.ui.switchMode('raw') + + const command = await ace.create(MakeTest, ['posts/create']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/test/main.stub', { + entity: ace.app.generators.createEntity('posts/create'), + suite: { + directory: 'tests/functional', + }, + }) + + await assert.fileEquals('tests/functional/posts/create.spec.ts', contents) + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create tests/functional/posts/create.spec.ts', + stream: 'stdout', + }, + ]) + }) + + test('prompt for suite selection when multiple suites are configured in rc file', async ({ + assert, + fs, + }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + tests: { + suites: [ + { + name: 'functional', + files: ['tests/functional/**/*.spec.ts'], + }, + { + name: 'unit', + files: ['tests/unit/**/*.spec.ts'], + }, + ], + }, + }, + }) + .create(fs.baseUrl) + + const ace = await new AceFactory().make(ignitor) + ace.ui.switchMode('raw') + + const command = await ace.create(MakeTest, ['posts/create']) + command.prompt + .trap('Select the suite for the test file') + .assertFails('', 'Please select a suite') + .assertPasses('functional') + .chooseOption(1) + + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/test/main.stub', { + entity: ace.app.generators.createEntity('posts/create'), + suite: { + directory: 'tests/unit', + }, + }) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create tests/unit/posts/create.spec.ts', + stream: 'stdout', + }, + ]) + await assert.fileEquals('tests/unit/posts/create.spec.ts', contents) + }) + + test('prompt for directory selection when suite has multiple directories', async ({ + assert, + fs, + }) => { + const ignitor = new IgnitorFactory() + .withCoreConfig() + .merge({ + rcFileContents: { + providers: [() => import('../../providers/app_provider.js')], + tests: { + suites: [ + { + name: 'functional', + files: ['tests/functional/**/*.spec.ts', 'features/tests/functional/*.spec.ts'], + }, + ], + }, + }, + }) + .create(fs.baseUrl) + + const ace = await new AceFactory().make(ignitor) + ace.ui.switchMode('raw') + + const command = await ace.create(MakeTest, ['posts/create']) + command.prompt + .trap('Select directory for the test file') + .assertPasses('features/tests/functional') + .assertFails('', 'Please select a directory') + .chooseOption(1) + + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/test/main.stub', { + entity: ace.app.generators.createEntity('posts/create'), + suite: { + directory: 'features/tests/functional', + }, + }) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create features/tests/functional/posts/create.spec.ts', + stream: 'stdout', + }, + ]) + await assert.fileEquals('features/tests/functional/posts/create.spec.ts', contents) + }) +}) diff --git a/tests/commands/make_validator.spec.ts b/tests/commands/make_validator.spec.ts new file mode 100644 index 00000000..a5937e01 --- /dev/null +++ b/tests/commands/make_validator.spec.ts @@ -0,0 +1,59 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { AceFactory } from '../../factories/core/ace.js' +import { StubsFactory } from '../../factories/stubs.js' +import MakeValidator from '../../commands/make/validator.js' + +test.group('Make validator', () => { + test('create validator file', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeValidator, ['invoice']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/validator/main.stub', { + entity: ace.app.generators.createEntity('invoice'), + }) + + await assert.fileEquals('app/validators/invoice.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/validators/invoice.ts', + stream: 'stdout', + }, + ]) + }) + + test('create validator file for a resource', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeValidator, ['invoice', '--resource']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/validator/resource.stub', { + entity: ace.app.generators.createEntity('invoice'), + }) + + await assert.fileEquals('app/validators/invoice.ts', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create app/validators/invoice.ts', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/make_view.spec.ts b/tests/commands/make_view.spec.ts new file mode 100644 index 00000000..a2abe5ae --- /dev/null +++ b/tests/commands/make_view.spec.ts @@ -0,0 +1,38 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' + +import MakeView from '../../commands/make/view.js' +import { StubsFactory } from '../../factories/stubs.js' +import { AceFactory } from '../../factories/core/ace.js' + +test.group('Make view', () => { + test('create view template', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(MakeView, ['welcome']) + await command.exec() + + const { contents } = await new StubsFactory().prepare('make/view/main.stub', { + entity: ace.app.generators.createEntity('welcome'), + }) + + await assert.fileEquals('resources/views/welcome.edge', contents) + + assert.deepEqual(ace.ui.logger.getLogs(), [ + { + message: 'green(DONE:) create resources/views/welcome.edge', + stream: 'stdout', + }, + ]) + }) +}) diff --git a/tests/commands/serve.spec.ts b/tests/commands/serve.spec.ts new file mode 100644 index 00000000..51a9d350 --- /dev/null +++ b/tests/commands/serve.spec.ts @@ -0,0 +1,358 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import Serve from '../../commands/serve.js' +import { AceFactory } from '../../factories/core/ace.js' + +const sleep = (duration: number) => new Promise((resolve) => setTimeout(resolve, duration)) + +test.group('Serve command', () => { + test('show error when assembler is not installed', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === '@adonisjs/assembler') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + await command.exec() + await sleep(600) + + assert.equal(command.exitCode, 1) + assert.lengthOf(ace.ui.logger.getLogs(), 1) + assert.equal(ace.ui.logger.getLogs()[0].stream, 'stderr') + assert.match(ace.ui.logger.getLogs()[0].message, /Cannot find package "@adonisjs\/assembler/) + }) + + test('fail when bin/server.js file is missing', async ({ assert, fs, cleanup }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === 'typescript') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + cleanup(() => command.devServer.close()) + await command.exec() + + await sleep(600) + + assert.equal(command.exitCode, 1) + }) + + test('show error in watch mode when typescript is not installed', async ({ + assert, + fs, + cleanup, + }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === 'typescript') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + cleanup(() => command.devServer.close()) + command.watch = true + await command.exec() + + await sleep(600) + + assert.equal(command.exitCode, 1) + assert.lengthOf(ace.ui.logger.getLogs(), 1) + assert.equal(ace.ui.logger.getLogs()[0].stream, 'stderr') + assert.match(ace.ui.logger.getLogs()[0].message, /Cannot find package "typescript/) + }) + + test('fail in watch mode when tsconfig file is missing', async ({ assert, fs, cleanup }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + cleanup(() => command.devServer.close()) + command.watch = true + await command.exec() + + await sleep(600) + + assert.equal(command.exitCode, 1) + }) + + test('do not fail in watch mode when ts-node is missing', async ({ assert, fs, cleanup }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + }) + ) + + await fs.create('index.ts', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + cleanup(() => command.devServer.close()) + command.watch = true + await command.exec() + + await sleep(600) + + /** + * In watch mode, we wait for errors to be fixed and then + * re-start the process + */ + assert.equal(command.exitCode, 0) + }) + + test('show error when configured assets bundler is missing', async ({ assert, fs, cleanup }) => { + await fs.create('bin/server.js', '') + await fs.create( + 'node_modules/ts-node/package.json', + JSON.stringify({ + name: 'ts-node', + exports: { + './esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.assetsBundler = { + name: 'vite', + devServer: { command: 'vite' }, + build: { command: 'vite build' }, + } + + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + cleanup(() => command.devServer.close()) + await command.exec() + await sleep(600) + + assert.exists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/starting "vite" dev server/) + }) + ) + assert.exists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/unable to connect to "vite" dev server/) + }) + ) + }) + + test('do not attempt to serve assets when assets bundler is not configured', async ({ + assert, + fs, + cleanup, + }) => { + await fs.create('bin/server.js', '') + await fs.create( + 'node_modules/ts-node/package.json', + JSON.stringify({ + name: 'ts-node', + exports: { + './esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + cleanup(() => command.devServer.close()) + await command.exec() + await sleep(600) + + assert.notExists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/starting "vite" dev server/) + }) + ) + }) + + test('do not attempt to serve assets when --no-assets flag is used', async ({ + assert, + fs, + cleanup, + }) => { + await fs.create('bin/server.js', '') + await fs.create( + 'node_modules/ts-node/package.json', + JSON.stringify({ + name: 'ts-node', + exports: { + './esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.assetsBundler = { + name: 'vite', + devServer: { command: 'vite' }, + build: { command: 'vite build' }, + } + + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-assets', '--no-clear']) + cleanup(() => command.devServer.close()) + await command.exec() + await sleep(600) + + assert.notExists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/starting "vite" dev server/) + }) + ) + }) + + test('do not launch assets bundler when disabled in rc file', async ({ fs, cleanup, assert }) => { + await fs.create('bin/server.js', '') + await fs.create( + 'node_modules/ts-node/package.json', + JSON.stringify({ + name: 'ts-node', + exports: { + './esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node/esm.js', '') + await fs.create('vite.config.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.assetsBundler = false + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-clear']) + cleanup(() => command.devServer.close()) + await command.exec() + await sleep(600) + + assert.notExists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/starting "vite" dev server/) + }) + ) + }) + + test('correctly pass hooks to the DevServer', async ({ assert, fs, cleanup }) => { + assert.plan(1) + + await fs.create( + 'bin/server.js', + ` + process.send({ isAdonisJS: true, environment: 'web' }); + ` + ) + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node', + exports: { './register/esm': './esm.js' }, + }) + ) + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(filePath), + }) + + ace.app.rcFile.hooks = { + onDevServerStarted: [ + async () => ({ + default: async () => assert.isTrue(true), + }), + ], + } + + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--no-assets', '--no-clear']) + cleanup(() => command.devServer.close()) + await command.exec() + await sleep(1200) + }) + + test('error if --hmr and --watch are used together', async ({ assert, fs }) => { + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => import(filePath), + }) + + ace.ui.switchMode('raw') + + const command = await ace.create(Serve, ['--hmr', '--watch', '--no-clear']) + await command.exec() + + assert.equal(command.exitCode, 1) + assert.lengthOf(ace.ui.logger.getLogs(), 1) + assert.equal(ace.ui.logger.getLogs()[0].stream, 'stderr') + assert.match(ace.ui.logger.getLogs()[0].message, /Cannot use --watch and --hmr flags together/) + }) +}) diff --git a/tests/commands/test.spec.ts b/tests/commands/test.spec.ts new file mode 100644 index 00000000..fb18b9a2 --- /dev/null +++ b/tests/commands/test.spec.ts @@ -0,0 +1,554 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import Test from '../../commands/test.js' +import { AceFactory } from '../../factories/core/ace.js' + +const sleep = (duration: number) => new Promise((resolve) => setTimeout(resolve, duration)) + +test.group('Test command', () => { + test('show error when assembler is not installed', async ({ assert, fs }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === '@adonisjs/assembler') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-clear']) + await command.exec() + await sleep(600) + + assert.equal(command.exitCode, 1) + assert.lengthOf(ace.ui.logger.getLogs(), 1) + assert.equal(ace.ui.logger.getLogs()[0].stream, 'stderr') + assert.match(ace.ui.logger.getLogs()[0].message, /Cannot find package "@adonisjs\/assembler/) + }) + + test('fail when bin/test.js file is missing', async ({ assert, fs, cleanup }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === 'typescript') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-clear']) + cleanup(() => command.testsRunner.close()) + await command.exec() + + await sleep(600) + assert.equal(command.exitCode, 1) + }) + + test('fail in watch mode when typescript is not installed', async ({ assert, fs, cleanup }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + if (filePath === 'typescript') { + return import(new URL(filePath, fs.baseUrl).href) + } + + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-clear']) + cleanup(() => command.testsRunner.close()) + command.watch = true + await command.exec() + + await sleep(600) + + assert.equal(command.exitCode, 1) + assert.lengthOf(ace.ui.logger.getLogs(), 1) + assert.equal(ace.ui.logger.getLogs()[0].stream, 'stderr') + assert.match(ace.ui.logger.getLogs()[0].message, /Cannot find package "typescript/) + }) + + test('show error in watch mode when tsconfig file is missing', async ({ + assert, + fs, + cleanup, + }) => { + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-clear']) + cleanup(() => command.testsRunner.close()) + command.watch = true + await command.exec() + + await sleep(600) + + assert.equal(command.exitCode, 1) + }) + + test('do not fail in watch mode when ts-node is missing', async ({ assert, fs, cleanup }) => { + await fs.create( + 'tsconfig.json', + JSON.stringify({ + include: ['**/*'], + exclude: [], + }) + ) + + await fs.create('index.ts', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + await ace.app.init() + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-clear']) + cleanup(() => command.testsRunner.close()) + command.watch = true + await command.exec() + + await sleep(600) + assert.equal(command.exitCode, 0) + }) + + test('show error when configured assets bundler is missing', async ({ assert, fs, cleanup }) => { + await fs.create('bin/server.js', '') + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.assetsBundler = { + name: 'vite', + devServer: { command: 'vite' }, + build: { command: 'vite build' }, + } + + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-clear']) + cleanup(() => command.testsRunner.close()) + await command.exec() + await sleep(600) + + assert.exists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/starting "vite" dev server/) + }) + ) + assert.exists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/unable to connect to "vite" dev server/) + }) + ) + }) + + test('do not attempt to serve assets when assets bundler is not configured', async ({ + assert, + fs, + cleanup, + }) => { + await fs.create('bin/test.js', '') + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-clear']) + cleanup(() => command.testsRunner.close()) + await command.exec() + await sleep(600) + + assert.notExists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/starting "vite" dev server/) + }) + ) + }) + + test('do not attempt to serve assets when --no-assets flag is used', async ({ + assert, + fs, + cleanup, + }) => { + await fs.create('bin/test.js', '') + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.app.rcFile.assetsBundler = { + name: 'vite', + build: { command: 'vite build' }, + devServer: { command: 'vite' }, + } + + ace.ui.switchMode('raw') + + const command = await ace.create(Test, ['--no-assets', '--no-clear']) + cleanup(() => command.testsRunner.close()) + await command.exec() + await sleep(600) + + assert.notExists( + ace.ui.logger.getLogs().find((log) => { + return log.message.match(/starting "vite" dev server/) + }) + ) + }) + + test('pass filters to bin/test.js script', async ({ assert, fs, cleanup }) => { + await fs.create( + 'package.json', + JSON.stringify({ + type: 'module', + }) + ) + + await fs.create( + 'bin/test.js', + ` + import { writeFile } from 'node:fs/promises' + await writeFile('argv.json', JSON.stringify(process.argv.splice(2), null, 2)) + ` + ) + + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + ace.app.rcFile.tests.suites = [ + { + name: 'unit', + files: ['tests/unit/**/*.spec(.js|.ts)'], + directories: ['tests/unit'], + }, + ] + + const command = await ace.create(Test, [ + '--no-clear', + '--files=math.spec', + '--groups=foo', + '--tags=bar', + '--tests="2 + 2 = 4"', + ]) + cleanup(() => command.testsRunner.close()) + await command.exec() + await sleep(600) + + await assert.fileEquals( + 'argv.json', + JSON.stringify( + ['--files', 'math.spec', '--groups', 'foo', '--tags', 'bar', '--tests', '2 + 2 = 4'], + null, + 2 + ) + ) + }) + + test('pass suites to bin/test.js script', async ({ assert, fs, cleanup }) => { + await fs.create( + 'package.json', + JSON.stringify({ + type: 'module', + }) + ) + + await fs.create( + 'bin/test.js', + ` + import { writeFile } from 'node:fs/promises' + await writeFile('argv.json', JSON.stringify(process.argv.splice(2), null, 2)) + ` + ) + + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + ace.app.rcFile.tests.suites = [ + { + name: 'unit', + files: 'tests/unit/**/*.spec(.js|.ts)', + directories: ['tests/unit'], + }, + ] + + const command = await ace.create(Test, ['unit', 'functional', '--no-clear']) + cleanup(() => command.testsRunner.close()) + await command.exec() + await sleep(600) + + await assert.fileEquals('argv.json', JSON.stringify(['unit', 'functional'], null, 2)) + }) + + test('pass unknown flags to bin/test.js script', async ({ assert, fs, cleanup }) => { + await fs.create( + 'package.json', + JSON.stringify({ + type: 'module', + }) + ) + + await fs.create( + 'bin/test.js', + ` + import { writeFile } from 'node:fs/promises' + await writeFile('argv.json', JSON.stringify(process.argv.splice(2), null, 2)) + ` + ) + + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + ace.app.rcFile.tests.suites = [ + { + name: 'unit', + files: 'tests/unit/**/*.spec(.js|.ts)', + directories: ['tests/unit'], + }, + ] + + const command = await ace.create(Test, ['--browser=firefox', '--inspect', '--no-clear']) + cleanup(() => command.testsRunner.close()) + await command.exec() + await sleep(600) + + await assert.fileEquals( + 'argv.json', + JSON.stringify(['--browser', 'firefox', '--inspect'], null, 2) + ) + }) + + test('pass unknown flags with array values to bin/test.js script', async ({ + assert, + fs, + cleanup, + }) => { + await fs.create( + 'package.json', + JSON.stringify({ + type: 'module', + }) + ) + + await fs.create( + 'bin/test.js', + ` + import { writeFile } from 'node:fs/promises' + await writeFile('argv.json', JSON.stringify(process.argv.splice(2), null, 2)) + ` + ) + + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + ace.app.rcFile.tests.suites = [ + { + name: 'unit', + files: ['tests/unit/**/*.spec(.js|.ts)'], + directories: ['tests/unit'], + }, + ] + + const command = await ace.create(Test, [ + '--browser=firefox', + '--browser=chrome', + '--inspect', + '--no-clear', + ]) + await command.exec() + cleanup(() => command.testsRunner.close()) + await sleep(600) + + await assert.fileEquals( + 'argv.json', + JSON.stringify(['--browser', 'firefox', '--browser', 'chrome', '--inspect'], null, 2) + ) + }) + + test('pass all japa flags to the script', async ({ assert, fs, cleanup }) => { + await fs.create( + 'package.json', + JSON.stringify({ + type: 'module', + }) + ) + + await fs.create( + 'bin/test.js', + ` + import { writeFile } from 'node:fs/promises' + await writeFile('argv.json', JSON.stringify(process.argv.splice(2), null, 2)) + ` + ) + + await fs.create( + 'node_modules/ts-node-maintained/package.json', + JSON.stringify({ + name: 'ts-node-maintained', + exports: { + './register/esm': './esm.js', + }, + }) + ) + + await fs.create('node_modules/ts-node-maintained/esm.js', '') + + const ace = await new AceFactory().make(fs.baseUrl, { + importer: (filePath) => { + return import(filePath) + }, + }) + + ace.ui.switchMode('raw') + ace.app.rcFile.tests.suites = [ + { + name: 'unit', + files: ['tests/unit/**/*.spec(.js|.ts)'], + directories: ['tests/unit'], + }, + ] + + const command = await ace.create(Test, [ + '--no-clear', + '--reporters=ndjson,spec', + '--failed', + '--retries=2', + '--timeout=3000', + ]) + cleanup(() => command.testsRunner.close()) + await command.exec() + await sleep(600) + + await assert.fileEquals( + 'argv.json', + JSON.stringify( + ['--reporters', 'ndjson,spec', '--timeout', '3000', '--failed', '--retries', '2'], + null, + 2 + ) + ) + }) +}) diff --git a/tests/dumper/dumper.spec.ts b/tests/dumper/dumper.spec.ts new file mode 100644 index 00000000..5a1eb2ba --- /dev/null +++ b/tests/dumper/dumper.spec.ts @@ -0,0 +1,92 @@ +/* + * @adonisjs/core + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' +import { AppFactory } from '@adonisjs/application/factories' +import { HttpContextFactory } from '@adonisjs/http-server/factories' + +import { Dumper } from '../../modules/dumper/dumper.js' +import { AceFactory } from '../../factories/core/ace.js' +import { E_DUMP_DIE_EXCEPTION } from '../../modules/dumper/errors.js' + +test.group('Dumper', () => { + test('dump and die', ({ fs }) => { + const app = new AppFactory().create(fs.baseUrl) + const dumper = new Dumper(app) + + dumper.dd('hello') + }).throws('Dump and Die exception', E_DUMP_DIE_EXCEPTION) + + test('render dump as HTML', async ({ fs, assert }) => { + assert.plan(3) + const app = new AppFactory().create(fs.baseUrl) + const dumper = new Dumper(app) + + const ctx = new HttpContextFactory().create() + + try { + dumper.dd('hello') + } catch (error) { + await error.handle(error, ctx) + assert.include(ctx.response.getBody(), '