├── .editorconfig ├── .github ├── COMMIT_CONVENTION.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── banner.png ├── dependabot.yml └── workflows │ ├── commitlint.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .nycrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── adonis-typings ├── container.ts ├── index.ts └── scheduler.ts ├── bin ├── japa_types.ts └── test.ts ├── commands ├── index.ts └── scheduler_work.ts ├── instructions.md ├── package.json ├── pnpm-lock.yaml ├── providers └── scheduler_provider.ts ├── src ├── manages_frequencies.ts ├── schedule.ts └── scheduler.ts ├── templates └── tasks.txt ├── tests ├── manages_frequencies.spec.ts ├── schedule.spec.ts ├── scheduler_provider.spec.ts └── sheduler.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.json] 10 | insert_final_newline = ignore 11 | 12 | [**.min.js] 13 | indent_style = ignore 14 | insert_final_newline = ignore 15 | 16 | [MakeFile] 17 | indent_style = space 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. 6 | 7 | ``` js 8 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ 9 | ``` 10 | 11 | ## Commit Message Format 12 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 13 | 14 | > The **scope** is optional 15 | 16 | ``` 17 | feat(router): add support for prefix 18 | 19 | Prefix makes it easier to append a path to a group of routes 20 | ``` 21 | 22 | 1. `feat` is type. 23 | 2. `router` is scope and is optional 24 | 3. `add support for prefix` is the subject 25 | 4. The **body** is followed by a blank line. 26 | 5. The optional **footer** can be added after the body, followed by a blank line. 27 | 28 | ## Types 29 | Only one type can be used at a time and only following types are allowed. 30 | 31 | - feat 32 | - fix 33 | - docs 34 | - style 35 | - refactor 36 | - perf 37 | - test 38 | - workflow 39 | - ci 40 | - chore 41 | - types 42 | - build 43 | 44 | If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. 45 | 46 | ### Revert 47 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. 48 | 49 | ## Scope 50 | The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. 51 | 52 | ## Subject 53 | The subject contains succinct description of the change: 54 | 55 | - use the imperative, present tense: "change" not "changed" nor "changes". 56 | - don't capitalize first letter 57 | - no dot (.) at the end 58 | 59 | ## Body 60 | 61 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". 62 | The body should include the motivation for the change and contrast this with previous behavior. 63 | 64 | ## Footer 65 | 66 | The footer should contain any information about **Breaking Changes** and is also the place to 67 | reference GitHub issues that this commit **Closes**. 68 | 69 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. 70 | 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests. And following this guidelines will make your pull request easier to merge 4 | 5 | ## Prerequisites 6 | 7 | - Install [EditorConfig](http://editorconfig.org/) plugin for your code editor to make sure it uses correct settings. 8 | - Fork the repository and clone your fork. 9 | - Install dependencies: `pnpm install`. 10 | 11 | ## Coding style 12 | 13 | We make use of [eslint](https://eslint.org/) to lint our code. 14 | 15 | ## Development work-flow 16 | 17 | Always make sure to lint and test your code before pushing it to the GitHub. 18 | 19 | ```bash 20 | pnpm test 21 | ``` 22 | 23 | Just lint the code 24 | 25 | ```bash 26 | pnpm lint 27 | ``` 28 | 29 | **Make sure you add sufficient tests for the change**. 30 | 31 | ## Other notes 32 | 33 | - Do not change version number inside the `package.json` file. 34 | - Do not update `CHANGELOG.md` file. 35 | 36 | ## Need help? 37 | 38 | Feel free to ask. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Prerequisites 4 | 5 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 6 | 7 | - Ensure the issue isn't already reported. 8 | - Ensure you are reporting the bug in the correct repo. 9 | 10 | *Delete the above section and the instructions in the sections below before submitting* 11 | 12 | ## Description 13 | 14 | If this is a feature request, explain why it should be added. Specific use-cases are best. 15 | 16 | For bug reports, please provide as much *relevant* info as possible. 17 | 18 | ## Package version 19 | 20 | 21 | ## Error Message & Stack Trace 22 | 23 | ## Relevant Information 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 6 | 7 | ## Types of changes 8 | 9 | What types of changes does your code introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/verful/notifications/blob/master/.github/CONTRIBUTING.md) doc 22 | - [ ] Lint and unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works. 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further comments 27 | 28 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 29 | -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verful/adonis-scheduler/734704e40f50319df5ceac24b055004bf5cc9672/.github/banner.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | groups: 13 | minor-and-patch: 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | with: 10 | fetch-depth: 0 11 | - uses: wagoid/commitlint-github-action@v5 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: ['main'] 5 | jobs: 6 | lint-and-test: 7 | name: Lint and test 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: 12 | - 16.15.1 13 | - 18.x 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: pnpm/action-setup@v2 17 | with: 18 | version: 8 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'pnpm' 24 | - name: Install 25 | run: pnpm install 26 | - name: lint 27 | run: pnpm lint 28 | - name: Run tests 29 | run: pnpm test 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: ['main'] 5 | jobs: 6 | release: 7 | name: Release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Use Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 20 16 | 17 | - name: Install pnpm 18 | uses: pnpm/action-setup@v2 19 | with: 20 | version: 8 21 | run_install: false 22 | 23 | - name: Get pnpm store directory 24 | shell: bash 25 | run: | 26 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 27 | 28 | - uses: actions/cache@v3 29 | name: Setup pnpm cache 30 | with: 31 | path: ${{ env.STORE_PATH }} 32 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pnpm-store- 35 | 36 | - name: Install dependencies 37 | run: pnpm install 38 | 39 | - name: Build 40 | run: pnpm build 41 | 42 | - name: Semantic Release 43 | run: pnpm semantic-release 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | shrinkwrap.yaml 13 | test/__app 14 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "all": true, 4 | "check-coverage": true, 5 | "include": ["src/**/*.ts", "providers/**/*.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Arthur Rezende 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |
7 |

Adonis Scheduler

8 |

Schedule tasks in AdonisJS with ease

