├── .editorconfig ├── .env.defaults ├── .env.schema ├── .env.test ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ ├── --feature-request.md │ └── --question.md └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .remarkignore ├── LICENSE ├── README.md ├── api.js ├── app └── controllers │ ├── api │ ├── index.js │ └── v1 │ │ ├── config.js │ │ ├── control.js │ │ ├── index.js │ │ ├── jobs.js │ │ └── sse.js │ └── index.js ├── config ├── api.js ├── env.js ├── index.js └── logger.js ├── gulpfile.js ├── helpers ├── logger.js └── markdown.js ├── index.js ├── locales ├── ar.json ├── cs.json ├── da.json ├── de.json ├── en.json ├── es.json ├── fi.json ├── fr.json ├── he.json ├── hu.json ├── id.json ├── it.json ├── ja.json ├── ko.json ├── nl.json ├── no.json ├── pl.json ├── pt.json ├── ru.json ├── sv.json ├── th.json ├── tr.json ├── uk.json ├── vi.json └── zh.json ├── nodemon.json ├── package-scripts.js ├── package.json ├── routes ├── api │ ├── index.js │ └── v1 │ │ └── index.js └── index.js ├── test ├── api │ └── v1 │ │ ├── config.js │ │ ├── index.js │ │ ├── jobs │ │ ├── get.js │ │ └── post.js │ │ ├── no-jwt.js │ │ ├── restart.js │ │ ├── run.js │ │ ├── sse.js │ │ ├── start.js │ │ └── stop.js ├── jobs │ ├── basic.js │ ├── index.js │ └── long.js ├── plugin │ ├── index.js │ └── options.js └── utils.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.env.defaults: -------------------------------------------------------------------------------- 1 | ################# 2 | ## environment ## 3 | ################# 4 | NODE_ENV=development 5 | 6 | ########### 7 | ## proxy ## 8 | ########### 9 | PROXY_PORT= 10 | 11 | ########## 12 | ## http ## 13 | ########## 14 | HTTP_PROTOCOL=http 15 | HTTP_PORT= 16 | 17 | ################ 18 | ## api server ## 19 | ################ 20 | API_HOST=localhost 21 | API_PORT=4000 22 | API_PROTOCOL={{HTTP_PROTOCOL}} 23 | API_URL={{API_PROTOCOL}}://{{API_HOST}}:{{API_PORT}} 24 | API_SSL_KEY_PATH= 25 | API_SSL_CERT_PATH= 26 | API_SSL_CA_PATH= 27 | API_RATELIMIT_WHITELIST= 28 | 29 | ######### 30 | ## app ## 31 | ######### 32 | APP_NAME=Lad 33 | APP_COLOR=#94CC27 34 | TWITTER=@niftylettuce 35 | TRANSPORT_DEBUG=false 36 | SHOW_STACK=true 37 | SHOW_META=true 38 | SUPPORT_REQUEST_MAX_LENGTH=5000 39 | # koa-better-error-handler 40 | ERROR_HANDLER_BASE_URL={{WEB_URL}} 41 | # @ladjs/i18n 42 | I18N_SYNC_FILES=true 43 | I18N_AUTO_RELOAD=true 44 | I18N_UPDATE_FILES=true 45 | # @ladjs/auth 46 | AUTH_LOCAL_ENABLED=true 47 | AUTH_FACEBOOK_ENABLED=false 48 | AUTH_TWITTER_ENABLED=false 49 | AUTH_GOOGLE_ENABLED=false 50 | AUTH_GITHUB_ENABLED=false 51 | AUTH_LINKEDIN_ENABLED=false 52 | AUTH_INSTAGRAM_ENABLED=false 53 | AUTH_OTP_ENABLED=false 54 | AUTH_STRIPE_ENABLED=false 55 | # your google client ID and secret from: 56 | # https://console.developers.google.com 57 | GOOGLE_CLIENT_ID= 58 | GOOGLE_CLIENT_SECRET= 59 | GOOGLE_CALLBACK_URL={{{WEB_URL}}}/auth/google/ok 60 | GOOGLE_APPLICATION_CREDENTIALS= 61 | # your github client ID and secret from: 62 | # https://github.com/settings/applications 63 | GITHUB_CLIENT_ID= 64 | GITHUB_CLIENT_SECRET= 65 | GITHUB_CALLBACK_URL={{{WEB_URL}}}/auth/github/ok 66 | # your Postmark token from: 67 | # https//postmarkapp.com 68 | POSTMARK_API_TOKEN= 69 | # your CodeCov token from: 70 | # https://codecov.io 71 | CODECOV_TOKEN= 72 | # aws credentials 73 | # https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html 74 | # https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html 75 | # https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/loading-node-credentials-json-file.html 76 | AWS_PROFILE= 77 | AWS_S3_BUCKET= 78 | AWS_CLOUDFRONT_DOMAIN= 79 | 80 | ############# 81 | ## mongodb ## 82 | ############# 83 | MONGO_USER= 84 | MONGO_PASS= 85 | MONGO_HOST=localhost 86 | MONGO_PORT=27017 87 | MONGO_NAME=lad_{{NODE_ENV}} 88 | MONGO_URI="mongodb://{{MONGO_HOST}}:{{MONGO_PORT}}/{{MONGO_NAME}}" 89 | 90 | WEB_MONGO_USER={{MONGO_USER}} 91 | WEB_MONGO_PASS={{MONGO_PASS}} 92 | WEB_MONGO_HOST={{MONGO_HOST}} 93 | WEB_MONGO_PORT={{MONGO_PORT}} 94 | WEB_MONGO_NAME={{MONGO_NAME}} 95 | WEB_MONGO_URI={{{MONGO_URI}}} 96 | 97 | API_MONGO_USER={{MONGO_USER}} 98 | API_MONGO_PASS={{MONGO_PASS}} 99 | API_MONGO_HOST={{MONGO_HOST}} 100 | API_MONGO_PORT={{MONGO_PORT}} 101 | API_MONGO_NAME={{MONGO_NAME}} 102 | API_MONGO_URI={{{MONGO_URI}}} 103 | 104 | BREE_MONGO_USER={{MONGO_USER}} 105 | BREE_MONGO_PASS={{MONGO_PASS}} 106 | BREE_MONGO_HOST={{MONGO_HOST}} 107 | BREE_MONGO_PORT={{MONGO_PORT}} 108 | BREE_MONGO_NAME={{MONGO_NAME}} 109 | BREE_MONGO_URI={{{MONGO_URI}}} 110 | 111 | ########### 112 | ## redis ## 113 | ########### 114 | REDIS_PORT=6379 115 | REDIS_HOST=localhost 116 | REDIS_PASSWORD= 117 | WEB_REDIS_PORT={{REDIS_PORT}} 118 | WEB_REDIS_HOST={{REDIS_HOST}} 119 | WEB_REDIS_PASSWORD={{REDIS_PASSWORD}} 120 | API_REDIS_PORT={{REDIS_PORT}} 121 | API_REDIS_HOST={{REDIS_HOST}} 122 | API_REDIS_PASSWORD={{REDIS_PASSWORD}} 123 | BREE_REDIS_PORT={{REDIS_PORT}} 124 | BREE_REDIS_HOST={{REDIS_HOST}} 125 | BREE_REDIS_PASSWORD={{REDIS_PASSWORD}} 126 | MANDARIN_REDIS_PORT={{REDIS_PORT}} 127 | MANDARIN_REDIS_HOST={{REDIS_HOST}} 128 | MANDARIN_REDIS_PASSWORD={{REDIS_PASSWORD}} 129 | 130 | ############# 131 | ## certbot ## 132 | ############# 133 | CERTBOT_WELL_KNOWN_NAME= 134 | CERTBOT_WELL_KNOWN_CONTENTS= 135 | 136 | ################# 137 | ## api secrets ## 138 | ################# 139 | API_SECRETS=secret, 140 | JWT_SECRET=secret 141 | 142 | ##################### 143 | ## cache responses ## 144 | ##################### 145 | CACHE_RESPONSES= 146 | 147 | ##################### 148 | ## slack api token ## 149 | ##################### 150 | SLACK_API_TOKEN= 151 | -------------------------------------------------------------------------------- /.env.schema: -------------------------------------------------------------------------------- 1 | ################# 2 | ## environment ## 3 | ################# 4 | NODE_ENV= 5 | 6 | ########### 7 | ## proxy ## 8 | ########### 9 | PROXY_PORT= 10 | 11 | ########## 12 | ## http ## 13 | ########## 14 | HTTP_PROTOCOL= 15 | HTTP_PORT= 16 | 17 | ################ 18 | ## api server ## 19 | ################ 20 | API_HOST= 21 | API_PORT= 22 | API_PROTOCOL= 23 | API_URL= 24 | API_SSL_KEY_PATH= 25 | API_SSL_CERT_PATH= 26 | API_SSL_CA_PATH= 27 | API_RATELIMIT_WHITELIST= 28 | 29 | ######### 30 | ## app ## 31 | ######### 32 | APP_NAME= 33 | APP_COLOR= 34 | TWITTER= 35 | TRANSPORT_DEBUG= 36 | SHOW_STACK= 37 | SHOW_META= 38 | SUPPORT_REQUEST_MAX_LENGTH= 39 | # koa-better-error-handler 40 | ERROR_HANDLER_BASE_URL= 41 | # @ladjs/i18n 42 | I18N_SYNC_FILES= 43 | I18N_AUTO_RELOAD= 44 | I18N_UPDATE_FILES= 45 | # @ladjs/auth 46 | AUTH_LOCAL_ENABLED= 47 | AUTH_FACEBOOK_ENABLED= 48 | AUTH_TWITTER_ENABLED= 49 | AUTH_GOOGLE_ENABLED= 50 | AUTH_GITHUB_ENABLED= 51 | AUTH_LINKEDIN_ENABLED= 52 | AUTH_INSTAGRAM_ENABLED= 53 | AUTH_OTP_ENABLED= 54 | AUTH_STRIPE_ENABLED= 55 | # your google client ID and secret from: 56 | # https://console.developers.google.com 57 | GOOGLE_CLIENT_ID= 58 | GOOGLE_CLIENT_SECRET= 59 | GOOGLE_CALLBACK_URL= 60 | GOOGLE_APPLICATION_CREDENTIALS= 61 | # your github client ID and secret from: 62 | # https://github.com/settings/applications 63 | GITHUB_CLIENT_ID= 64 | GITHUB_CLIENT_SECRET= 65 | GITHUB_CALLBACK_URL= 66 | # your Postmark token from: 67 | # https//postmarkapp.com 68 | POSTMARK_API_TOKEN= 69 | # your CodeCov token from: 70 | # https://codecov.io 71 | CODECOV_TOKEN= 72 | # aws credentials 73 | # https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html 74 | # https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html 75 | # https://docs.aws.amazon.com/en_pv/sdk-for-javascript/v2/developer-guide/loading-node-credentials-json-file.html 76 | AWS_PROFILE= 77 | AWS_S3_BUCKET= 78 | AWS_CLOUDFRONT_DOMAIN= 79 | 80 | ############# 81 | ## mongodb ## 82 | ############# 83 | MONGO_USER= 84 | MONGO_PASS= 85 | MONGO_HOST= 86 | MONGO_PORT= 87 | MONGO_NAME= 88 | MONGO_URI= 89 | 90 | WEB_MONGO_USER= 91 | WEB_MONGO_PASS= 92 | WEB_MONGO_HOST= 93 | WEB_MONGO_NAME= 94 | WEB_MONGO_PORT= 95 | WEB_MONGO_URI= 96 | 97 | API_MONGO_PASS= 98 | API_MONGO_USER= 99 | API_MONGO_HOST= 100 | API_MONGO_NAME= 101 | API_MONGO_PORT= 102 | API_MONGO_URI= 103 | 104 | BREE_MONGO_USER= 105 | BREE_MONGO_PASS= 106 | BREE_MONGO_HOST= 107 | BREE_MONGO_NAME= 108 | BREE_MONGO_PORT= 109 | BREE_MONGO_URI= 110 | 111 | ########### 112 | ## redis ## 113 | ########### 114 | REDIS_PORT= 115 | REDIS_HOST= 116 | REDIS_PASSWORD= 117 | WEB_REDIS_PORT= 118 | WEB_REDIS_HOST= 119 | WEB_REDIS_PASSWORD= 120 | API_REDIS_PORT= 121 | API_REDIS_HOST= 122 | API_REDIS_PASSWORD= 123 | BREE_REDIS_PORT= 124 | BREE_REDIS_HOST= 125 | BREE_REDIS_PASSWORD= 126 | MANDARIN_REDIS_PORT= 127 | MANDARIN_REDIS_HOST= 128 | MANDARIN_REDIS_PASSWORD= 129 | 130 | ############# 131 | ## certbot ## 132 | ############# 133 | CERTBOT_WELL_KNOWN_NAME= 134 | CERTBOT_WELL_KNOWN_CONTENTS= 135 | 136 | ################# 137 | ## api secrets ## 138 | ################# 139 | API_SECRETS= 140 | JWT_SECRET= 141 | 142 | ##################### 143 | ## cache responses ## 144 | ##################### 145 | CACHE_RESPONSES= 146 | 147 | ##################### 148 | ## slack api token ## 149 | ##################### 150 | SLACK_API_TOKEN= 151 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | ################# 2 | ## api secrets ## 3 | ################# 4 | JWT_SECRET=secret 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: niftylettuce 4 | patreon: niftylettuce 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Something is not working as it should 4 | title: "[fix] DESCRIPTIVE TITLE" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the bug 11 | 12 | - Node.js version: 13 | - OS & version: 14 | 15 | 16 | 17 | #### Actual behavior 18 | 19 | ... 20 | 21 | #### Expected behavior 22 | 23 | ... 24 | 25 | #### Code to reproduce 26 | 27 | ```js 28 | ... 29 | ``` 30 | 31 | 38 | 39 | #### Checklist 40 | 41 | - [ ] I have read the documentation. 42 | - [ ] I have tried my code with the latest version of Node.js and @breejs/api. 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "⭐ Feature request" 3 | about: Suggest an idea for Bree 4 | title: "[feat] DESCRIPTIVE TITLE" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### What problem are you trying to solve? 11 | 12 | ... 13 | 14 | #### Describe the feature 15 | 16 | ... 17 | 18 | 19 | 20 | #### Checklist 21 | 22 | - [ ] I have read the documentation and made sure this feature doesn't already exist. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ Question" 3 | about: Something is unclear or needs to be discussed 4 | title: "[discussion] DESCRIPTIVE TITLE" 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### What would you like to discuss? 11 | 12 | ... 13 | 14 | #### Checklist 15 | 16 | - [ ] I have read the documentation. 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test-coverage: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu-latest 13 | node_version: 14 | - 14 15 | - 16 16 | - 18 17 | 18 | name: Node ${{ matrix.node_version }} on ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Checkout repo 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup node 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node_version }} 28 | 29 | - name: Install yarn 30 | run: npm install -g yarn 31 | 32 | - name: Setup redis 33 | uses: zhulik/redis-action@1.1.0 34 | 35 | - name: Install dependencies 36 | run: yarn --frozen-lockfile 37 | 38 | - name: Run tests 39 | run: yarn test-coverage 40 | env: 41 | API_HOST: 0.0.0.0 42 | 43 | - name: Uninstall yarn 44 | if: always() 45 | run: npm uninstall -g yarn 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .idea 4 | node_modules 5 | coverage 6 | .nyc_output 7 | *.awspublish* 8 | *.env 9 | *.pem 10 | build 11 | temp.md 12 | !test/.env 13 | !.gulpfile.env 14 | *.lcov 15 | .base64-cache 16 | .vscode 17 | package-lock.json 18 | *-ar.md 19 | *-AR.md 20 | *-cs.md 21 | *-CS.md 22 | *-da.md 23 | *-DA.md 24 | *-de.md 25 | *-DE.md 26 | *-en.md 27 | *-EN.md 28 | *-es.md 29 | *-ES.md 30 | *-fi.md 31 | *-FI.md 32 | *-fr.md 33 | *-FR.md 34 | *-he.md 35 | *-HE.md 36 | *-hu.md 37 | *-HU.md 38 | *-id.md 39 | *-ID.md 40 | *-it.md 41 | *-IT.md 42 | *-ja.md 43 | *-JA.md 44 | *-ko.md 45 | *-KO.md 46 | *-nl.md 47 | *-NL.md 48 | *-no.md 49 | *-NO.md 50 | *-pl.md 51 | *-PL.md 52 | *-pt.md 53 | *-PT.md 54 | *-ru.md 55 | *-RU.md 56 | *-sv.md 57 | *-SV.md 58 | *-th.md 59 | *-TH.md 60 | *-tr.md 61 | *-TR.md 62 | *-uk.md 63 | *-UK.md 64 | *-vi.md 65 | *-VI.md 66 | *-zh.md 67 | *-ZH.md 68 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.md,!test/snapshots/**/*.md,!test/**/snapshots/**/*.md,!locales/README.md': [ 3 | (filenames) => filenames.map((filename) => `remark ${filename} -qfo`) 4 | ], 5 | 'package.json': ['fixpack'], 6 | '*.js': ['xo --fix'] 7 | }; 8 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | test/snapshots/**/*.md 2 | test/**/snapshots/**/*.md 3 | locales/README.md 4 | *-*.md 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All Rights Reserved. 2 | 3 | Copyright (c) 2021 shadowgate15 (https://github.com/shadowgate15) 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @breejs/api 2 | 3 | [![build status](https://github.com/breejs/ts-worker/actions/workflows/ci.yml/badge.svg)](https://github.com/breejs/ts-worker/actions/workflows/ci.yml) 4 | [![code coverage](https://img.shields.io/codecov/c/github/breejs/api.svg)](https://codecov.io/gh/breejs/api) 5 | [![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 6 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 7 | [![made with lad](https://img.shields.io/badge/made_with-lad-95CC28.svg)](https://lad.js.org) 8 | 9 | > An API for [Bree][]. 10 | 11 | ## Table of Contents 12 | 13 | 14 | ## Install 15 | 16 | [npm][]: 17 | 18 | ```sh 19 | npm install @breejs/api 20 | ``` 21 | 22 | [yarn][]: 23 | 24 | ```sh 25 | yarn add @breejs/api 26 | ``` 27 | 28 | ## Usage 29 | 30 | Extend bree with the plugin: 31 | 32 | ```js 33 | Bree.extend(require('@breejs/api').plugin); 34 | 35 | const bree = new Bree(config); 36 | ``` 37 | 38 | The API will start automatically when the Bree constructor is called. 39 | 40 | ## Options 41 | 42 | | Option | Type | Description | 43 | | :------: | :------: | ---------------------------------------------------------------------------------------------- | 44 | | port | Number | The port the API will listen on. Default: `62893` | 45 | | jwt | Object | Configurations for JWT. Only option is `secret` which will be the secret used to verify JWT. | 46 | | sse | Object | Configurations for SSE. See [koa-sse][] for list of options. | 47 | 48 | ## API 49 | 50 | Check out the [API Docs](https://documenter.getpostman.com/view/17142435/TzzDLbNG). 51 | 52 | ## Contributors 53 | 54 | 55 | ## License 56 | 57 | [MIT](LICENSE) © [Nick Baugh](http://niftylettuce.com/) 58 | 59 | ## 60 | 61 | [npm]: https://www.npmjs.com/ 62 | 63 | [yarn]: https://yarnpkg.com/ 64 | 65 | [Bree]: https://jobscheduler.net/#/ 66 | 67 | [koa-sse]: https://github.com/yklykl530/koa-sse 68 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unassigned-import 2 | require('./config/env'); 3 | 4 | const process = require('process'); 5 | 6 | const API = require('@ladjs/api'); 7 | const Graceful = require('@ladjs/graceful'); 8 | const ip = require('ip'); 9 | 10 | const logger = require('./helpers/logger'); 11 | const apiConfig = require('./config/api'); 12 | 13 | const api = new API(apiConfig()); 14 | 15 | if (!module.parent) { 16 | const graceful = new Graceful({ 17 | servers: [api], 18 | logger 19 | }); 20 | graceful.listen(); 21 | 22 | (async () => { 23 | try { 24 | await api.listen(api.config.port); 25 | if (process.send) process.send('ready'); 26 | const { port } = api.server.address(); 27 | logger.info( 28 | `Bree API server listening on ${port} (LAN: ${ip.address()}:${port})` 29 | ); 30 | } catch (error) { 31 | logger.error(error); 32 | // eslint-disable-next-line unicorn/no-process-exit 33 | process.exit(1); 34 | } 35 | })(); 36 | } 37 | 38 | module.exports = api; 39 | -------------------------------------------------------------------------------- /app/controllers/api/index.js: -------------------------------------------------------------------------------- 1 | const v1 = require('./v1'); 2 | 3 | module.exports = { v1 }; 4 | -------------------------------------------------------------------------------- /app/controllers/api/v1/config.js: -------------------------------------------------------------------------------- 1 | async function get(ctx) { 2 | ctx.body = ctx.bree.config; 3 | } 4 | 5 | module.exports = { get }; 6 | -------------------------------------------------------------------------------- /app/controllers/api/v1/control.js: -------------------------------------------------------------------------------- 1 | const Boom = require('@hapi/boom'); 2 | 3 | async function checkJobName(ctx, next) { 4 | const { jobName } = ctx.params; 5 | 6 | if (jobName && !ctx.bree.config.jobs.some((j) => j.name === jobName)) 7 | return ctx.throw(Boom.badData('Job name does not exist')); 8 | 9 | return next(); 10 | } 11 | 12 | async function addJobNameToQuery(ctx, next) { 13 | const { jobName } = ctx.params; 14 | 15 | ctx.query = { name: jobName }; 16 | 17 | return next(); 18 | } 19 | 20 | async function start(ctx, next) { 21 | const { jobName } = ctx.params; 22 | 23 | await ctx.bree.start(jobName); 24 | 25 | ctx.body = {}; 26 | 27 | return next(); 28 | } 29 | 30 | async function stop(ctx, next) { 31 | const { jobName } = ctx.params; 32 | 33 | await ctx.bree.stop(jobName); 34 | 35 | ctx.body = {}; 36 | 37 | return next(); 38 | } 39 | 40 | async function run(ctx, next) { 41 | const { jobName } = ctx.params; 42 | 43 | await ctx.bree.run(jobName); 44 | 45 | ctx.body = {}; 46 | 47 | return next(); 48 | } 49 | 50 | async function restart(ctx, next) { 51 | const { jobName } = ctx.params; 52 | 53 | await ctx.bree.stop(jobName); 54 | await ctx.bree.start(jobName); 55 | 56 | ctx.body = {}; 57 | 58 | return next(); 59 | } 60 | 61 | module.exports = { checkJobName, addJobNameToQuery, start, stop, run, restart }; 62 | -------------------------------------------------------------------------------- /app/controllers/api/v1/index.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | const jobs = require('./jobs'); 3 | const control = require('./control'); 4 | const sse = require('./sse'); 5 | 6 | const test = (ctx) => { 7 | ctx.body = { breeExists: Boolean(ctx.bree) }; 8 | }; 9 | 10 | module.exports = { config, test, jobs, control, sse }; 11 | -------------------------------------------------------------------------------- /app/controllers/api/v1/jobs.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Boom = require('@hapi/boom'); 3 | 4 | async function get(ctx) { 5 | const { bree } = ctx; 6 | 7 | let body = bree.config.jobs; 8 | 9 | body = body.map((j) => { 10 | j.status = 'done'; 11 | 12 | if (bree.workers.has(j.name)) j.status = 'active'; 13 | else if (bree.timeouts.has(j.name)) j.status = 'delayed'; 14 | else if (bree.intervals.has(j.name)) j.status = 'waiting'; 15 | 16 | return j; 17 | }); 18 | 19 | body = _.filter(body, ctx.query); 20 | 21 | ctx.body = body; 22 | } 23 | 24 | async function add(ctx) { 25 | const { bree } = ctx; 26 | const { body } = ctx.request; 27 | 28 | if (body.copy) { 29 | if (!_.isArray(body.jobs)) body.jobs = [body.jobs]; 30 | 31 | const newJobs = []; 32 | 33 | for (const job of body.jobs) { 34 | const origJob = bree.config.jobs.find((j) => j.name === job.name); 35 | 36 | if (!origJob) 37 | return ctx.throw(Boom.badData('Name does not exist in jobs')); 38 | 39 | const newJob = _.clone(origJob); 40 | newJob.name = origJob.name + '(1)'; 41 | 42 | if (origJob.name.endsWith(')')) { 43 | newJob.name = origJob.name.replace( 44 | /(^.*)(\((\d+)\))(?=$)/i, 45 | (_match, subName, _p2, num) => 46 | `${subName}(${Number.parseInt(num, 10) + 1})` 47 | ); 48 | } 49 | 50 | newJobs.push(newJob); 51 | } 52 | 53 | body.jobs = newJobs; 54 | } 55 | 56 | let jobs; 57 | 58 | try { 59 | jobs = await bree.add(body.jobs); 60 | } catch (err) { 61 | return ctx.throw(Boom.badData(err)); 62 | } 63 | 64 | if (body.start) { 65 | await Promise.all(jobs.map((j) => bree.start(j.name))); 66 | } 67 | 68 | ctx.body = { jobs }; 69 | } 70 | 71 | module.exports = { get, add }; 72 | -------------------------------------------------------------------------------- /app/controllers/api/v1/sse.js: -------------------------------------------------------------------------------- 1 | async function connect(ctx) { 2 | if (ctx.sse) { 3 | // likely not the best way to do this 4 | // TODO: fork koa-sse and move this into the ping interval 5 | // runs every 60s 6 | const interval = setInterval(() => { 7 | const connected = ctx.sse.send({ 8 | event: 'status', 9 | data: isActive(ctx) 10 | }); 11 | 12 | console.log('connected'); 13 | 14 | // clear the interval if the client is disconnected 15 | if (!connected) { 16 | clearInterval(interval); 17 | } 18 | }, 60_000); 19 | ctx.sse.send({ 20 | event: 'status', 21 | data: isActive(ctx) 22 | }); 23 | 24 | // send bree events over sse 25 | for (const event of ['worker created', 'worker deleted']) { 26 | ctx.bree.on(event, (name) => { 27 | ctx.sse.send({ event, data: name }); 28 | }); 29 | } 30 | 31 | ctx.bree.on('worker message', (data) => { 32 | ctx.sse.send({ event: 'worker message', data: JSON.stringify(data) }); 33 | }); 34 | 35 | ctx.bree.on('worker error', (data) => { 36 | ctx.sse.send({ event: 'worker error', data: JSON.stringify(data) }); 37 | }); 38 | 39 | ctx.sse.on('close', () => { 40 | ctx.logger.error('SSE closed'); 41 | 42 | clearInterval(interval); 43 | }); 44 | } 45 | } 46 | 47 | function isActive(ctx) { 48 | return Boolean( 49 | ctx.bree.workers.size > 0 || 50 | ctx.bree.timeouts.size > 0 || 51 | ctx.bree.intervals.size > 0 52 | ); 53 | } 54 | 55 | module.exports = { connect }; 56 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | const api = require('./api'); 2 | 3 | module.exports = { api }; 4 | -------------------------------------------------------------------------------- /config/api.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('@ladjs/shared-config'); 2 | const jwt = require('koa-jwt'); 3 | const sse = require('koa-sse-stream'); 4 | 5 | const logger = require('../helpers/logger'); 6 | const routes = require('../routes'); 7 | const config = require('../config'); 8 | 9 | module.exports = (opts = {}) => { 10 | const sseMiddleware = sse({ ...config.sse, ...opts.sse }); 11 | const jwtMiddleware = jwt({ 12 | ...config.jwt, 13 | ...opts.jwt, 14 | getToken(ctx, _) { 15 | // pull token off of url if it is the sse endpoint 16 | if (ctx.url.indexOf('/v1/sse') === 0) { 17 | const splitUrl = ctx.url.split('/'); 18 | 19 | if (splitUrl.length === 4) { 20 | return splitUrl[3]; 21 | } 22 | } 23 | 24 | return null; 25 | } 26 | }); 27 | 28 | return { 29 | ...sharedConfig('API'), 30 | port: config.port, 31 | ...opts, 32 | routes: routes.api, 33 | logger, 34 | hookBeforeRoutes(app) { 35 | app.use((ctx, next) => { 36 | // return early if jwt is set to false 37 | if (!opts.jwt && typeof opts.jwt === 'boolean') { 38 | return next(); 39 | } 40 | 41 | return jwtMiddleware(ctx, next); 42 | }); 43 | 44 | app.use((ctx, next) => { 45 | // only do this on sse route 46 | if (ctx.url.indexOf('/v1/sse') === 0) { 47 | return sseMiddleware(ctx, next); 48 | } 49 | 50 | return next(); 51 | }); 52 | } 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const path = require('path'); 3 | 4 | const test = process.env.NODE_ENV === 'test'; 5 | 6 | // note that we had to specify absolute paths here bc 7 | // otherwise tests run from the root folder wont work 8 | const env = require('@ladjs/env')({ 9 | path: path.join(__dirname, '..', test ? '.env.test' : '.env'), 10 | defaults: path.join(__dirname, '..', '.env.defaults'), 11 | schema: path.join(__dirname, '..', '.env.schema') 12 | }); 13 | 14 | module.exports = env; 15 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const pkg = require('../package'); 4 | const env = require('./env'); 5 | const loggerConfig = require('./logger'); 6 | 7 | const config = { 8 | // package.json 9 | pkg, 10 | 11 | // server 12 | env: env.NODE_ENV, 13 | urls: { 14 | api: env.API_URL 15 | }, 16 | 17 | // app 18 | supportRequestMaxLength: env.SUPPORT_REQUEST_MAX_LENGTH, 19 | logger: loggerConfig, 20 | appName: env.APP_NAME, 21 | appColor: env.APP_COLOR, 22 | twitter: env.TWITTER, 23 | port: 62_893, 24 | 25 | // build directory 26 | buildBase: 'build', 27 | 28 | // jwt options 29 | jwt: { 30 | secret: env.JWT_SECRET 31 | }, 32 | 33 | // sse options 34 | sse: { 35 | maxClients: 10_000, 36 | pingInterval: 60_000 37 | }, 38 | 39 | // store IP address 40 | // 41 | storeIPAddress: { 42 | ip: 'ip', 43 | lastIps: 'last_ips' 44 | }, 45 | 46 | // field name for a user's last locale 47 | // (this gets re-used by email-templates and @ladjs/i18n; see below) 48 | lastLocaleField: 'last_locale' 49 | }; 50 | 51 | // set build dir based off build base dir name 52 | config.buildDir = path.join(__dirname, '..', config.buildBase); 53 | 54 | module.exports = config; 55 | -------------------------------------------------------------------------------- /config/logger.js: -------------------------------------------------------------------------------- 1 | const Axe = require('axe'); 2 | const { WebClient } = require('@slack/web-api'); 3 | const signale = require('signale'); 4 | const pino = require('pino')({ 5 | customLevels: { 6 | log: 30 7 | }, 8 | hooks: { 9 | // 10 | logMethod(inputArgs, method) { 11 | return method.call(this, { 12 | // 13 | // message: inputArgs[0], 14 | msg: inputArgs[0], 15 | meta: inputArgs[1] 16 | }); 17 | } 18 | } 19 | }); 20 | 21 | const env = require('./env'); 22 | 23 | const isProduction = env.NODE_ENV === 'production'; 24 | 25 | const config = { 26 | logger: isProduction ? pino : signale, 27 | level: isProduction ? 'warn' : 'debug', 28 | showStack: env.SHOW_STACK, 29 | showMeta: env.SHOW_META, 30 | capture: false, 31 | name: env.APP_NAME 32 | }; 33 | 34 | // create our application logger that uses a custom callback function 35 | const axe = new Axe({ ...config }); 36 | 37 | if (env.SLACK_API_TOKEN) { 38 | // custom logger for Slack that inherits our Axe config 39 | // (with the exception of a `callback` function for logging to Slack) 40 | const slackLogger = new Axe(config); 41 | 42 | // create an instance of the Slack Web Client API for posting messages 43 | const web = new WebClient(env.SLACK_API_TOKEN, { 44 | // 45 | logger: slackLogger, 46 | logLevel: config.level 47 | }); 48 | 49 | axe.setCallback(async (level, message, meta) => { 50 | try { 51 | // if meta did not have `slack: true` 52 | // and it was not an error then return early 53 | if (!meta.slack && !['error', 'fatal'].includes(level)) return; 54 | 55 | // otherwise post a message to the slack channel 56 | const fields = [ 57 | { 58 | title: 'Level', 59 | value: meta.level, 60 | short: true 61 | }, 62 | { 63 | title: 'Environment', 64 | value: meta.app.environment, 65 | short: true 66 | }, 67 | { 68 | title: 'Hostname', 69 | value: meta.app.hostname, 70 | short: true 71 | }, 72 | { 73 | title: 'Hash', 74 | value: meta.app.hash, 75 | short: true 76 | } 77 | ]; 78 | 79 | if (meta.user && meta.user.email) 80 | fields.push({ 81 | title: 'Email', 82 | value: meta.user.email, 83 | short: true 84 | }); 85 | 86 | const result = await web.chat.postMessage({ 87 | channel: 'logs', 88 | username: 'Cabin', 89 | icon_emoji: ':evergreen_tree:', 90 | attachments: [ 91 | { 92 | title: meta.err && meta.err.message ? meta.err.message : message, 93 | color: 'danger', 94 | text: meta.err && meta.err.stack ? meta.err.stack : null, 95 | fields 96 | } 97 | ] 98 | }); 99 | 100 | // finally log the result from slack 101 | axe.info('web.chat.postMessage', { result, callback: false }); 102 | } catch (err) { 103 | axe.error(err, { callback: false }); 104 | } 105 | }); 106 | } 107 | 108 | module.exports = { 109 | logger: axe, 110 | capture: false 111 | }; 112 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unassigned-import 2 | require('./config/env'); 3 | 4 | const del = require('del'); 5 | const gulpRemark = require('gulp-remark'); 6 | const gulpXo = require('gulp-xo'); 7 | const lr = require('gulp-livereload'); 8 | const { lastRun, watch, series, parallel, src, dest } = require('gulp'); 9 | 10 | const config = require('./config'); 11 | 12 | // const PROD = config.env === 'production'; 13 | // const DEV = config.env === 'development'; 14 | const TEST = config.env === 'test'; 15 | 16 | function xo() { 17 | return src('.', { since: lastRun(xo) }) 18 | .pipe(gulpXo({ quiet: true, fix: true })) 19 | .pipe(gulpXo.format()) 20 | .pipe(gulpXo.failAfterError()); 21 | } 22 | 23 | function remark() { 24 | return src('.', { since: lastRun(remark) }) 25 | .pipe( 26 | gulpRemark({ 27 | quiet: true, 28 | frail: true 29 | }) 30 | ) 31 | .pipe(dest('.')); 32 | } 33 | 34 | function clean() { 35 | return del([config.buildBase]); 36 | } 37 | 38 | const build = series(clean, parallel(...(TEST ? [] : [xo, remark]))); 39 | 40 | module.exports = { 41 | clean, 42 | build, 43 | watch() { 44 | lr.listen(config.livereload); 45 | watch(['**/*.js'], xo); 46 | }, 47 | xo, 48 | remark 49 | }; 50 | 51 | exports.default = build; 52 | -------------------------------------------------------------------------------- /helpers/logger.js: -------------------------------------------------------------------------------- 1 | const Axe = require('axe'); 2 | 3 | const loggerConfig = require('../config/logger'); 4 | 5 | const logger = new Axe(loggerConfig); 6 | 7 | module.exports = logger; 8 | -------------------------------------------------------------------------------- /helpers/markdown.js: -------------------------------------------------------------------------------- 1 | const markdownIt = require('markdown-it'); 2 | const markdownItEmoji = require('markdown-it-emoji'); 3 | const markdownItGitHubHeadings = require('markdown-it-github-headings'); 4 | const markdownItHighlightJS = require('markdown-it-highlightjs'); 5 | const markdownItTaskCheckbox = require('markdown-it-task-checkbox'); 6 | 7 | // 8 | // 9 | // 10 | // 11 | const markdown = markdownIt({ 12 | html: true, 13 | linkify: true 14 | }); 15 | markdown.use(markdownItHighlightJS); 16 | markdown.use(markdownItTaskCheckbox); 17 | markdown.use(markdownItEmoji); 18 | markdown.use(markdownItGitHubHeadings, { 19 | prefix: '' 20 | }); 21 | 22 | module.exports = markdown; 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const API = require('@ladjs/api'); 2 | 3 | const api = require('./api'); 4 | const apiConfig = require('./config/api'); 5 | 6 | const config = require('./config'); 7 | 8 | function plugin(opts, Bree) { 9 | opts = { 10 | port: config.port, 11 | jwt: config.jwt, 12 | sse: config.sse, 13 | ...opts 14 | }; 15 | 16 | const api = new API(apiConfig(opts)); 17 | 18 | const oldInit = Bree.prototype.init; 19 | 20 | Bree.prototype.init = async function () { 21 | // hook error handler and message handler 22 | const oldErrorHandler = this.config.errorHandler; 23 | const oldWorkerMessageHandler = this.config.workerMessageHandler; 24 | 25 | this.config.errorHandler = function (error, data) { 26 | if (oldErrorHandler) { 27 | oldErrorHandler.call(this, error, data); 28 | } 29 | 30 | this.emit('worker error', { 31 | error, 32 | name: data?.name, 33 | data: data ? JSON.stringify(data) : undefined 34 | }); 35 | }; 36 | 37 | this.config.errorHandler = this.config.errorHandler.bind(this); 38 | 39 | this.config.workerMessageHandler = function (data) { 40 | if (oldWorkerMessageHandler) { 41 | oldWorkerMessageHandler.call(this, data); 42 | } 43 | 44 | this.emit('worker message', data); 45 | }; 46 | 47 | this.config.workerMessageHandler = 48 | this.config.workerMessageHandler.bind(this); 49 | 50 | await oldInit.call(this); 51 | 52 | // assign bree to the context 53 | api.app.context.bree = this; 54 | 55 | this.api = api; 56 | 57 | this.api.listen(opts.port).catch((err) => { 58 | throw err; 59 | }); 60 | }; 61 | } 62 | 63 | module.exports = { 64 | api, 65 | plugin 66 | }; 67 | -------------------------------------------------------------------------------- /locales/ar.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/cs.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/da.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/fi.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/he.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/hu.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/id.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/no.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/pl.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/pt.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/ru.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/sv.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/th.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/tr.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/uk.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/vi.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /locales/zh.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["locales", "test", "build", "assets", "gulpfile.js"] 3 | } 4 | -------------------------------------------------------------------------------- /package-scripts.js: -------------------------------------------------------------------------------- 1 | const { series, concurrent } = require('nps-utils'); 2 | 3 | module.exports = { 4 | scripts: { 5 | all: series.nps('build', 'apps-and-watch'), 6 | appsAndWatch: concurrent.nps('apps', 'watch'), 7 | apps: concurrent.nps('api'), 8 | 9 | api: 'nodemon api.js', 10 | 11 | watch: 'gulp watch', 12 | clean: 'gulp clean', 13 | build: 'gulp build', 14 | publishAssets: 'gulp publish', 15 | 16 | lintJs: 'gulp xo', 17 | lintMd: 'gulp remark', 18 | lint: concurrent.nps('lint-js', 'lint-md'), 19 | 20 | // 21 | pretest: concurrent.nps('lint', 'build'), 22 | 23 | test: 'ava', 24 | testCoverage: series('nps pretest', 'nyc ava'), 25 | testUpdateSnapshots: series('nps pretest', 'ava --update-snapshots'), 26 | 27 | coverage: 'nyc report --reporter=text-lcov > coverage.lcov && codecov' 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@breejs/api", 3 | "description": "An API for Bree.", 4 | "version": "2.1.0", 5 | "author": "shadowgate15 (https://github.com/shadowgate15)", 6 | "ava": { 7 | "files": [ 8 | "test/*.js", 9 | "test/**/*.js", 10 | "!test/jobs", 11 | "!test/utils.js" 12 | ] 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/breejs/api/issues", 16 | "email": "taylorschley@me.com" 17 | }, 18 | "commitlint": { 19 | "extends": [ 20 | "@commitlint/config-conventional" 21 | ] 22 | }, 23 | "dependencies": { 24 | "@hapi/boom": "^10.0.0", 25 | "@koa/router": "^10.0.0", 26 | "@ladjs/api": "^9.0.2", 27 | "@ladjs/env": "^3.0.0", 28 | "@ladjs/graceful": "^2.0.1", 29 | "@ladjs/shared-config": "^7.0.3", 30 | "@slack/web-api": "^6.0.0", 31 | "axe": "^8.0.0", 32 | "crypto-random-string": "^5.0.0", 33 | "del": "^6.0.0", 34 | "humanize-string": "^3.0.0", 35 | "ip": "^1.1.5", 36 | "koa-jwt": "^4.0.1", 37 | "koa-sse-stream": "^0.2.0", 38 | "lodash": "^4.17.20", 39 | "markdown-it": "^13.0.1", 40 | "markdown-it-emoji": "^2.0.0", 41 | "markdown-it-github-headings": "^2.0.0", 42 | "markdown-it-highlightjs": "^4.0.1", 43 | "markdown-it-task-checkbox": "^1.0.6", 44 | "pino": "^8.0.0", 45 | "signale": "^1.4.0" 46 | }, 47 | "devDependencies": { 48 | "@commitlint/cli": "^17.0.2", 49 | "@commitlint/config-conventional": "^17.0.2", 50 | "ava": "^4.3.0", 51 | "bree": "^9.0.0", 52 | "codecov": "^3.8.1", 53 | "delay": "^5.0.0", 54 | "eslint": "^8.17.0", 55 | "eslint-config-xo-lass": "^1.0.5", 56 | "eslint-formatter-pretty": "^4.0.0", 57 | "eslint-plugin-compat": "^4.0.2", 58 | "eslint-plugin-no-smart-quotes": "^1.1.0", 59 | "eventsource": "^2.0.2", 60 | "fixpack": "^4.0.0", 61 | "get-port": "^5.1.1", 62 | "gulp": "^4.0.2", 63 | "gulp-cli": "^2.3.0", 64 | "gulp-livereload": "^4.0.2", 65 | "gulp-remark": "^9.0.0", 66 | "gulp-xo": "^0.25.0", 67 | "husky": "^8.0.1", 68 | "jsonwebtoken": "^8.5.1", 69 | "lint-staged": "13.0.0", 70 | "make-dir": "^3.1.0", 71 | "ms": "^2.1.3", 72 | "nodemon": "^2.0.7", 73 | "npm-run-all": "^4.1.5", 74 | "nps": "^5.10.0", 75 | "nps-utils": "^1.7.0", 76 | "nyc": "^15.1.0", 77 | "parse-git-config": "^3.0.0", 78 | "rc": "^1.2.8", 79 | "remark-cli": "^9.0.0", 80 | "remark-preset-github": "^4.0.1", 81 | "supertest": "^6.1.3", 82 | "xo": "^0.49.0" 83 | }, 84 | "engines": { 85 | "node": ">=14" 86 | }, 87 | "homepage": "https://github.com/breejs/api", 88 | "husky": { 89 | "hooks": { 90 | "pre-commit": "lint-staged", 91 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 92 | } 93 | }, 94 | "keywords": [ 95 | "api", 96 | "bree", 97 | "lad", 98 | "lass" 99 | ], 100 | "license": "Unlicensed", 101 | "main": "index.js", 102 | "nodemonConfig": { 103 | "ignore": [ 104 | "build/**", 105 | "assets/**", 106 | "test/**" 107 | ] 108 | }, 109 | "peerDependencies": { 110 | "bree": "~9.0" 111 | }, 112 | "prettier": { 113 | "singleQuote": true, 114 | "bracketSpacing": true, 115 | "trailingComma": "none" 116 | }, 117 | "publishConfig": { 118 | "access": "public" 119 | }, 120 | "remarkConfig": { 121 | "plugins": [ 122 | "preset-github" 123 | ] 124 | }, 125 | "repository": { 126 | "type": "git", 127 | "url": "https://github.com/breejs/api" 128 | }, 129 | "scripts": { 130 | "build": "nps build", 131 | "coverage": "nps coverage", 132 | "lint": "nps lint", 133 | "prepare": "husky install", 134 | "pretest": "nps pretest", 135 | "start": "nps", 136 | "test": "nps test", 137 | "test-coverage": "nps test-coverage", 138 | "watch": "nps watch" 139 | }, 140 | "xo": { 141 | "prettier": true, 142 | "space": true, 143 | "extends": [ 144 | "xo-lass" 145 | ], 146 | "overrides": [ 147 | { 148 | "files": [ 149 | "assets/js/*.js", 150 | "assets/js/**/*.js" 151 | ], 152 | "envs": [ 153 | "browser" 154 | ], 155 | "plugins": [ 156 | "compat", 157 | "no-smart-quotes" 158 | ], 159 | "rules": { 160 | "compat/compat": "error", 161 | "no-smart-quotes/no-smart-quotes": "error" 162 | } 163 | } 164 | ], 165 | "rules": { 166 | "ava/assertion-arguments": "off", 167 | "unicorn/prevent-abbreviations": "off", 168 | "unicorn/no-fn-reference-in-iterator": "off", 169 | "import/extensions": "off", 170 | "capitalized-comments": "off", 171 | "unicorn/catch-error-name": "off" 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /routes/api/index.js: -------------------------------------------------------------------------------- 1 | const Router = require('@koa/router'); 2 | 3 | const v1 = require('./v1'); 4 | 5 | const router = new Router(); 6 | router.use(v1.routes()); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /routes/api/v1/index.js: -------------------------------------------------------------------------------- 1 | const Router = require('@koa/router'); 2 | 3 | const api = require('../../../app/controllers/api'); 4 | const config = require('../../../config'); 5 | 6 | const router = new Router({ 7 | prefix: '/v1' 8 | }); 9 | 10 | if (config.env === 'test') { 11 | router.get('/test', api.v1.test); 12 | } 13 | 14 | router.get('/config', api.v1.config.get); 15 | 16 | router.get('/jobs', api.v1.jobs.get); 17 | router.post('/jobs', api.v1.jobs.add); 18 | 19 | router.use(api.v1.control.checkJobName); 20 | 21 | router.post('/start', api.v1.control.start); 22 | router.post( 23 | '/start/:jobName', 24 | api.v1.control.start, 25 | api.v1.control.addJobNameToQuery, 26 | api.v1.jobs.get 27 | ); 28 | 29 | router.post('/stop', api.v1.control.stop); 30 | router.post( 31 | '/stop/:jobName', 32 | api.v1.control.stop, 33 | api.v1.control.addJobNameToQuery, 34 | api.v1.jobs.get 35 | ); 36 | 37 | router.post( 38 | '/run/:jobName', 39 | api.v1.control.run, 40 | api.v1.control.addJobNameToQuery, 41 | api.v1.jobs.get 42 | ); 43 | 44 | router.post('/restart', api.v1.control.restart); 45 | router.post( 46 | '/restart/:jobName', 47 | api.v1.control.restart, 48 | api.v1.control.addJobNameToQuery, 49 | api.v1.jobs.get 50 | ); 51 | 52 | router.get('/sse', api.v1.sse.connect); 53 | router.get( 54 | '/sse/:token', 55 | api.v1.sse.connect, 56 | api.v1.control.addJobNameToQuery, 57 | api.v1.jobs.get 58 | ); 59 | 60 | module.exports = router; 61 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const api = require('./api'); 2 | 3 | module.exports = { 4 | api 5 | }; 6 | -------------------------------------------------------------------------------- /test/api/v1/config.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const jwt = require('jsonwebtoken'); 3 | const _ = require('lodash'); 4 | 5 | const config = require('../../../config'); 6 | 7 | const utils = require('../../utils'); 8 | 9 | const rootUrl = '/v1/config'; 10 | 11 | test.before(async (t) => { 12 | await utils.setupApiServer(t); 13 | t.context.token = jwt.sign({}, config.jwt.secret); 14 | 15 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 16 | }); 17 | 18 | test('GET > successfully', async (t) => { 19 | const { api, bree } = t.context; 20 | 21 | const res = await api.get(rootUrl); 22 | 23 | t.is(res.status, 200); 24 | t.like( 25 | res.body, 26 | _.omit(bree.config, ['logger', 'workerMessageHandler', 'errorHandler']) 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /test/api/v1/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | const config = require('../../../config'); 5 | 6 | const utils = require('../../utils'); 7 | 8 | test.before(async (t) => { 9 | await utils.setupApiServer(t); 10 | t.context.token = jwt.sign({}, config.jwt.secret); 11 | }); 12 | 13 | test('fails when no creds are presented', async (t) => { 14 | const { api } = t.context; 15 | const res = await api.get('/v1/test'); 16 | 17 | t.is(res.status, 401); 18 | }); 19 | 20 | test('works with jwt auth', async (t) => { 21 | const { api, token } = t.context; 22 | const res = await api.get('/v1/test').auth(token, { type: 'bearer' }); 23 | 24 | t.is(res.status, 200); 25 | }); 26 | 27 | test('has bree in context', async (t) => { 28 | const { api, token } = t.context; 29 | const res = await api.get('/v1/test').auth(token, { type: 'bearer' }); 30 | 31 | t.is(res.status, 200); 32 | t.is(true, Boolean(res.body.breeExists)); 33 | }); 34 | -------------------------------------------------------------------------------- /test/api/v1/jobs/get.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { once } = require('events'); 3 | const test = require('ava'); 4 | const jwt = require('jsonwebtoken'); 5 | const delay = require('delay'); 6 | 7 | const config = require('../../../../config'); 8 | 9 | const utils = require('../../../utils'); 10 | 11 | const rootUrl = '/v1/jobs'; 12 | 13 | test.before(async (t) => { 14 | await utils.setupApiServer(t, { 15 | jobs: [ 16 | { name: 'done', path: path.join(utils.root, 'basic.js') }, 17 | { 18 | name: 'delayed', 19 | path: path.join(utils.root, 'basic.js'), 20 | timeout: 1000 21 | }, 22 | { 23 | name: 'waiting', 24 | path: path.join(utils.root, 'basic.js'), 25 | interval: 1000 26 | }, 27 | { 28 | name: 'active', 29 | path: path.join(utils.root, 'long.js') 30 | } 31 | ] 32 | }); 33 | t.context.token = jwt.sign({}, config.jwt.secret); 34 | 35 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 36 | 37 | await t.context.bree.start(); 38 | }); 39 | 40 | test.serial('successfully', async (t) => { 41 | const { api, bree } = t.context; 42 | 43 | // wait until the first worker finishes so our expect is correctly timed 44 | await once(bree, 'worker deleted'); 45 | 46 | const res = await api.get(rootUrl); 47 | 48 | t.is(res.status, 200); 49 | 50 | t.deepEqual( 51 | res.body, 52 | bree.config.jobs.map((j) => { 53 | j.status = j.name; 54 | 55 | return j; 56 | }) 57 | ); 58 | }); 59 | 60 | test('successfully filter by name', async (t) => { 61 | const { api, bree } = t.context; 62 | 63 | const res = await api.get(`${rootUrl}?name=done`); 64 | 65 | t.is(res.status, 200); 66 | 67 | t.deepEqual( 68 | res.body, 69 | bree.config.jobs.filter((j) => j.name === 'done') 70 | ); 71 | }); 72 | 73 | test.serial('successfully filter by status', async (t) => { 74 | const { api, bree } = t.context; 75 | 76 | await delay(200); 77 | 78 | const res = await api.get(`${rootUrl}?status=done`); 79 | 80 | t.is(res.status, 200); 81 | 82 | t.deepEqual( 83 | res.body, 84 | bree.config.jobs.filter((j) => j.name === 'done') 85 | ); 86 | }); 87 | -------------------------------------------------------------------------------- /test/api/v1/jobs/post.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('ava'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const config = require('../../../../config'); 6 | 7 | const utils = require('../../../utils'); 8 | 9 | const rootUrl = '/v1/jobs'; 10 | 11 | test.before(async (t) => { 12 | await utils.setupApiServer(t, { 13 | jobs: [{ name: 'done', path: path.join(utils.root, 'basic.js') }] 14 | }); 15 | t.context.token = jwt.sign({}, config.jwt.secret); 16 | 17 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 18 | 19 | await t.context.bree.start(); 20 | }); 21 | 22 | test('successfully add one job', async (t) => { 23 | const { bree, api } = t.context; 24 | 25 | const jobs = { 26 | name: 'successfulOne', 27 | path: path.join(utils.root, 'long.js') 28 | }; 29 | 30 | const res = await api.post(rootUrl).send({ 31 | jobs 32 | }); 33 | 34 | t.is(res.status, 200); 35 | 36 | t.truthy(bree.config.jobs.find((j) => j.name === 'successfulOne')); 37 | // ensure job hasn't been started 38 | t.falsy(bree.workers.has('successfulOne')); 39 | }); 40 | 41 | test('successfully add multiple jobs', async (t) => { 42 | const { bree, api } = t.context; 43 | 44 | const jobs = [ 45 | { 46 | name: 'array1', 47 | path: path.join(utils.root, 'long.js') 48 | }, 49 | { 50 | name: 'array2', 51 | path: path.join(utils.root, 'long.js') 52 | }, 53 | { 54 | name: 'array3', 55 | path: path.join(utils.root, 'long.js') 56 | } 57 | ]; 58 | 59 | const res = await api.post(rootUrl).send({ 60 | jobs 61 | }); 62 | 63 | t.is(res.status, 200); 64 | 65 | t.truthy(bree.config.jobs.find((j) => j.name === 'array1')); 66 | t.truthy(bree.config.jobs.find((j) => j.name === 'array2')); 67 | t.truthy(bree.config.jobs.find((j) => j.name === 'array3')); 68 | // ensure job hasn't been started 69 | t.falsy(bree.workers.has('array1')); 70 | t.falsy(bree.workers.has('array2')); 71 | t.falsy(bree.workers.has('array3')); 72 | }); 73 | 74 | test('fails if data is bad', async (t) => { 75 | const { api } = t.context; 76 | 77 | const jobs = {}; 78 | 79 | const res = await api.post(rootUrl).send({ 80 | jobs 81 | }); 82 | 83 | t.is(res.status, 422); 84 | }); 85 | 86 | test('successfully auto start job', async (t) => { 87 | const { bree, api } = t.context; 88 | 89 | const jobs = { 90 | name: 'auto-start', 91 | path: path.join(utils.root, 'long.js') 92 | }; 93 | 94 | const res = await api.post(rootUrl).send({ 95 | jobs, 96 | start: true 97 | }); 98 | 99 | t.is(res.status, 200); 100 | 101 | t.truthy(bree.config.jobs.find((j) => j.name === 'auto-start')); 102 | // ensure job hasn't been started 103 | t.truthy(bree.workers.has('auto-start')); 104 | }); 105 | 106 | test('successfully duplicate job', async (t) => { 107 | const { bree, api } = t.context; 108 | 109 | const jobs = { 110 | name: 'orig(1)', 111 | path: path.join(utils.root, 'long.js') 112 | }; 113 | 114 | await api.post(rootUrl).send({ jobs }); 115 | 116 | const res = await api 117 | .post(rootUrl) 118 | .send({ copy: true, jobs: { name: 'orig(1)' } }); 119 | 120 | t.is(res.status, 200); 121 | 122 | t.truthy(bree.config.jobs.find((j) => j.name === 'orig(2)')); 123 | 124 | t.falsy(bree.workers.has('org(2)')); 125 | }); 126 | -------------------------------------------------------------------------------- /test/api/v1/no-jwt.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const utils = require('../../utils'); 4 | 5 | test.before(async (t) => { 6 | await utils.setupApiServer(t, {}, { jwt: false }); 7 | }); 8 | 9 | test('works when no creds are presented', async (t) => { 10 | const { api } = t.context; 11 | const res = await api.get('/v1/test'); 12 | 13 | t.is(res.status, 200); 14 | }); 15 | -------------------------------------------------------------------------------- /test/api/v1/restart.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('ava'); 3 | const jwt = require('jsonwebtoken'); 4 | const delay = require('delay'); 5 | 6 | const config = require('../../../config'); 7 | 8 | const utils = require('../../utils'); 9 | 10 | const rootUrl = '/v1/restart'; 11 | 12 | test.before(async (t) => { 13 | await utils.setupApiServer(t, { 14 | jobs: [ 15 | { name: 'done', path: path.join(utils.root, 'basic.js') }, 16 | { 17 | name: 'delayed', 18 | path: path.join(utils.root, 'basic.js'), 19 | timeout: 1000 20 | }, 21 | { 22 | name: 'waiting', 23 | path: path.join(utils.root, 'basic.js'), 24 | interval: 1000 25 | }, 26 | { 27 | name: 'active', 28 | path: path.join(utils.root, 'long.js') 29 | } 30 | ] 31 | }); 32 | t.context.token = jwt.sign({}, config.jwt.secret); 33 | 34 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 35 | }); 36 | 37 | test.serial('successfully restart all jobs', async (t) => { 38 | const { bree, api } = t.context; 39 | 40 | const res = await api.post(rootUrl).send({}); 41 | 42 | t.is(res.status, 200); 43 | 44 | await delay(200); 45 | 46 | t.truthy(bree.workers.has('active')); 47 | t.truthy(bree.timeouts.has('delayed')); 48 | t.truthy(bree.intervals.has('waiting')); 49 | }); 50 | 51 | test.serial('successfully restart active job', async (t) => { 52 | const { bree, api } = t.context; 53 | 54 | const res = await api.post(`${rootUrl}/active`).send({}); 55 | 56 | t.is(res.status, 200); 57 | t.is(res.body.length, 1); 58 | t.like(res.body[0], { name: 'active' }); 59 | 60 | await delay(200); 61 | 62 | t.truthy(bree.workers.has('active')); 63 | }); 64 | -------------------------------------------------------------------------------- /test/api/v1/run.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('ava'); 3 | const jwt = require('jsonwebtoken'); 4 | const delay = require('delay'); 5 | 6 | const config = require('../../../config'); 7 | 8 | const utils = require('../../utils'); 9 | 10 | const rootUrl = '/v1/run'; 11 | 12 | test.before(async (t) => { 13 | await utils.setupApiServer(t, { 14 | jobs: [ 15 | { name: 'done', path: path.join(utils.root, 'basic.js') }, 16 | { 17 | name: 'delayed', 18 | path: path.join(utils.root, 'long.js'), 19 | timeout: 1000 20 | }, 21 | { 22 | name: 'waiting', 23 | path: path.join(utils.root, 'basic.js'), 24 | interval: 1000 25 | }, 26 | { 27 | name: 'active', 28 | path: path.join(utils.root, 'long.js') 29 | } 30 | ] 31 | }); 32 | t.context.token = jwt.sign({}, config.jwt.secret); 33 | 34 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 35 | }); 36 | 37 | test('successfully run named job', async (t) => { 38 | const { api, bree } = t.context; 39 | 40 | const res = await api.post(`${rootUrl}/delayed`).send({}); 41 | 42 | t.is(res.status, 200); 43 | 44 | await delay(200); 45 | 46 | t.falsy(bree.workers.has('active')); 47 | t.falsy(bree.intervals.has('waiting')); 48 | t.falsy(bree.timeouts.has('delayed')); 49 | t.truthy(bree.workers.has('delayed')); 50 | }); 51 | -------------------------------------------------------------------------------- /test/api/v1/sse.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { once } = require('events'); 3 | const test = require('ava'); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | const config = require('../../../config'); 7 | 8 | const utils = require('../../utils'); 9 | 10 | const rootUrl = '/v1/sse'; 11 | 12 | test.before(async (t) => { 13 | await utils.setupApiServer(t, { 14 | jobs: [ 15 | { 16 | name: 'delayed', 17 | path: path.join(utils.root, 'basic.js'), 18 | interval: 1000 19 | } 20 | ] 21 | }); 22 | t.context.token = jwt.sign({}, config.jwt.secret); 23 | 24 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 25 | 26 | await t.context.bree.start(); 27 | }); 28 | 29 | test('successfully connect to sse', async (t) => { 30 | const es = utils.setupEventSource(t, rootUrl); 31 | 32 | await once(es, 'open'); 33 | 34 | t.pass(); 35 | }); 36 | 37 | const eventsMacro = test.macro({ 38 | async exec(t, event) { 39 | const es = utils.setupEventSource(t, rootUrl); 40 | 41 | const [res] = await once(es, event); 42 | 43 | t.is(res.type, event); 44 | t.is(res.data, 'delayed'); 45 | }, 46 | title(_, event) { 47 | return `successfully listen to "${event}" messages`; 48 | } 49 | }); 50 | 51 | test(eventsMacro, 'worker created'); 52 | test(eventsMacro, 'worker deleted'); 53 | -------------------------------------------------------------------------------- /test/api/v1/start.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('ava'); 3 | const jwt = require('jsonwebtoken'); 4 | const delay = require('delay'); 5 | 6 | const config = require('../../../config'); 7 | 8 | const utils = require('../../utils'); 9 | 10 | const rootUrl = '/v1/start'; 11 | 12 | test.before(async (t) => { 13 | await utils.setupApiServer(t, { 14 | jobs: [ 15 | { name: 'done', path: path.join(utils.root, 'basic.js') }, 16 | { 17 | name: 'delayed', 18 | path: path.join(utils.root, 'basic.js'), 19 | timeout: 1000 20 | }, 21 | { 22 | name: 'waiting', 23 | path: path.join(utils.root, 'basic.js'), 24 | interval: 1000 25 | }, 26 | { 27 | name: 'active', 28 | path: path.join(utils.root, 'long.js') 29 | } 30 | ] 31 | }); 32 | t.context.token = jwt.sign({}, config.jwt.secret); 33 | 34 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 35 | }); 36 | 37 | test.serial('successfully start all jobs', async (t) => { 38 | const { bree, api } = t.context; 39 | 40 | const res = await api.post(rootUrl).send({}); 41 | 42 | t.is(res.status, 200); 43 | 44 | await delay(200); 45 | 46 | t.truthy(bree.workers.has('active')); 47 | t.truthy(bree.timeouts.has('delayed')); 48 | t.truthy(bree.intervals.has('waiting')); 49 | }); 50 | 51 | test.serial('successfully start named job', async (t) => { 52 | const { api, bree } = t.context; 53 | 54 | const jobs = [ 55 | { 56 | name: 'immediate', 57 | path: path.join(utils.root, 'long.js') 58 | }, 59 | { 60 | name: 'timeout', 61 | path: path.join(utils.root, 'long.js'), 62 | timeout: 2000 63 | } 64 | ]; 65 | 66 | await api.post('/v1/jobs').send({ 67 | jobs 68 | }); 69 | 70 | const res = await api.post(`${rootUrl}/timeout`).send({}); 71 | 72 | t.is(res.status, 200); 73 | t.is(res.body.length, 1); 74 | t.like(res.body[0], { name: 'timeout' }); 75 | 76 | await delay(200); 77 | 78 | t.falsy(bree.workers.has('immediate')); 79 | t.truthy(bree.timeouts.has('timeout')); 80 | }); 81 | -------------------------------------------------------------------------------- /test/api/v1/stop.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const test = require('ava'); 3 | const jwt = require('jsonwebtoken'); 4 | const delay = require('delay'); 5 | 6 | const config = require('../../../config'); 7 | 8 | const utils = require('../../utils'); 9 | 10 | const rootUrl = '/v1/stop'; 11 | 12 | test.before(async (t) => { 13 | await utils.setupApiServer(t, { 14 | jobs: [ 15 | { name: 'done', path: path.join(utils.root, 'basic.js') }, 16 | { 17 | name: 'delayed', 18 | path: path.join(utils.root, 'basic.js'), 19 | timeout: 1000 20 | }, 21 | { 22 | name: 'waiting', 23 | path: path.join(utils.root, 'basic.js'), 24 | interval: 1000 25 | }, 26 | { 27 | name: 'active', 28 | path: path.join(utils.root, 'long.js') 29 | } 30 | ] 31 | }); 32 | t.context.token = jwt.sign({}, config.jwt.secret); 33 | 34 | t.context.api = t.context.api.auth(t.context.token, { type: 'bearer' }); 35 | 36 | await t.context.bree.start(); 37 | 38 | await delay(200); 39 | }); 40 | 41 | test.serial('successfully stop named job', async (t) => { 42 | const { api, bree } = t.context; 43 | 44 | t.truthy(bree.workers.has('active')); 45 | 46 | const res = await api.post(`${rootUrl}/active`).send({}); 47 | 48 | t.is(res.status, 200); 49 | t.is(res.body.length, 1); 50 | t.like(res.body[0], { name: 'active' }); 51 | 52 | t.falsy(bree.workers.has('active')); 53 | }); 54 | 55 | test.serial('successfully stop all jobs', async (t) => { 56 | const { bree, api } = t.context; 57 | 58 | t.not(bree.timeouts.size, 0); 59 | t.not(bree.intervals.size, 0); 60 | 61 | const res = await api.post(rootUrl).send({}); 62 | 63 | t.is(res.status, 200); 64 | 65 | t.is(bree.workers.size, 0); 66 | t.is(bree.timeouts.size, 0); 67 | t.is(bree.intervals.size, 0); 68 | }); 69 | -------------------------------------------------------------------------------- /test/jobs/basic.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const { parentPort } = require('worker_threads'); 3 | 4 | setTimeout(() => { 5 | console.log('hello'); 6 | process.exit(0); 7 | }, 100); 8 | 9 | if (parentPort) { 10 | parentPort.on('message', (message) => { 11 | if (message === 'error') throw new Error('oops'); 12 | if (message === 'cancel') { 13 | parentPort.postMessage('cancelled'); 14 | return; 15 | } 16 | 17 | parentPort.postMessage(message); 18 | process.exit(0); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/jobs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/jobs/long.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const { parentPort } = require('worker_threads'); 3 | 4 | setTimeout(() => { 5 | console.log('hello'); 6 | process.exit(0); 7 | }, 10_000); 8 | 9 | if (parentPort) { 10 | parentPort.on('message', (message) => { 11 | if (message === 'error') throw new Error('oops'); 12 | if (message === 'cancel') { 13 | parentPort.postMessage('cancelled'); 14 | return; 15 | } 16 | 17 | parentPort.postMessage(message); 18 | process.exit(0); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/plugin/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const { baseConfig } = require('../utils'); 4 | 5 | test.before((t) => { 6 | t.context.Bree = require('bree'); 7 | t.context.plugin = require('../..').plugin; 8 | 9 | t.context.Bree.extend(t.context.plugin); 10 | }); 11 | 12 | test('api does not exist on bree constructor', (t) => { 13 | const { Bree } = t.context; 14 | 15 | t.is(typeof Bree.api, 'undefined'); 16 | }); 17 | 18 | test('api does exist on bree instance', async (t) => { 19 | const { Bree } = t.context; 20 | 21 | const bree = new Bree(baseConfig); 22 | await bree.init(); 23 | 24 | t.log(bree); 25 | // just to make sure this works correctly 26 | t.is(typeof bree.start, 'function'); 27 | t.log(bree.api); 28 | t.is(typeof bree.api, 'object'); 29 | 30 | // options is set correctly by default 31 | t.is(bree.api.config.port, 62_893); 32 | t.is(bree.api.config.jwt.secret, 'secret'); 33 | }); 34 | -------------------------------------------------------------------------------- /test/plugin/options.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Bree = require('bree'); 3 | 4 | const { plugin } = require('../..'); 5 | const { baseConfig } = require('../utils'); 6 | 7 | test('can modify options', async (t) => { 8 | t.not(plugin.$i, true); 9 | 10 | Bree.extend(plugin, { 11 | port: 3000, 12 | jwt: { secret: 'thisisasecret' }, 13 | sse: { maxClients: 100 } 14 | }); 15 | 16 | const bree = new Bree(baseConfig); 17 | await bree.init(); 18 | 19 | t.is(bree.api.config.port, 3000); 20 | t.is(bree.api.config.jwt.secret, 'thisisasecret'); 21 | t.is(bree.api.config.sse.maxClients, 100); 22 | }); 23 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | // Necessary utils for testing 2 | // Librarires required for testing 3 | const path = require('path'); 4 | const request = require('supertest'); 5 | const getPort = require('get-port'); 6 | const EventSource = require('eventsource'); 7 | 8 | // 9 | // setup utilities 10 | // 11 | exports.setupApiServer = async (t, config = {}, pluginConfig = {}) => { 12 | // Must require here in order to load changes made during setup 13 | const Bree = require('bree'); 14 | const { plugin } = require('../'); 15 | 16 | const port = await getPort(); 17 | 18 | Bree.extend(plugin, { port, ...pluginConfig }); 19 | 20 | const bree = new Bree({ ...baseConfig, ...config }); 21 | 22 | await bree.init(); 23 | 24 | t.context.api = request.agent(bree.api.server); 25 | t.context.bree = bree; 26 | }; 27 | 28 | const root = path.join(__dirname, 'jobs'); 29 | exports.root = root; 30 | 31 | const baseConfig = { 32 | root, 33 | timeout: 0, 34 | interval: 0, 35 | hasSeconds: false, 36 | defaultExtension: 'js', 37 | jobs: ['basic'] 38 | }; 39 | exports.baseConfig = baseConfig; 40 | 41 | exports.setupEventSource = (t, endpoint) => { 42 | const { token, bree } = t.context; 43 | 44 | return new EventSource( 45 | `http://${bree.api.config.serverHost}:${bree.api.config.port}${endpoint}/${token}` 46 | ); 47 | }; 48 | --------------------------------------------------------------------------------