9 |
10 | 11 |
12 | 13 | [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url] 14 | 15 |
16 | 17 | 18 | ## **Pre-requisites** 19 | The `@verful/scheduler` package requires `@adonisjs/core >= 5.9.0` 20 | 21 | ## **Setup** 22 | 23 | Install the package from the npm registry as follows. 24 | 25 | ``` 26 | npm i @verful/scheduler 27 | # or 28 | yarn add @verful/scheduler 29 | ``` 30 | 31 | Next, configure the package by running the following ace command. 32 | 33 | ``` 34 | node ace configure @verful/scheduler 35 | ``` 36 | 37 | ## **Defining Scheduled Tasks** 38 | You may define all of your scheduled tasks in the `start/tasks.ts` preloaded file. To get started, let's take a look at an example. In this example, we will schedule a closure to be called every day at midnight. Within the closure we will execute a database query to clear a table: 39 | 40 | ```typescript 41 | import Scheduler from '@ioc:Verful/Scheduler' 42 | import Database from '@ioc:Adonis/Lucid/Database' 43 | 44 | Scheduler.call(async () => { 45 | Database.from('recent_users').delete() 46 | }).daily() 47 | 48 | ``` 49 | 50 | ### Scheduling Ace Commands 51 | 52 | In addition to scheduling closures, you may also schedule Ace commands and system commands. For example, you may use the command method to schedule an Ace command using the commands name. 53 | 54 | ```typescript 55 | import Scheduler from '@ioc:Verful/Scheduler' 56 | 57 | Scheduler.command('queue:flush').everyFiveMinutes() 58 | ``` 59 | 60 | ### Scheduling Shell Commands 61 | 62 | The `exec` method may be used to issue a command to the operating system: 63 | 64 | ```typescript 65 | import Scheduler from '@ioc:Verful/Scheduler' 66 | 67 | Scheduler.exec('node script.js').daily() 68 | ``` 69 | 70 | ### Schedule Frequency Options 71 | 72 | We've already seen a few examples of how you may configure a task to run at specified intervals. However, there are many more task schedule frequencies that you may assign to a task: 73 | 74 | | Method | Description | 75 | | ------------------------------- | ------------------------------------------------------- | 76 | | `.cron('* * * * *')` | Run the task on a custom cron schedule | 77 | | `.everyMinute()` | Run the task every minute | 78 | | `.everyTwoMinutes()` | Run the task every two minutes | 79 | | `.everyThreeMinutes()` | Run the task every three minutes | 80 | | `.everyFourMinutes()` | Run the task every four minutes | 81 | | `.everyFiveMinutes()` | Run the task every five minutes | 82 | | `.everyTenMinutes()` | Run the task every ten minutes | 83 | | `.everyFifteenMinutes()` | Run the task every fifteen minutes | 84 | | `.everyThirtyMinutes()` | Run the task every thirty minutes | 85 | | `.hourly()` | Run the task every hour | 86 | | `.hourlyAt(17)` | Run the task every hour at 17 minutes past the hour | 87 | | `.everyOddHour(minutes)` | Run the task every odd hour | 88 | | `.everyTwoHours(minutes)` | Run the task every two hours | 89 | | `.everyThreeHours(minutes)` | Run the task every three hours | 90 | | `.everyFourHours(minutes)` | Run the task every four hours | 91 | | `.everySixHours(minutes)` | Run the task every six hours | 92 | | `.daily()` | Run the task every day at midnight | 93 | | `.dailyAt('13:00')` | Run the task every day at 13:00 | 94 | | `.twiceDaily(1, 13)` | Run the task daily at 1:00 & 13:00 | 95 | | `.twiceDailyAt(1, 13, 15)` | Run the task daily at 1:15 & 13:15 | 96 | | `.weekly()` | Run the task every Sunday at 00:00 | 97 | | `.weeklyOn(1, '8:00')` | Run the task every week on Monday at 8:00 | 98 | | `.monthly()` | Run the task on the first day of every month at 00:00 | 99 | | `.monthlyOn(4, '15:00')` | Run the task every month on the 4th at 15:00 | 100 | | `.twiceMonthly(1, 16, '13:00')` | Run the task monthly on the 1st and 16th at 13:00 | 101 | | `.lastDayOfMonth('15:00')` | Run the task on the last day of the month at 15:00 | 102 | | `.quarterly()` | Run the task on the first day of every quarter at 00:00 | 103 | | `.quarterlyOn(4, '14:00')` | Run the task every quarter on the 4th at 14:00 | 104 | | `.yearly()` | Run the task on the first day of every year at 00:00 | 105 | | `.yearlyOn(6, 1, '17:00')` | Run the task every year on June 1st at 17:00 | 106 | 107 | These methods may be combined with additional constraints to create even more finely tuned schedules that only run on certain days of the week. For example, you may schedule a command to run weekly on Monday: 108 | 109 | ```typescript 110 | import Scheduler from '@ioc:Verful/Scheduler' 111 | 112 | // Run once per week on Monday at 1 PM... 113 | Scheduler.call(() => { 114 | // ... 115 | }).weekly().mondays().at('13:00') 116 | 117 | // Run hourly from 8 AM to 5 PM on weekdays... 118 | Scheduler.command('foo') 119 | .weekdays() 120 | .hourly() 121 | .between('8:00', '17:00') 122 | ``` 123 | 124 | A list of additional schedule constraints may be found below: 125 | 126 | | Method | Description | 127 | | ----------------------------- | ----------------------------------------------------- | 128 | | `.weekdays()` | Limit the task to weekdays | 129 | | `.weekends()` | Limit the task to weekends | 130 | | `.sundays()` | Limit the task to Sunday | 131 | | `.mondays()` | Limit the task to Monday | 132 | | `.tuesdays()` | Limit the task to Tuesday | 133 | | `.wednesdays()` | Limit the task to Wednesday | 134 | | `.thursdays()` | Limit the task to Thursday | 135 | | `.fridays()` | Limit the task to Friday | 136 | | `.saturdays()` | Limit the task to Saturday | 137 | | `.days(days)` | Limit the task to specific days | 138 | | `.between(start, end)` | Limit the task to run between start and end times | 139 | | `.unlessBetween(start, end)` | Limit the task to not run between start and end times | 140 | | `.when(Closure)` | Limit the task based on a truth test | 141 | | `.environments(environments)` | Limit the task to specific environments | 142 | 143 | 144 | #### Day Constraints 145 | 146 | The `days` method may be used to limit the execution of a task to specific days of the week. For example, you may schedule a command to run hourly on Sundays and Wednesdays: 147 | 148 | ```typescript 149 | import Scheduler from '@ioc:Verful/Scheduler' 150 | 151 | Scheduler.command('emails:send') 152 | .hourly() 153 | .days([0, 3]) 154 | ``` 155 | 156 | #### Between Time Constraints 157 | 158 | The `between` method may be used to limit the execution of a task based on the time of day: 159 | 160 | ```typescript 161 | import Scheduler from '@ioc:Verful/Scheduler' 162 | 163 | Scheduler.command('emails:send') 164 | .hourly() 165 | .between('7:00', '22:00') 166 | ``` 167 | 168 | Similarly, the `unlessBetween` method can be used to exclude the execution of a task for a period of time: 169 | 170 | ```typescript 171 | import Scheduler from '@ioc:Verful/Scheduler' 172 | 173 | Scheduler.command('emails:send') 174 | .hourly() 175 | .unlessBetween('23:00', '4:00') 176 | ``` 177 | 178 | #### Truth Test Constraints 179 | 180 | The `when` method may be used to limit the execution of a task based on the result of a given truth test. In other words, if the given closure returns `true`, the task will execute as long as no other constraining conditions prevent the task from running: 181 | 182 | ```typescript 183 | import Scheduler from '@ioc:Verful/Scheduler' 184 | 185 | Scheduler.command('emails:send') 186 | .daily() 187 | .when(() => true); 188 | ``` 189 | 190 | The `skip` method may be seen as the inverse of `when`. If the `skip` method returns `true`, the scheduled task will not be executed: 191 | 192 | ```typescript 193 | import Scheduler from '@ioc:Verful/Scheduler' 194 | 195 | Scheduler.command('emails:send') 196 | .daily() 197 | .skip(() => true); 198 | ``` 199 | 200 | When using chained when methods, the scheduled command will only execute if all when conditions return true. 201 | 202 | #### Environment Constraints 203 | 204 | The environments method may be used to execute tasks only on the given environments (as defined by the NODE_ENV environment variable): 205 | 206 | ```typescript 207 | import Scheduler from '@ioc:Verful/Scheduler' 208 | 209 | Scheduler.command('emails:send') 210 | .daily() 211 | .environments(['staging', 'production']); 212 | ``` 213 | 214 | ## Running the Scheduler 215 | 216 | Run the `scheduler:work` ace command, it doesn't need to be put into a cron job, as the scheduler will process the jobs as the time passes 217 | 218 | [npm-image]: https://img.shields.io/npm/v/@verful/scheduler.svg?style=for-the-badge&logo=**npm** 219 | [npm-url]: https://npmjs.org/package/@verful/scheduler "npm" 220 | 221 | [license-image]: https://img.shields.io/npm/l/@verful/scheduler?color=blueviolet&style=for-the-badge 222 | [license-url]: LICENSE.md "license" 223 | 224 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 225 | [typescript-url]: "typescript" 226 | -------------------------------------------------------------------------------- /adonis-typings/container.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Core/Application' { 2 | import { SchedulerContract } from '@ioc:Verful/Scheduler' 3 | 4 | export interface ContainerBindings { 5 | 'Verful/Scheduler': SchedulerContract 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /adonis-typings/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /adonis-typings/scheduler.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Verful/Scheduler' { 2 | import { DateTime } from 'luxon' 3 | 4 | export type ScheduleHandler = () => void 5 | export type Time = `${string}:${string}` 6 | export type Condition = () => boolean | Promise 7 | 8 | export interface ManagesFrequenciesContract { 9 | expression: string 10 | cron(expression: string): this 11 | everyMinute(): this 12 | everyTwoMinutes(): this 13 | everyThreeMinutes(): this 14 | everyFourMinutes(): this 15 | everyFiveMinutes(): this 16 | everyTenMinutes(): this 17 | everyFifteenMinutes(): this 18 | everyThirtyMinutes(): this 19 | hourly(): this 20 | hourlyAt(offset: number | number[]): this 21 | everyTwoHours(): this 22 | everyFourHours(): this 23 | everySixHours(): this 24 | daily(): this 25 | dailyAt(time: Time): this 26 | twiceDaily(): this 27 | twiceDailyAt(first: number, second: number, offset: number): this 28 | weekly(): this 29 | weeklyOn(daysOfWeek: number | number[] | string, time: Time): this 30 | monthly(): this 31 | monthlyOn(dayOfMonth: number, time: Time): this 32 | twiceMonthly(first: number, second: number, time: Time): this 33 | lastDayOfMonth(time: Time): this 34 | yearly(): this 35 | yearlyOn(month: number, dayOfMonth?: string | number, time?: Time): this 36 | days(days: number | number[] | string): this 37 | weekdays(): this 38 | weekends(): this 39 | sundays(): this 40 | mondays(): this 41 | tuesdays(): this 42 | wednesdays(): this 43 | thursdays(): this 44 | fridays(): this 45 | saturdays(): this 46 | } 47 | 48 | export interface ScheduleContract extends ManagesFrequenciesContract { 49 | command: ScheduleHandler 50 | filters: Condition[] 51 | rejects: Condition[] 52 | skip(condition: Condition): this 53 | when(condition: Condition): this 54 | between(start: Time, end: Time): this 55 | unlessBetween(start: Time, end: Time): this 56 | environments(environments: Array<'production' | 'development' | 'staging' | 'test'>): this 57 | } 58 | 59 | export interface SchedulerContract { 60 | call(handler: ScheduleHandler): ScheduleContract 61 | command(command: string | string[]): ScheduleContract 62 | exec(command: string): ScheduleContract 63 | start(): void 64 | } 65 | 66 | const Scheduler: SchedulerContract 67 | 68 | export default Scheduler 69 | } 70 | -------------------------------------------------------------------------------- /bin/japa_types.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | import '@japa/runner' 3 | 4 | declare module '@japa/runner' { 5 | interface TestContext { 6 | app: ApplicationContract 7 | } 8 | 9 | interface Test { 10 | // notify TypeScript about custom test properties 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | 3 | import { resolve } from 'node:path' 4 | 5 | import { Application } from '@adonisjs/core/build/standalone' 6 | import { assert } from '@japa/assert' 7 | import { processCliArgs, configure, run, TestContext } from '@japa/runner' 8 | import { specReporter } from '@japa/spec-reporter' 9 | import { Filesystem } from '@poppinss/dev-utils' 10 | 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Configure tests 14 | |-------------------------------------------------------------------------- 15 | | 16 | | The configure method accepts the configuration to configure the Japa 17 | | tests runner. 18 | | 19 | | The first method call "processCliArgs" process the command line arguments 20 | | and turns them into a config object. Using this method is not mandatory. 21 | | 22 | | Please consult japa.dev/runner-config for the config docs. 23 | */ 24 | const fs = new Filesystem(resolve(__dirname, '__app')) 25 | 26 | configure({ 27 | ...processCliArgs(process.argv.slice(2)), 28 | files: ['tests/**/*.spec.ts'], 29 | plugins: [assert()], 30 | reporters: [specReporter()], 31 | importer: (filePath) => import(filePath), 32 | forceExit: true, 33 | setup: [ 34 | async () => { 35 | await fs.add('.env', '') 36 | await fs.add( 37 | 'config/app.ts', 38 | ` 39 | export const profiler = { enabled: true } 40 | export const appKey = 'averylong32charsrandomsecretkey', 41 | export const http = { 42 | cookie: {}, 43 | trustProxy: () => true, 44 | } 45 | ` 46 | ) 47 | const app = new Application(fs.basePath, 'test', { 48 | providers: ['@adonisjs/core', '../../providers/scheduler_provider'], 49 | }) 50 | 51 | await app.setup() 52 | await app.registerProviders() 53 | await app.bootProviders() 54 | 55 | return async () => { 56 | await app.shutdown() 57 | await fs.cleanup() 58 | } 59 | }, 60 | ], 61 | }) 62 | 63 | TestContext.getter('app', () => require('@adonisjs/core/build/services/app.js').default) 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Run tests 68 | |-------------------------------------------------------------------------- 69 | | 70 | | The following "run" method is required to execute all the tests. 71 | | 72 | */ 73 | run() 74 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Export your commands using the full path to the build code 3 | * ex: '@org/package/build/commands/Command' 4 | */ 5 | 6 | export default ['@verful/scheduler/build/commands/scheduler_work'] 7 | -------------------------------------------------------------------------------- /commands/scheduler_work.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '@adonisjs/core/build/standalone' 2 | 3 | export default class ProcessSchedule extends BaseCommand { 4 | public static commandName = 'scheduler:work' 5 | 6 | public static description = 'Process the scheduled tasks' 7 | 8 | public static settings = { 9 | loadApp: true, 10 | stayAlive: true, 11 | } 12 | 13 | public async run() { 14 | this.application.container.use('Verful/Scheduler').start() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | The package has been configured successfully. 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@verful/scheduler", 3 | "description": "Easily manage scheduled tasks in AdonisJS", 4 | "version": "1.6.0", 5 | "keywords": [ 6 | "adonisjs", 7 | "scheduler", 8 | "cron", 9 | "cronjob" 10 | ], 11 | "license": "MIT", 12 | "author": { 13 | "name": "Arthur Rezende", 14 | "email": "arthur-er@protonmail.com" 15 | }, 16 | "homepage": "https://github.com/verful/adonis-scheduler#readme", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/verful/adonis-scheduler.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/verful/adonis-scheduler/issues" 23 | }, 24 | "engines": { 25 | "node": ">=18" 26 | }, 27 | "publishConfig": { 28 | "access": "public", 29 | "tag": "latest" 30 | }, 31 | "main": "./build/providers/scheduler_provider.js", 32 | "types": "build/adonis-typings/index.d.ts", 33 | "files": [ 34 | "build/adonis-typings", 35 | "build/commands", 36 | "build/providers", 37 | "build/src", 38 | "build/templates", 39 | "build/instructions.md" 40 | ], 41 | "scripts": { 42 | "pretest": "pnpm lint", 43 | "test": "node --require=@adonisjs/require-ts/build/register bin/test.ts", 44 | "coverage": "nyc pnpm test", 45 | "clean": "del-cli build", 46 | "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", 47 | "compile": "pnpm lint && pnpm clean && tsc", 48 | "build": "pnpm compile && pnpm copyfiles", 49 | "release": "np --message=\"chore(release): %s\"", 50 | "version": "pnpm build", 51 | "prepublishOnly": "pnpm build", 52 | "lint": "eslint . --ext=.ts", 53 | "format": "prettier --write ." 54 | }, 55 | "dependencies": { 56 | "luxon": "^3.4.2", 57 | "node-cron": "^3.0.2" 58 | }, 59 | "devDependencies": { 60 | "@adonisjs/application": "^5.3.0", 61 | "@adonisjs/core": "^5.9.0", 62 | "@adonisjs/require-ts": "^2.0.13", 63 | "@adonisjs/sink": "^5.4.3", 64 | "@commitlint/cli": "^18.4.3", 65 | "@commitlint/config-conventional": "^18.4.3", 66 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 67 | "@japa/assert": "^1.4.1", 68 | "@japa/run-failed-tests": "^1.1.1", 69 | "@japa/runner": "^2.5.1", 70 | "@japa/spec-reporter": "^1.3.3", 71 | "@poppinss/dev-utils": "^2.0.3", 72 | "@semantic-release/git": "^10.0.1", 73 | "@types/luxon": "^3.3.2", 74 | "@types/node": "^20.10.5", 75 | "@types/node-cron": "^3.0.11", 76 | "@types/sinon": "^17.0.2", 77 | "copyfiles": "^2.4.1", 78 | "del-cli": "^5.1.0", 79 | "eslint": "^8.56.0", 80 | "eslint-config-prettier": "^9.1.0", 81 | "eslint-plugin-adonis": "^2.1.1", 82 | "eslint-plugin-import-helpers": "^1.3.1", 83 | "eslint-plugin-prettier": "^5.1.2", 84 | "eslint-plugin-unicorn": "^50.0.1", 85 | "execa": "^5.1.1", 86 | "husky": "^8.0.3", 87 | "lint-staged": "^15.2.0", 88 | "np": "^9.2.0", 89 | "nyc": "^15.1.0", 90 | "prettier": "^3.1.1", 91 | "reflect-metadata": "^0.2.1", 92 | "semantic-release": "^22.0.12", 93 | "sinon": "^17.0.1", 94 | "typescript": "^5.3.3" 95 | }, 96 | "peerDependencies": { 97 | "@adonisjs/core": "^5.9.0" 98 | }, 99 | "adonisjs": { 100 | "instructionsMd": "./build/instructions.md", 101 | "preloads": [ 102 | "./start/tasks" 103 | ], 104 | "templates": { 105 | "start": [ 106 | { 107 | "src": "tasks.txt", 108 | "dest": "tasks" 109 | } 110 | ] 111 | }, 112 | "types": "@verful/scheduler", 113 | "providers": [ 114 | "@verful/scheduler" 115 | ], 116 | "commands": [ 117 | "@verful/scheduler/build/commands" 118 | ] 119 | }, 120 | "eslintConfig": { 121 | "extends": [ 122 | "plugin:adonis/typescriptApp", 123 | "plugin:unicorn/recommended", 124 | "prettier" 125 | ], 126 | "plugins": [ 127 | "prettier", 128 | "eslint-plugin-import-helpers" 129 | ], 130 | "rules": { 131 | "prettier/prettier": "error", 132 | "import-helpers/order-imports": [ 133 | "warn", 134 | { 135 | "newlinesBetween": "always", 136 | "groups": [ 137 | "/node:(.*)/", 138 | "module", 139 | "/@ioc:(.*)/", 140 | [ 141 | "parent", 142 | "sibling", 143 | "index" 144 | ] 145 | ], 146 | "alphabetize": { 147 | "order": "asc", 148 | "ignoreCase": true 149 | } 150 | } 151 | ], 152 | "unicorn/prefer-module": "off", 153 | "unicorn/prefer-top-level-await": "off", 154 | "unicorn/text-encoding-identifier-case": "off", 155 | "unicorn/no-null": "off", 156 | "unicorn/no-array-reduce": "off", 157 | "unicorn/prevent-abbreviations": [ 158 | "error", 159 | { 160 | "allowList": { 161 | "env": true 162 | } 163 | } 164 | ], 165 | "unicorn/filename-case": [ 166 | "error", 167 | { 168 | "cases": { 169 | "snakeCase": true 170 | } 171 | } 172 | ], 173 | "unicorn/no-array-callback-reference": "off" 174 | } 175 | }, 176 | "eslintIgnore": [ 177 | "build" 178 | ], 179 | "prettier": { 180 | "trailingComma": "es5", 181 | "semi": false, 182 | "singleQuote": true, 183 | "useTabs": false, 184 | "quoteProps": "consistent", 185 | "bracketSpacing": true, 186 | "arrowParens": "always", 187 | "printWidth": 100 188 | }, 189 | "commitlint": { 190 | "extends": [ 191 | "@commitlint/config-conventional" 192 | ], 193 | "rules": { 194 | "body-max-line-length": [ 195 | 0, 196 | "always" 197 | ] 198 | } 199 | }, 200 | "lint-staged": { 201 | "*.ts": [ 202 | "eslint --fix" 203 | ] 204 | }, 205 | "release": { 206 | "branches": [ 207 | "main" 208 | ], 209 | "plugins": [ 210 | "@semantic-release/commit-analyzer", 211 | "@semantic-release/release-notes-generator", 212 | "@semantic-release/npm", 213 | "@semantic-release/github", 214 | [ 215 | "@semantic-release/git", 216 | { 217 | "assets": [ 218 | "package.json" 219 | ], 220 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 221 | } 222 | ] 223 | ] 224 | }, 225 | "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" 226 | } 227 | -------------------------------------------------------------------------------- /providers/scheduler_provider.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | 3 | export default class ScheduleProvider { 4 | constructor(protected app: ApplicationContract) {} 5 | 6 | public register() { 7 | this.app.container.singleton('Verful/Scheduler', () => { 8 | const { default: Scheduler } = 9 | require('../src/scheduler') as typeof import('../src/scheduler') 10 | return new Scheduler(this.app) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/manages_frequencies.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | 3 | import { ManagesFrequenciesContract, Time } from '@ioc:Verful/Scheduler' 4 | 5 | const Weekdays = Object.freeze({ 6 | MONDAY: 1, 7 | TUESDAY: 2, 8 | WEDNESDAY: 3, 9 | THURSDAY: 4, 10 | FRIDAY: 5, 11 | SATURDAY: 6, 12 | SUNDAY: 7, 13 | } as const) 14 | 15 | export default class ManagesFrequencies implements ManagesFrequenciesContract { 16 | public expression = '* * * * *' 17 | 18 | protected currentTimezone = 'UTC' 19 | 20 | public timezone(timezone: string) { 21 | this.currentTimezone = timezone 22 | 23 | return this 24 | } 25 | 26 | protected spliceIntoPosition(position: number, value: string | number) { 27 | const segments = this.expression.split(' ') 28 | 29 | segments[position] = String(value) 30 | 31 | return this.cron(segments.join(' ')) 32 | } 33 | 34 | public everyMinute() { 35 | return this.spliceIntoPosition(0, '*') 36 | } 37 | 38 | public everyTwoMinutes() { 39 | return this.spliceIntoPosition(0, '*/2') 40 | } 41 | 42 | public everyThreeMinutes() { 43 | return this.spliceIntoPosition(0, '*/3') 44 | } 45 | 46 | public everyFourMinutes() { 47 | return this.spliceIntoPosition(0, '*/4') 48 | } 49 | 50 | public everyFiveMinutes() { 51 | return this.spliceIntoPosition(0, '*/5') 52 | } 53 | 54 | public everyTenMinutes() { 55 | return this.spliceIntoPosition(0, '*/10') 56 | } 57 | 58 | public everyFifteenMinutes() { 59 | return this.spliceIntoPosition(0, '*/15') 60 | } 61 | 62 | public everyThirtyMinutes() { 63 | return this.spliceIntoPosition(0, '0,30') 64 | } 65 | 66 | public hourly() { 67 | return this.spliceIntoPosition(0, 0) 68 | } 69 | 70 | public hourlyAt(offset: number | number[]) { 71 | const offsetString = Array.isArray(offset) ? offset.join(',') : offset 72 | 73 | return this.spliceIntoPosition(0, offsetString) 74 | } 75 | 76 | public everyTwoHours() { 77 | return this.spliceIntoPosition(0, 0).spliceIntoPosition(1, '*/2') 78 | } 79 | 80 | public everyThreeHours() { 81 | return this.spliceIntoPosition(0, 0).spliceIntoPosition(1, '*/3') 82 | } 83 | 84 | public everyFourHours() { 85 | return this.spliceIntoPosition(0, 0).spliceIntoPosition(1, '*/4') 86 | } 87 | 88 | public everySixHours() { 89 | return this.spliceIntoPosition(0, 0).spliceIntoPosition(1, '*/6') 90 | } 91 | 92 | public daily() { 93 | return this.spliceIntoPosition(0, 0).spliceIntoPosition(1, 0) 94 | } 95 | 96 | public dailyAt(time: Time) { 97 | const [hour, minute] = time.split(':').map((value) => String(Number(value))) 98 | 99 | return this.spliceIntoPosition(0, minute).spliceIntoPosition(1, hour) 100 | } 101 | 102 | public twiceDaily() { 103 | return this.twiceDailyAt(0, 12, 0) 104 | } 105 | 106 | public twiceDailyAt(first: number, second: number, offset: number = 0) { 107 | const hours = `${first},${second}` 108 | 109 | return this.spliceIntoPosition(0, offset).spliceIntoPosition(1, hours) 110 | } 111 | 112 | public weekly() { 113 | return this.spliceIntoPosition(0, 0).spliceIntoPosition(1, 0).spliceIntoPosition(4, 0) 114 | } 115 | 116 | public weeklyOn(daysOfWeek: number | number[] | string, time: Time) { 117 | return this.days(daysOfWeek).dailyAt(time) 118 | } 119 | 120 | public monthly() { 121 | return this.spliceIntoPosition(0, 0).spliceIntoPosition(1, 0).spliceIntoPosition(2, 1) 122 | } 123 | 124 | public monthlyOn(dayOfMonth: number, time: Time = '00:00') { 125 | return this.dailyAt(time).spliceIntoPosition(2, dayOfMonth) 126 | } 127 | 128 | public twiceMonthly(first = 1, second = 16, time: Time = '00:00') { 129 | const daysOfMonth = `${first},${second}` 130 | 131 | return this.spliceIntoPosition(2, daysOfMonth).dailyAt(time) 132 | } 133 | 134 | public lastDayOfMonth(time: Time = '00:00') { 135 | return this.spliceIntoPosition( 136 | 2, 137 | DateTime.now().setZone(this.currentTimezone).endOf('month').day 138 | ).dailyAt(time) 139 | } 140 | 141 | public quarterly() { 142 | return this.spliceIntoPosition(0, 0) 143 | .spliceIntoPosition(1, 0) 144 | .spliceIntoPosition(2, 1) 145 | .spliceIntoPosition(3, '1-12/3') 146 | } 147 | 148 | public yearly() { 149 | return this.spliceIntoPosition(0, 0) 150 | .spliceIntoPosition(1, 0) 151 | .spliceIntoPosition(2, 1) 152 | .spliceIntoPosition(3, 1) 153 | } 154 | 155 | public yearlyOn(month: number, dayOfMonth: string | number = 1, time: Time = '00:00') { 156 | return this.spliceIntoPosition(2, dayOfMonth).spliceIntoPosition(3, month).dailyAt(time) 157 | } 158 | 159 | public days(days: number | number[] | string) { 160 | const daysString = Array.isArray(days) ? days.join(',') : days 161 | 162 | return this.spliceIntoPosition(4, daysString) 163 | } 164 | 165 | public weekdays() { 166 | return this.days(`${Weekdays.MONDAY}-${Weekdays.FRIDAY}`) 167 | } 168 | 169 | public weekends() { 170 | return this.days(`${Weekdays.SATURDAY},${Weekdays.SUNDAY}`) 171 | } 172 | 173 | public sundays() { 174 | return this.days(Weekdays.SUNDAY) 175 | } 176 | 177 | public mondays() { 178 | return this.days(Weekdays.MONDAY) 179 | } 180 | 181 | public tuesdays() { 182 | return this.days(Weekdays.TUESDAY) 183 | } 184 | 185 | public wednesdays() { 186 | return this.days(Weekdays.WEDNESDAY) 187 | } 188 | 189 | public thursdays() { 190 | return this.days(Weekdays.THURSDAY) 191 | } 192 | 193 | public fridays() { 194 | return this.days(Weekdays.FRIDAY) 195 | } 196 | 197 | public saturdays() { 198 | return this.days(Weekdays.SATURDAY) 199 | } 200 | 201 | public cron(expression: string) { 202 | this.expression = expression 203 | 204 | return this 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/schedule.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | 3 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 4 | import { Condition, ScheduleContract, ScheduleHandler, Time } from '@ioc:Verful/Scheduler' 5 | 6 | import ManagesFrequencies from './manages_frequencies' 7 | 8 | export default class Schedule extends ManagesFrequencies implements ScheduleContract { 9 | constructor( 10 | private app: ApplicationContract, 11 | public command: ScheduleHandler 12 | ) { 13 | super() 14 | } 15 | 16 | protected inTimeInterval(startTime: Time, endTime: Time) { 17 | const [startHours, startMinutes] = startTime.split(':').map(Number) 18 | const [endHours, endMinutes] = endTime.split(':').map(Number) 19 | 20 | let [now, start, end] = [ 21 | DateTime.now().setZone(this.currentTimezone), 22 | DateTime.now() 23 | .set({ minute: startMinutes, hour: startHours, second: 0, millisecond: 0 }) 24 | .setZone(this.currentTimezone), 25 | DateTime.now() 26 | .set({ minute: endMinutes, hour: endHours, second: 0, millisecond: 0 }) 27 | .setZone(this.currentTimezone), 28 | ] 29 | 30 | if (end < start) { 31 | if (start > now) { 32 | start = start.minus({ days: 1 }) 33 | } else { 34 | end = end.plus({ days: 1 }) 35 | } 36 | } 37 | 38 | return () => now > start && now < end 39 | } 40 | 41 | public between(start: Time, end: Time) { 42 | return this.when(this.inTimeInterval(start, end)) 43 | } 44 | 45 | public unlessBetween(start: Time, end: Time) { 46 | return this.skip(this.inTimeInterval(start, end)) 47 | } 48 | 49 | public filters: Condition[] = [] 50 | 51 | public rejects: Condition[] = [] 52 | 53 | public skip(condition: Condition) { 54 | this.rejects.push(condition) 55 | 56 | return this 57 | } 58 | 59 | public when(condition: Condition) { 60 | this.filters.push(condition) 61 | 62 | return this 63 | } 64 | 65 | public environments(environments: Array<'production' | 'development' | 'staging' | 'test'>) { 66 | this.when(() => environments.includes(this.app.nodeEnvironment as any)) 67 | 68 | return this 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/scheduler.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import cron from 'node-cron' 3 | 4 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 5 | import { ScheduleContract, SchedulerContract } from '@ioc:Verful/Scheduler' 6 | 7 | import Schedule from './schedule' 8 | 9 | export default class Scheduler implements SchedulerContract { 10 | private events: Schedule[] = [] 11 | 12 | constructor(private app: ApplicationContract) {} 13 | 14 | public call(callback: () => void): ScheduleContract { 15 | const schedule = new Schedule(this.app, callback) 16 | 17 | this.events.push(schedule) 18 | 19 | return schedule 20 | } 21 | 22 | public command(command: string | string[]) { 23 | const commandArguments = Array.isArray(command) ? command : command.split(' ') 24 | const callback = async () => { 25 | try { 26 | const result = await execa.node('ace', commandArguments) 27 | return result.stdout 28 | } catch (error) { 29 | return error 30 | } 31 | } 32 | 33 | return this.call(callback) 34 | } 35 | 36 | public exec(command: string) { 37 | const callback = async () => { 38 | try { 39 | const result = await execa.command(command) 40 | return result.stdout 41 | } catch (error) { 42 | return error 43 | } 44 | } 45 | 46 | return this.call(callback) 47 | } 48 | 49 | public start() { 50 | this.app.logger.info('Schedule processing started') 51 | for (const event of this.events) 52 | cron.schedule(event.expression, async () => { 53 | for (const filter of event.filters) { 54 | if (!(await filter())) return 55 | } 56 | 57 | for (const reject of event.rejects) { 58 | if (await reject()) return 59 | } 60 | 61 | return await event.command() 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /templates/tasks.txt: -------------------------------------------------------------------------------- 1 | import Scheduler from '@ioc:Verful/Scheduler' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Scheduled tasks 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Scheduled tasks allow you to run recurrent tasks in the background of your 9 | | application. Here you can define all your scheduled tasks. 10 | | 11 | | You can define a scheduled task using the `.call` method on the Scheduler object 12 | | as shown in the following example 13 | | 14 | | ``` 15 | | Scheduler.call(() => { 16 | | console.log('I am a scheduled task') 17 | | }).everyMinute() 18 | | ``` 19 | | 20 | | The example above will print the message `I am a scheduled task` every minute. 21 | | 22 | | You can also schedule ace commands using the `.command` method on the Scheduler 23 | | object as shown in the following example 24 | | 25 | | ``` 26 | | Scheduler.command('greet').everyMinute() 27 | | ``` 28 | | 29 | | The example above will run the `greet` command every minute. 30 | | 31 | | You can also schedule shell commands with arguments using the `.exec` method on the Scheduler 32 | | object as shown in the following example 33 | | 34 | | ``` 35 | | Scheduler.exec('node ace greet').everyMinute() 36 | | ``` 37 | | 38 | | The example above will run the `node ace greet` command every minute. 39 | | 40 | | Happy scheduling! 41 | */ 42 | -------------------------------------------------------------------------------- /tests/manages_frequencies.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@japa/runner' 2 | import sinon from 'sinon' 3 | 4 | import ManagesFrequencies from '../src/manages_frequencies' 5 | 6 | test.group('ManagesFrequencies', (group) => { 7 | group.each.setup(() => { 8 | return () => sinon.restore() 9 | }) 10 | 11 | test('can create instance', ({ assert }) => { 12 | const managesFrequencies = new ManagesFrequencies() 13 | 14 | assert.instanceOf(managesFrequencies, ManagesFrequencies) 15 | }) 16 | 17 | test('can set timezone', ({ assert }) => { 18 | const managesFrequencies = new ManagesFrequencies() 19 | const timezone = 'America/New_York' 20 | 21 | managesFrequencies.timezone(timezone) 22 | 23 | assert.equal(managesFrequencies['currentTimezone'], timezone) 24 | }) 25 | 26 | test('can set expression with cron', ({ assert }) => { 27 | const managesFrequencies = new ManagesFrequencies() 28 | const expression = '0 0 * * *' 29 | 30 | managesFrequencies.cron(expression) 31 | 32 | assert.equal(managesFrequencies['expression'], expression) 33 | }) 34 | 35 | test('can set expression for every minute', ({ assert }) => { 36 | const managesFrequencies = new ManagesFrequencies() 37 | 38 | managesFrequencies.everyMinute() 39 | 40 | assert.equal(managesFrequencies['expression'], '* * * * *') 41 | }) 42 | 43 | test('can set expression for every two minutes', ({ assert }) => { 44 | const managesFrequencies = new ManagesFrequencies() 45 | 46 | managesFrequencies.everyTwoMinutes() 47 | 48 | assert.equal(managesFrequencies['expression'], '*/2 * * * *') 49 | }) 50 | 51 | test('can set expression for every three minutes', ({ assert }) => { 52 | const managesFrequencies = new ManagesFrequencies() 53 | 54 | managesFrequencies.everyThreeMinutes() 55 | 56 | assert.equal(managesFrequencies['expression'], '*/3 * * * *') 57 | }) 58 | 59 | test('can set expression for every four minutes', ({ assert }) => { 60 | const managesFrequencies = new ManagesFrequencies() 61 | 62 | managesFrequencies.everyFourMinutes() 63 | 64 | assert.equal(managesFrequencies['expression'], '*/4 * * * *') 65 | }) 66 | 67 | test('can set expression for every five minutes', ({ assert }) => { 68 | const managesFrequencies = new ManagesFrequencies() 69 | 70 | managesFrequencies.everyFiveMinutes() 71 | 72 | assert.equal(managesFrequencies['expression'], '*/5 * * * *') 73 | }) 74 | 75 | test('can set expression for every ten minutes', ({ assert }) => { 76 | const managesFrequencies = new ManagesFrequencies() 77 | 78 | managesFrequencies.everyTenMinutes() 79 | 80 | assert.equal(managesFrequencies['expression'], '*/10 * * * *') 81 | }) 82 | 83 | test('can set expression for every fifteen minutes', ({ assert }) => { 84 | const managesFrequencies = new ManagesFrequencies() 85 | 86 | managesFrequencies.everyFifteenMinutes() 87 | 88 | assert.equal(managesFrequencies['expression'], '*/15 * * * *') 89 | }) 90 | 91 | test('can set expression for every thirty minutes', ({ assert }) => { 92 | const managesFrequencies = new ManagesFrequencies() 93 | 94 | managesFrequencies.everyThirtyMinutes() 95 | 96 | assert.equal(managesFrequencies['expression'], '0,30 * * * *') 97 | }) 98 | 99 | test('can set expression for hourly', ({ assert }) => { 100 | const managesFrequencies = new ManagesFrequencies() 101 | 102 | managesFrequencies.hourly() 103 | 104 | assert.equal(managesFrequencies['expression'], '0 * * * *') 105 | }) 106 | 107 | test('can set expression for hourly at offset', ({ assert }) => { 108 | const managesFrequencies = new ManagesFrequencies() 109 | 110 | managesFrequencies.hourlyAt(3) 111 | 112 | assert.equal(managesFrequencies['expression'], '3 * * * *') 113 | }) 114 | 115 | test('can set expression for hourly at multiple offsets', ({ assert }) => { 116 | const managesFrequencies = new ManagesFrequencies() 117 | 118 | managesFrequencies.hourlyAt([3, 9]) 119 | 120 | assert.equal(managesFrequencies['expression'], '3,9 * * * *') 121 | }) 122 | 123 | test('can set expression for every two hours', ({ assert }) => { 124 | const managesFrequencies = new ManagesFrequencies() 125 | 126 | managesFrequencies.everyTwoHours() 127 | 128 | assert.equal(managesFrequencies['expression'], '0 */2 * * *') 129 | }) 130 | 131 | test('can set expression for every three hours', ({ assert }) => { 132 | const managesFrequencies = new ManagesFrequencies() 133 | 134 | managesFrequencies.everyThreeHours() 135 | 136 | assert.equal(managesFrequencies['expression'], '0 */3 * * *') 137 | }) 138 | 139 | test('can set expression for every four hours', ({ assert }) => { 140 | const managesFrequencies = new ManagesFrequencies() 141 | 142 | managesFrequencies.everyFourHours() 143 | 144 | assert.equal(managesFrequencies['expression'], '0 */4 * * *') 145 | }) 146 | 147 | test('can set expression for every six hours', ({ assert }) => { 148 | const managesFrequencies = new ManagesFrequencies() 149 | 150 | managesFrequencies.everySixHours() 151 | 152 | assert.equal(managesFrequencies['expression'], '0 */6 * * *') 153 | }) 154 | 155 | test('can set expression for daily', ({ assert }) => { 156 | const managesFrequencies = new ManagesFrequencies() 157 | 158 | managesFrequencies.daily() 159 | 160 | assert.equal(managesFrequencies['expression'], '0 0 * * *') 161 | }) 162 | 163 | test('can set expression for daily at specific time', ({ assert }) => { 164 | const managesFrequencies = new ManagesFrequencies() 165 | 166 | managesFrequencies.dailyAt('12:30') 167 | 168 | assert.equal(managesFrequencies['expression'], '30 12 * * *') 169 | }) 170 | 171 | test('can set expression for twice daily', ({ assert }) => { 172 | const managesFrequencies = new ManagesFrequencies() 173 | 174 | managesFrequencies.twiceDaily() 175 | 176 | assert.equal(managesFrequencies['expression'], '0 0,12 * * *') 177 | }) 178 | 179 | test('can set expression for twice daily at specific times', ({ assert }) => { 180 | const managesFrequencies = new ManagesFrequencies() 181 | 182 | managesFrequencies.twiceDailyAt(1, 13) 183 | 184 | assert.equal(managesFrequencies['expression'], '0 1,13 * * *') 185 | }) 186 | 187 | test('can set expression for weekly', ({ assert }) => { 188 | const managesFrequencies = new ManagesFrequencies() 189 | 190 | managesFrequencies.weekly() 191 | 192 | assert.equal(managesFrequencies['expression'], '0 0 * * 0') 193 | }) 194 | 195 | test('can set expression for weekly on specific days', ({ assert }) => { 196 | const managesFrequencies = new ManagesFrequencies() 197 | 198 | managesFrequencies.weeklyOn([2, 5], '15:00') 199 | 200 | assert.equal(managesFrequencies['expression'], '0 15 * * 2,5') 201 | }) 202 | 203 | test('can set days of the week', ({ assert }) => { 204 | const managesFrequencies = new ManagesFrequencies() 205 | const daysOfWeek = [1, 2, 3] // Monday, Tuesday, Wednesday 206 | 207 | managesFrequencies.days(daysOfWeek) 208 | 209 | assert.equal(managesFrequencies['expression'], '* * * * 1,2,3') 210 | }) 211 | 212 | test('can set weekdays', ({ assert }) => { 213 | const managesFrequencies = new ManagesFrequencies() 214 | 215 | managesFrequencies.weekdays() 216 | 217 | assert.equal(managesFrequencies['expression'], '* * * * 1-5') 218 | }) 219 | 220 | test('can set weekends', ({ assert }) => { 221 | const managesFrequencies = new ManagesFrequencies() 222 | 223 | managesFrequencies.weekends() 224 | 225 | assert.equal(managesFrequencies['expression'], '* * * * 6,7') 226 | }) 227 | 228 | test('can set specific day of the week', ({ assert }) => { 229 | const managesFrequencies = new ManagesFrequencies() 230 | 231 | managesFrequencies.sundays() 232 | 233 | assert.equal(managesFrequencies['expression'], '* * * * 7') 234 | }) 235 | 236 | test('can set expression for mondays', ({ assert }) => { 237 | const managesFrequencies = new ManagesFrequencies() 238 | 239 | managesFrequencies.mondays() 240 | 241 | assert.equal(managesFrequencies['expression'], '* * * * 1') 242 | }) 243 | 244 | test('can set expression for tuesdays', ({ assert }) => { 245 | const managesFrequencies = new ManagesFrequencies() 246 | 247 | managesFrequencies.tuesdays() 248 | 249 | assert.equal(managesFrequencies['expression'], '* * * * 2') 250 | }) 251 | 252 | test('can set expression for wednesdays', ({ assert }) => { 253 | const managesFrequencies = new ManagesFrequencies() 254 | 255 | managesFrequencies.wednesdays() 256 | 257 | assert.equal(managesFrequencies['expression'], '* * * * 3') 258 | }) 259 | 260 | test('can set expression for thursdays', ({ assert }) => { 261 | const managesFrequencies = new ManagesFrequencies() 262 | 263 | managesFrequencies.thursdays() 264 | 265 | assert.equal(managesFrequencies['expression'], '* * * * 4') 266 | }) 267 | 268 | test('can set expression for fridays', ({ assert }) => { 269 | const managesFrequencies = new ManagesFrequencies() 270 | 271 | managesFrequencies.fridays() 272 | 273 | assert.equal(managesFrequencies['expression'], '* * * * 5') 274 | }) 275 | 276 | test('can set expression for saturdays', ({ assert }) => { 277 | const managesFrequencies = new ManagesFrequencies() 278 | 279 | managesFrequencies.saturdays() 280 | 281 | assert.equal(managesFrequencies['expression'], '* * * * 6') 282 | }) 283 | 284 | test('can combine frequency methods', ({ assert }) => { 285 | const managesFrequencies = new ManagesFrequencies() 286 | 287 | managesFrequencies.everyTwoHours().weekdays() 288 | 289 | assert.equal(managesFrequencies['expression'], '0 */2 * * 1-5') 290 | }) 291 | 292 | test('can combine frequency methods for specific days', ({ assert }) => { 293 | const managesFrequencies = new ManagesFrequencies() 294 | 295 | managesFrequencies.weeklyOn([2, 4], '12:00') 296 | 297 | assert.equal(managesFrequencies['expression'], '0 12 * * 2,4') 298 | }) 299 | 300 | test('can set expression for monthly', ({ assert }) => { 301 | const managesFrequencies = new ManagesFrequencies() 302 | 303 | managesFrequencies.monthly() 304 | 305 | assert.equal(managesFrequencies['expression'], '0 0 1 * *') 306 | }) 307 | 308 | test('can set expression for monthly on specific day and time', ({ assert }) => { 309 | const managesFrequencies = new ManagesFrequencies() 310 | 311 | managesFrequencies.monthlyOn(15, '14:30') 312 | 313 | assert.equal(managesFrequencies['expression'], '30 14 15 * *') 314 | }) 315 | 316 | test('can set expression for twice monthly', ({ assert }) => { 317 | const managesFrequencies = new ManagesFrequencies() 318 | 319 | managesFrequencies.twiceMonthly() 320 | 321 | assert.equal(managesFrequencies['expression'], '0 0 1,16 * *') 322 | }) 323 | 324 | test('can set expression for last day of month', ({ assert }) => { 325 | const managesFrequencies = new ManagesFrequencies() 326 | 327 | managesFrequencies.lastDayOfMonth() 328 | 329 | const lastDayOfMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0) 330 | const expectedExpression = `0 0 ${lastDayOfMonth.getDate()} * *` 331 | 332 | assert.equal(managesFrequencies['expression'], expectedExpression) 333 | }) 334 | 335 | test('can set expression for quarterly', ({ assert }) => { 336 | const managesFrequencies = new ManagesFrequencies() 337 | 338 | managesFrequencies.quarterly() 339 | 340 | assert.equal(managesFrequencies['expression'], '0 0 1 1-12/3 *') 341 | }) 342 | 343 | test('can set expression for yearly', ({ assert }) => { 344 | const managesFrequencies = new ManagesFrequencies() 345 | 346 | managesFrequencies.yearly() 347 | 348 | assert.equal(managesFrequencies['expression'], '0 0 1 1 *') 349 | }) 350 | 351 | test('can set expression for yearly on specific month and day', ({ assert }) => { 352 | const managesFrequencies = new ManagesFrequencies() 353 | 354 | managesFrequencies.yearlyOn(6, 20, '10:45') 355 | 356 | assert.equal(managesFrequencies['expression'], '45 10 20 6 *') 357 | }) 358 | }) 359 | -------------------------------------------------------------------------------- /tests/schedule.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@japa/runner' 2 | import { DateTime } from 'luxon' 3 | import sinon from 'sinon' 4 | 5 | import Schedule from '../src/schedule' 6 | 7 | test.group('Schedule', (group) => { 8 | group.each.setup(() => { 9 | // Stub DateTime.now() to a fixed value for consistent testing 10 | sinon.stub(DateTime, 'now').returns( 11 | DateTime.fromObject({ 12 | year: 2021, 13 | month: 7, 14 | day: 31, 15 | hour: 7, 16 | minute: 30, 17 | }) 18 | ) // July 31, 2021 7:30 19 | 20 | return () => { 21 | // Restore the stubs 22 | sinon.restore() 23 | } 24 | }) 25 | 26 | test('can set between condition', ({ assert }) => { 27 | const schedule = new Schedule({} as any, () => {}) 28 | 29 | const start = '7:00' 30 | const end = '8:00' 31 | 32 | schedule.between(start, end) 33 | 34 | assert.lengthOf(schedule.filters, 1) 35 | assert.isFunction(schedule.filters[0]) 36 | }) 37 | 38 | test('can set unlessBetween condition', ({ assert }) => { 39 | const schedule = new Schedule({} as any, () => {}) 40 | 41 | const start = '7:00' 42 | const end = '8:00' 43 | 44 | schedule.unlessBetween(start, end) 45 | 46 | assert.lengthOf(schedule.rejects, 1) 47 | assert.isFunction(schedule.rejects[0]) 48 | }) 49 | 50 | test('can set between condition that wraps midnight', ({ assert }) => { 51 | const schedule = new Schedule({} as any, () => {}) 52 | 53 | const start = '23:00' 54 | const end = '1:00' 55 | 56 | schedule.between(start, end) 57 | 58 | assert.lengthOf(schedule.filters, 1) 59 | assert.isFunction(schedule.filters[0]) 60 | }) 61 | 62 | test('can set unlessBetween condition that wraps midnight', ({ assert }) => { 63 | const schedule = new Schedule({} as any, () => {}) 64 | 65 | const start = '23:00' 66 | const end = '1:00' 67 | 68 | schedule.unlessBetween(start, end) 69 | 70 | assert.lengthOf(schedule.rejects, 1) 71 | assert.isFunction(schedule.rejects[0]) 72 | }) 73 | 74 | test('time interval check is correct', ({ assert }) => { 75 | const schedule = new Schedule({} as any, () => {}) 76 | 77 | assert.isTrue(schedule['inTimeInterval']('7:00', '8:00')()) 78 | assert.isTrue(schedule['inTimeInterval']('23:00', '8:00')()) 79 | 80 | assert.isFalse(schedule['inTimeInterval']('6:00', '7:00')()) 81 | assert.isFalse(schedule['inTimeInterval']('23:00', '1:00')()) 82 | }) 83 | 84 | test('can set skip condition', ({ assert }) => { 85 | const schedule = new Schedule({} as any, () => {}) 86 | 87 | schedule.skip(() => true) 88 | 89 | assert.lengthOf(schedule.rejects, 1) 90 | assert.isFunction(schedule.rejects[0]) 91 | }) 92 | 93 | test('can set when condition', ({ assert }) => { 94 | const schedule = new Schedule({} as any, () => {}) 95 | 96 | schedule.when(() => true) 97 | 98 | assert.lengthOf(schedule.filters, 1) 99 | assert.isFunction(schedule.filters[0]) 100 | }) 101 | 102 | test('can set environments condition', ({ assert }) => { 103 | const schedule = new Schedule({ nodeEnvironment: 'test' } as any, () => {}) 104 | 105 | schedule.environments(['test']) 106 | 107 | assert.lengthOf(schedule.filters, 1) 108 | assert.isFunction(schedule.filters[0]) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /tests/scheduler_provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@japa/runner' 2 | 3 | import Scheduler from '../src/scheduler' 4 | 5 | test.group('SchedulerProvider', () => { 6 | test('Bindings registered correctly', ({ assert, app }) => { 7 | assert.instanceOf(app.container.resolveBinding('Verful/Scheduler'), Scheduler) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /tests/sheduler.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@japa/runner' 2 | import execa from 'execa' 3 | import cron from 'node-cron' 4 | import sinon from 'sinon' 5 | 6 | import Scheduler from '../src/scheduler' 7 | 8 | test.group('Scheduler', (group) => { 9 | group.each.setup(() => { 10 | return () => sinon.restore() 11 | }) 12 | 13 | test('calls a callback on scheduling', ({ app, assert }) => { 14 | const scheduler = new Scheduler(app) 15 | const callback = sinon.stub() 16 | 17 | scheduler.call(callback) 18 | 19 | assert.lengthOf(scheduler['events'], 1) 20 | }) 21 | 22 | test('schedules a command to be executed', ({ app, assert }) => { 23 | sinon.stub(execa, 'node') 24 | 25 | const scheduler = new Scheduler(app) 26 | const command = 'some:command' 27 | scheduler.command(command) 28 | 29 | assert.lengthOf(scheduler['events'], 1) 30 | }) 31 | 32 | test('schedules an execution of a command', ({ app, assert }) => { 33 | sinon.stub(execa, 'node') 34 | 35 | const scheduler = new Scheduler(app) 36 | const command = 'some:command' 37 | scheduler.exec(command) 38 | 39 | assert.lengthOf(scheduler['events'], 1) 40 | }) 41 | 42 | test('the scheduler executes scheduled functions', async ({ app, assert }) => { 43 | assert.plan(3) 44 | 45 | const scheduler = new Scheduler(app) 46 | const mockFilter = sinon.stub().resolves(true) 47 | const mockReject = sinon.stub().resolves(false) 48 | const mockCommand = sinon.stub().resolves('Command executed') 49 | 50 | scheduler.call(mockCommand).when(mockFilter).skip(mockReject).everyMinute() 51 | 52 | // @ts-ignore: Simulate cron triggering the callback 53 | sinon.stub(cron, 'schedule').callsFake((expression, cronCallback) => { 54 | // @ts-ignore 55 | cronCallback().then(() => { 56 | assert.isTrue(mockFilter.called) 57 | assert.isTrue(mockReject.called) 58 | assert.isTrue(mockCommand.called) 59 | }) 60 | }) 61 | 62 | await scheduler.start() 63 | }) 64 | 65 | test('the scheduler executes scheduled ace commands', async ({ app, assert }) => { 66 | assert.plan(3) 67 | 68 | const scheduler = new Scheduler(app) 69 | const mockFilter = sinon.stub().resolves(true) 70 | const mockReject = sinon.stub().resolves(false) 71 | 72 | scheduler.command('make:user').when(mockFilter).skip(mockReject).everyMinute() 73 | 74 | // @ts-ignore: stub execa.node to avoid executing the command 75 | const execaStub = sinon.stub(execa, 'node').resolves({ stdout: 'Command executed' }) 76 | 77 | // @ts-ignore: Simulate cron triggering the callback 78 | sinon.stub(cron, 'schedule').callsFake((expression, cronCallback) => { 79 | // @ts-ignore 80 | cronCallback().then(() => { 81 | assert.isTrue(mockFilter.called) 82 | assert.isTrue(mockReject.called) 83 | assert.isTrue(execaStub.called) 84 | }) 85 | }) 86 | 87 | await scheduler.start() 88 | }) 89 | 90 | test('the scheduler executes scheduled shell commands', async ({ app, assert }) => { 91 | assert.plan(3) 92 | 93 | const scheduler = new Scheduler(app) 94 | const mockFilter = sinon.stub().resolves(true) 95 | const mockReject = sinon.stub().resolves(false) 96 | 97 | scheduler.exec('node ace make:user').when(mockFilter).skip(mockReject).everyMinute() 98 | 99 | // @ts-ignore: stub execa.node to avoid executing the command 100 | const execaStub = sinon.stub(execa, 'command').resolves({ stdout: 'Command executed' }) 101 | 102 | // @ts-ignore: Simulate cron triggering the callback 103 | sinon.stub(cron, 'schedule').callsFake((expression, cronCallback) => { 104 | // @ts-ignore 105 | cronCallback().then(() => { 106 | assert.isTrue(mockFilter.called) 107 | assert.isTrue(mockReject.called) 108 | assert.isTrue(execaStub.called) 109 | }) 110 | }) 111 | 112 | await scheduler.start() 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "CommonJS", 5 | "lib": ["ES2022"], 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "removeComments": true, 9 | "declaration": true, 10 | "rootDir": "./", 11 | "outDir": "./build", 12 | "strictNullChecks": true, 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noImplicitAny": true, 17 | "strictFunctionTypes": true, 18 | "noImplicitThis": true, 19 | "skipLibCheck": true, 20 | "experimentalDecorators": true, 21 | "emitDecoratorMetadata": true, 22 | "types": ["@types/node", "@adonisjs/core"] 23 | }, 24 | "include": ["./**/*"], 25 | "exclude": ["./node_modules", "./build"] 26 | } 27 | --------------------------------------------------------------------------------