├── .cz-config.js ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ ├── main.yaml │ ├── release.yaml │ └── sync-docs.yaml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .mocharc.json ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .sonarcloud.properties ├── .vscode ├── settings.json └── tasks.json ├── .yo-rc.json ├── CHANGELOG.md ├── DEVELOPING.md ├── LICENSE ├── README.md ├── catalog-info.yaml ├── commitlint.config.js ├── docs └── README.md ├── mkdocs.yml ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── acceptance │ │ └── README.md │ ├── integration │ │ └── README.md │ ├── mock-sdk.ts │ └── unit │ │ ├── README.md │ │ ├── apns.provider.unit.ts │ │ ├── fcm.provider.unit.ts │ │ ├── nodemailer.provider.unit.ts │ │ ├── pubnub.provider.unit.ts │ │ ├── ses.provider.unit.ts │ │ ├── sns.provider.unit.ts │ │ ├── socketio.provider.unit.ts │ │ └── twilio.provider.unit.ts ├── component.ts ├── controllers │ └── README.md ├── decorators │ └── README.md ├── error-keys.ts ├── index.ts ├── keys.ts ├── mixins │ └── README.md ├── providers │ ├── README.md │ ├── email │ │ ├── index.ts │ │ ├── nodemailer │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ ├── nodemailer.provider.ts │ │ │ └── types.ts │ │ ├── ses │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ ├── ses.provider.ts │ │ │ └── types.ts │ │ └── types.ts │ ├── index.ts │ ├── notification.provider.ts │ ├── push │ │ ├── apns │ │ │ ├── apns.provider.ts │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ └── types.ts │ │ ├── fcm │ │ │ ├── fcm.provider.ts │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── pubnub │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ ├── pubnub.provider.ts │ │ │ └── types.ts │ │ ├── socketio │ │ │ ├── index.ts │ │ │ ├── keys.ts │ │ │ ├── socketio.provider.ts │ │ │ └── types.ts │ │ └── types.ts │ └── sms │ │ ├── index.ts │ │ ├── sns │ │ ├── index.ts │ │ ├── keys.ts │ │ ├── sns.provider.ts │ │ └── types.ts │ │ ├── twilio │ │ ├── index.ts │ │ ├── keys.ts │ │ ├── twilio.provider.ts │ │ └── types.ts │ │ └── types.ts ├── release_notes │ ├── mymarkdown.ejs │ ├── post-processing.js │ └── release-notes.js ├── repositories │ └── README.md └── types.ts └── tsconfig.json /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | {value: 'feat', name: 'feat: A new feature'}, 4 | {value: 'fix', name: 'fix: A bug fix'}, 5 | {value: 'docs', name: 'docs: Documentation only changes'}, 6 | { 7 | value: 'style', 8 | name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', 9 | }, 10 | { 11 | value: 'refactor', 12 | name: 'refactor: A code change that neither fixes a bug nor adds a feature', 13 | }, 14 | { 15 | value: 'perf', 16 | name: 'perf: A code change that improves performance', 17 | }, 18 | {value: 'test', name: 'test: Adding missing tests'}, 19 | { 20 | value: 'chore', 21 | name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation', 22 | }, 23 | {value: 'revert', name: 'revert: Reverting a commit'}, 24 | {value: 'WIP', name: 'WIP: Work in progress'}, 25 | ], 26 | 27 | scopes: [ 28 | {name: 'chore'}, 29 | {name: 'deps'}, 30 | {name: 'ci-cd'}, 31 | {name: 'component'}, 32 | {name: 'provider'}, 33 | {name: 'core'}, 34 | {name: 'maintenance'}, 35 | ], 36 | 37 | appendBranchNameToCommitMessage: true, 38 | appendIssueFromBranchName: true, 39 | allowTicketNumber: false, 40 | isTicketNumberRequired: false, 41 | 42 | // override the messages, defaults are as follows 43 | messages: { 44 | type: "Select the type of change that you're committing:", 45 | scope: 'Denote the SCOPE of this change:', 46 | // used if allowCustomScopes is true 47 | customScope: 'Denote the SCOPE of this change:', 48 | subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', 49 | body: 'Provide a LONGER description of the change (mandatory). Use "\\n" to break new line:\n', 50 | breaking: 'List any BREAKING CHANGES (optional):\n', 51 | footer: 'List any ISSUES CLOSED by this change (optional). E.g.: GH-144:\n', 52 | confirmCommit: 'Are you sure you want to proceed with the commit above?', 53 | }, 54 | 55 | allowCustomScopes: false, 56 | allowBreakingChanges: ['feat', 'fix'], 57 | 58 | // limit subject length 59 | subjectLimit: 100, 60 | breaklineChar: '|', // It is supported for fields body and footer. 61 | footerPrefix: '', 62 | askForBreakingChangeFirst: true, // default is false 63 | }; 64 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | 5 | .eslintrc.js 6 | .cz-config.js 7 | commitlint.config.js 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@loopback/eslint-config', 3 | rules: { 4 | 'no-extra-boolean-cast': 'off', 5 | '@typescript-eslint/interface-name-prefix': 'off', 6 | 'no-prototype-builtins': 'off', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@sourcefuse.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # loopback4-notifications 2 | 3 | ## Contributing 4 | 5 | First off, thank you for considering contributing to the project. It's people like you that helps in keeping this extension useful. 6 | 7 | ### Where do I go from here ? 8 | 9 | If you've noticed a bug or have a question, [search the issue tracker](https://github.com/sourcefuse/loopback4-notifications/issues) to see if 10 | someone else in the community has already created a ticket. If not, go ahead and 11 | [make one](https://github.com/sourcefuse/loopback4-notifications/issues/new/choose)! 12 | 13 | ### Fork & create a branch 14 | 15 | If this is something you think you can fix, then [fork](https://help.github.com/articles/fork-a-repo) this repo and 16 | create a branch with a descriptive name. 17 | 18 | A good branch name would be (where issue #325 is the ticket you're working on): 19 | 20 | ```sh 21 | git checkout -b 325-add-new-feature 22 | ``` 23 | 24 | ### Make a Pull Request 25 | 26 | At this point, you should switch back to your master branch and make sure it's 27 | up to date with loopback4-notifications's master branch: 28 | 29 | ```sh 30 | git remote add upstream git@github.com:sourcefuse/loopback4-notifications.git 31 | git checkout master 32 | git pull upstream master 33 | ``` 34 | 35 | Then update your feature branch from your local copy of master, and push it! 36 | 37 | ```sh 38 | git checkout 325-add-new-feature 39 | git rebase master 40 | git push --set-upstream origin 325-add-new-feature 41 | ``` 42 | 43 | Finally, go to GitHub and [make a Pull Request](https://help.github.com/articles/creating-a-pull-request). 44 | 45 | ### Keeping your Pull Request updated 46 | 47 | If a maintainer asks you to "rebase" your PR, they're saying that a lot of code 48 | has changed, and that you need to update your branch so it's easier to merge. 49 | 50 | To learn more about rebasing in Git, there are a lot of [good][git rebasing] 51 | [resources][interactive rebase] but here's the suggested workflow: 52 | 53 | ```sh 54 | git checkout 325-add-new-feature 55 | git pull --rebase upstream master 56 | git push --force-with-lease 325-add-new-feature 57 | ``` 58 | 59 | [git rebasing]: http://git-scm.com/book/en/Git-Branching-Rebasing 60 | [interactive rebase]: https://help.github.com/articles/interactive-rebase 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Intermediate change (work in progress) 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | ## Checklist: 24 | 25 | - [ ] Performed a self-review of my own code 26 | - [ ] npm test passes on your machine 27 | - [ ] New tests added or existing tests modified to cover all changes 28 | - [ ] Code conforms with the style guide 29 | - [ ] API Documentation in code was updated 30 | - [ ] Any dependent changes have been merged and published in downstream modules 31 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Label to use when marking an issue or a PR as stale 2 | staleLabel: stale 3 | 4 | # Configuration for issues 5 | issues: 6 | # Number of days of inactivity before an issue becomes stale 7 | daysUntilStale: 90 8 | # Comment to post when marking an issue as stale. Set to `false` to disable 9 | markComment: > 10 | This issue has been marked stale because it has not seen any activity within 11 | three months. If you believe this to be an error, please contact one of the code owners. 12 | This issue will be closed within 15 days of being stale. 13 | # Number of days of inactivity before a stale issue is closed 14 | daysUntilClose: 15 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: > 17 | This issue has been closed due to continued inactivity. Thank you for your understanding. 18 | If you believe this to be in error, please contact one of the code owners. 19 | # Configuration for pull requests 20 | pulls: 21 | # Number of days of inactivity before a PR becomes stale 22 | daysUntilStale: 60 23 | # Comment to post when marking a PR as stale. Set to `false` to disable 24 | markComment: > 25 | This pull request has been marked stale because it has not seen any activity 26 | within two months. It will be closed within 15 days of being stale 27 | unless there is new activity. 28 | # Number of days of inactivity before a stale PR is closed 29 | daysUntilClose: 15 30 | # Comment to post when closing a stale issue. Set to `false` to disable 31 | closeComment: > 32 | This pull request has been closed due to continued inactivity. If you are 33 | interested in finishing the proposed changes, then feel free to re-open 34 | this pull request or open a new one. 35 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | # This workflow contains a single job called "npm_test" 10 | jobs: 11 | npm_test: 12 | # The type of runner that the job will run on 13 | runs-on: ubuntu-latest 14 | 15 | # Steps represent a sequence of tasks that will be executed as part of the job 16 | steps: 17 | # Checks-out your repository under $GITHUB_WORKSPACE 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: '18.x' 22 | 23 | - name: Install Dependencies 📌 24 | run: npm ci 25 | 26 | - name: Run Test Cases 🔧 27 | run: npm run test 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # This Manually Executable Workflow is for NPM Releases 2 | 3 | name: Release [Manual] 4 | on: workflow_dispatch 5 | permissions: 6 | contents: write 7 | jobs: 8 | Release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | # fetch-depth is necessary to get all tags 14 | # otherwise lerna can't detect the changes and will end up bumping the versions for all packages 15 | fetch-depth: 0 16 | token: ${{ secrets.RELEASE_COMMIT_GH_PAT }} 17 | - name: Setup Node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '18.x' 21 | - name: Configure CI Git User 22 | run: | 23 | git config --global user.name $CONFIG_USERNAME 24 | git config --global user.email $CONFIG_EMAIL 25 | git remote set-url origin https://$GITHUB_ACTOR:$GITHUB_PAT@github.com/sourcefuse/loopback4-notifications 26 | env: 27 | GITHUB_PAT: ${{ secrets.RELEASE_COMMIT_GH_PAT }} 28 | CONFIG_USERNAME: ${{ vars.RELEASE_COMMIT_USERNAME }} 29 | CONFIG_EMAIL: ${{ vars.RELEASE_COMMIT_EMAIL }} 30 | - name: Authenticate with Registry 31 | run: | 32 | echo "@${NPM_USERNAME}:registry=https://registry.npmjs.org/" > .npmrc 33 | echo "registry=https://registry.npmjs.org/" >> .npmrc 34 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc 35 | npm whoami 36 | env: 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | NPM_USERNAME: ${{ vars.NPM_USERNAME }} 39 | 40 | - name: Install 📌 41 | run: | 42 | npm install 43 | - name: Test 🔧 44 | run: npm run test 45 | - name: Semantic Publish to NPM 🚀 46 | # "HUSKY=0" disables pre-commit-msg check (Needed in order to allow semantic-release perform the release commit) 47 | run: HUSKY=0 npx semantic-release 48 | env: 49 | GH_TOKEN: ${{ secrets.RELEASE_COMMIT_GH_PAT }} 50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | - name: Changelog 📝 52 | run: cd src/release_notes && HUSKY=0 node release-notes.js 53 | -------------------------------------------------------------------------------- /.github/workflows/sync-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Sync Docs to arc-docs repo 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | DOCS_REPO: sourcefuse/arc-docs 10 | BRANCH_PREFIX: automated-docs-sync/ 11 | GITHUB_TOKEN: ${{secrets.ARC_DOCS_API_TOKEN_GITHUB}} 12 | CONFIG_USERNAME: ${{ vars.GIT_COMMIT_USERNAME }} 13 | CONFIG_EMAIL: ${{ vars.GIT_COMMIT_EMAIL }} 14 | 15 | jobs: 16 | sync-docs: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout Extension Code 21 | uses: actions/checkout@v3 22 | with: 23 | token: ${{env.GITHUB_TOKEN}} 24 | path: './extension/' 25 | 26 | - name: Checkout Docs Repository 27 | uses: actions/checkout@v3 28 | with: 29 | token: ${{env.GITHUB_TOKEN}} 30 | repository: ${{env.DOCS_REPO}} 31 | path: './arc-docs/' 32 | 33 | - name: Configure GIT 34 | id: configure_git 35 | working-directory: arc-docs 36 | run: | 37 | git config --global user.email $CONFIG_EMAIL 38 | git config --global user.name $CONFIG_USERNAME 39 | 40 | extension_branch="${{env.BRANCH_PREFIX}}$(basename $GITHUB_REPOSITORY)" 41 | echo "extension_branch=$extension_branch" >> $GITHUB_OUTPUT 42 | 43 | - name: Update Files 44 | id: update_files 45 | working-directory: arc-docs 46 | run: | 47 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}" 48 | 49 | # Create a new branch if it doesn't exist, or switch to it if it does 50 | git checkout -B $extension_branch || git checkout $extension_branch 51 | 52 | # Copy README from the extension repo 53 | cp ../extension/docs/README.md docs/arc-api-docs/extensions/$(basename $GITHUB_REPOSITORY)/ 54 | git add . 55 | 56 | if git diff --quiet --cached; then 57 | have_changes="false"; 58 | else 59 | have_changes="true"; 60 | fi 61 | 62 | echo "Have Changes to be commited: $have_changes" 63 | echo "have_changes=$have_changes" >> $GITHUB_OUTPUT 64 | 65 | - name: Commit Changes 66 | id: commit 67 | working-directory: arc-docs 68 | if: steps.update_files.outputs.have_changes == 'true' 69 | run: | 70 | git commit -m "sync $(basename $GITHUB_REPOSITORY) docs" 71 | - name: Push Changes 72 | id: push_branch 73 | if: steps.update_files.outputs.have_changes == 'true' 74 | working-directory: arc-docs 75 | run: | 76 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}" 77 | git push https://oauth2:${GITHUB_TOKEN}@github.com/${{env.DOCS_REPO}}.git HEAD:$extension_branch --force 78 | 79 | - name: Check PR Status 80 | id: pr_status 81 | if: steps.update_files.outputs.have_changes == 'true' 82 | working-directory: arc-docs 83 | run: | 84 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}" 85 | gh pr status --json headRefName >> "${{github.workspace}}/pr-status.json" 86 | pr_exists="$(jq --arg extension_branch "$extension_branch" '.createdBy[].headRefName == $extension_branch' "${{github.workspace}}/pr-status.json")" 87 | echo "PR Exists: $pr_exists" 88 | echo "pr_exists=$pr_exists" >> $GITHUB_OUTPUT 89 | 90 | - name: Create Pull Request 91 | id: create_pull_request 92 | if: steps.pr_status.outputs.pr_exists != 'true' && steps.update_files.outputs.have_changes == 'true' 93 | working-directory: arc-docs 94 | run: | 95 | extension_branch="${{ steps.configure_git.outputs.extension_branch }}" 96 | 97 | gh pr create --head $(git branch --show-current) --title "Sync ${{ github.event.repository.name }} Docs" --body "This Pull Request has been created by the 'sync-docs' action within the '${{ github.event.repository.name }}' repository, with the purpose of updating markdown files." 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Transpiled JavaScript files from Typescript 61 | /dist 62 | 63 | # Cache used by TypeScript's incremental build 64 | *.tsbuildinfo 65 | 66 | .DS_Store 67 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npx cz --hook || true 5 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "recursive": true, 3 | "require": "source-map-support/register" 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | scripts-prepend-node-path=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.json 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "all", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | # Path to sources 2 | sonar.sources=src 3 | sonar.exclusions=src/__tests__/** 4 | #sonar.inclusions= 5 | # Path to tests 6 | sonar.tests=src/__tests__ 7 | #sonar.test.exclusions= 8 | #sonar.test.inclusions= 9 | # Source encoding 10 | sonar.sourceEncoding=UTF-8 11 | # Exclusions for copy-paste detection 12 | #sonar.cpd.exclusions= 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [80], 3 | "editor.tabCompletion": "on", 4 | "editor.tabSize": 2, 5 | "editor.trimAutoWhitespace": true, 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.organizeImports": true, 9 | "source.fixAll.eslint": true 10 | }, 11 | 12 | "files.exclude": { 13 | "**/.DS_Store": true, 14 | "**/.git": true, 15 | "**/.hg": true, 16 | "**/.svn": true, 17 | "**/CVS": true 18 | }, 19 | "files.insertFinalNewline": true, 20 | "files.trimTrailingWhitespace": true, 21 | 22 | "typescript.tsdk": "./node_modules/typescript/lib", 23 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 24 | "typescript.preferences.quoteStyle": "single", 25 | "eslint.run": "onSave", 26 | "eslint.nodePath": "./node_modules", 27 | "eslint.validate": [ 28 | "javascript", 29 | "typescript" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Watch and Compile Project", 8 | "type": "shell", 9 | "command": "npm", 10 | "args": ["--silent", "run", "build:watch"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "problemMatcher": "$tsc-watch" 16 | }, 17 | { 18 | "label": "Build, Test and Lint", 19 | "type": "shell", 20 | "command": "npm", 21 | "args": ["--silent", "run", "test:dev"], 22 | "group": { 23 | "kind": "test", 24 | "isDefault": true 25 | }, 26 | "problemMatcher": ["$tsc", "$eslint-compact", "$eslint-stylish"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@loopback/cli": { 3 | "version": "2.7.1", 4 | "packageManager": "npm" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release [v9.0.1](https://github.com/sourcefuse/loopback4-notifications/compare/v9.0.0..v9.0.1) December 13, 2024 2 | Welcome to the December 13, 2024 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 3 | 4 | - [](https://github.com/sourcefuse/loopback4-notifications/issues/) :- [chore(deps): version update ](https://github.com/sourcefuse/loopback4-notifications/commit/9e6b399a9192e43c47b8cfde29da63b2e3c05e76) was commited on December 13, 2024 by [Sunny Tyagi](mailto:107617248+Tyagi-Sunny@users.noreply.github.com) 5 | 6 | - version update 7 | 8 | - gh-0 9 | 10 | 11 | Clink on the above links to understand the changes in detail. 12 | ___ 13 | 14 | ## Release [v9.0.0](https://github.com/sourcefuse/loopback4-notifications/compare/v8.1.0..v9.0.0) December 9, 2024 15 | Welcome to the December 9, 2024 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 16 | 17 | - [Android and iOS Push Notification payload using pnGcm and pnApns has been deprecated after June 2024. Need to update the payload to use pn_fcm and pn_apns](https://github.com/sourcefuse/loopback4-notifications/issues/211) :- [feat(provider): previous change was breaking ](https://github.com/sourcefuse/loopback4-notifications/commit/34656ce511f38b1210180d087ff99cbdaca3356c) was commited on December 9, 2024 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com) 18 | 19 | - commit for major version bump 20 | 21 | - BREAKING CHANGE: 22 | 23 | - yes 24 | 25 | - GH-211 26 | 27 | 28 | Clink on the above links to understand the changes in detail. 29 | ___ 30 | 31 | ## Release [v8.0.3](https://github.com/sourcefuse/loopback4-notifications/compare/v8.0.2..v8.0.3) June 4, 2024 32 | Welcome to the June 4, 2024 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 33 | 34 | - [loopback version updates](https://github.com/sourcefuse/loopback4-notifications/issues/191) :- [chore(deps): loopback version updates ](https://github.com/sourcefuse/loopback4-notifications/commit/ec31b58229f9ba8fac3e099f5698f4b06fa28eec) was commited on June 4, 2024 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com) 35 | 36 | - loopback version updates 37 | 38 | - GH-191 39 | 40 | 41 | Clink on the above links to understand the changes in detail. 42 | ___ 43 | 44 | ## Release [v8.0.2](https://github.com/sourcefuse/loopback4-notifications/compare/v8.0.1..v8.0.2) May 16, 2024 45 | Welcome to the May 16, 2024 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 46 | 47 | - [Version bump](https://github.com/sourcefuse/loopback4-notifications/issues/189) :- [fix(ci-cd): version bump for release ](https://github.com/sourcefuse/loopback4-notifications/commit/e4bcca377802b5ed7230c9419f151ccdfce10b8a) was commited on May 16, 2024 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com) 48 | 49 | - dummy commit 50 | 51 | - GH-189 52 | 53 | 54 | Clink on the above links to understand the changes in detail. 55 | ___ 56 | 57 | ## Release [v6.0.1](https://github.com/sourcefuse/loopback4-notifications/compare/v6.0.0..v6.0.1) July 14, 2023 58 | Welcome to the July 14, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 59 | 60 | - [](https://github.com/sourcefuse/loopback4-notifications/issues/0) :- [](https://github.com/sourcefuse/loopback4-notifications/commit/cb3dd91387e5e84891e021aea3bf189253b7c3dd) was commited on July 14, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com) 61 | 62 | - Closes #133 63 | 64 | - GH-0 65 | 66 | 67 | - [Add ARC Branding in README](https://github.com/sourcefuse/loopback4-notifications/issues/137) :- [docs(chore): add arc branding ](https://github.com/sourcefuse/loopback4-notifications/commit/6a43c5d6afca11d2f6ce8c295394012c0ff94aab) was commited on July 14, 2023 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com) 68 | 69 | - add arc branding 70 | 71 | - GH-137 72 | 73 | 74 | - [Loopback version update](https://github.com/sourcefuse/loopback4-notifications/issues/126) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-notifications/commit/ba011138a55bdb01f56cf8517af8454c858620ba) was commited on July 14, 2023 by [Surbhi](mailto:98279679+Surbhi-sharma1@users.noreply.github.com) 75 | 76 | - loopback version update 77 | 78 | - GH-126 79 | 80 | 81 | Clink on the above links to understand the changes in detail. 82 | ___ 83 | 84 | ## Release [v5.2.2](https://github.com/sourcefuse/loopback4-notifications/compare/v5.2.1..v5.2.2) April 25, 2023 85 | Welcome to the April 25, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 86 | 87 | - [Loopback version update](https://github.com/sourcefuse/loopback4-notifications/issues/126) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-notifications/commit/bdd4d669e6858395bb27eaeb35ae2fd843b172f7) was commited on April 25, 2023 by [RaghavaroraSF](mailto:97958393+RaghavaroraSF@users.noreply.github.com) 88 | 89 | - loopback version update 90 | 91 | - GH-126 92 | 93 | 94 | Clink on the above links to understand the changes in detail. 95 | ___ 96 | 97 | ## Release [v5.2.1](https://github.com/sourcefuse/loopback4-notifications/compare/v5.2.0..v5.2.1) April 24, 2023 98 | Welcome to the April 24, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 99 | 100 | - [Incorrect package used in documentation](https://github.com/sourcefuse/loopback4-notifications/issues/81) :- [fix(docs): typo and link formatting fix ](https://github.com/sourcefuse/loopback4-notifications/commit/aa831f831613e993918b7d0e7c934f1c94c5d525) was commited on April 7, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com) 101 | 102 | - of package name used in examples 103 | 104 | - GH-81 105 | 106 | 107 | Clink on the above links to understand the changes in detail. 108 | ___ 109 | 110 | ## Release [v5.2.1](https://github.com/sourcefuse/loopback4-notifications/compare/v5.2.0..v5.2.1) April 7, 2023 111 | Welcome to the April 7, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 112 | 113 | - [Incorrect package used in documentation](https://github.com/sourcefuse/loopback4-notifications/issues/81) :- [fix(docs): typo and link formatting fix ](https://github.com/sourcefuse/loopback4-notifications/commit/aa831f831613e993918b7d0e7c934f1c94c5d525) was commited on April 7, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com) 114 | 115 | - of package name used in examples 116 | 117 | - GH-81 118 | 119 | 120 | Clink on the above links to understand the changes in detail. 121 | ___ 122 | 123 | ## Release [v5.2.0](https://github.com/sourcefuse/loopback4-notifications/compare/v5.1.2..v5.2.0) March 14, 2023 124 | Welcome to the March 14, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 125 | 126 | - [loopback4-notifications: fixing mock-sdk in unit test](https://github.com/sourcefuse/loopback4-notifications/issues/120) :- [chore(deps): lint fixes \& fixing mock-sdk in unit test ](https://github.com/sourcefuse/loopback4-notifications/commit/eb4676b1f34c4eaaded7e23c3dc9d13ed80baefe) was commited on March 14, 2023 by [Gautam Agarwal](mailto:108651274+gautam23-sf@users.noreply.github.com) 127 | 128 | - lint fixes \& fixing mock-sdk in unit test 129 | 130 | - GH-120 131 | 132 | 133 | - [ loopback version update ](https://github.com/sourcefuse/loopback4-notifications/issues/118) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-notifications/commit/4243224dc8d17df85c3025c68c174b83edcadd2f) was commited on March 13, 2023 by [Gautam Agarwal](mailto:108651274+gautam23-sf@users.noreply.github.com) 134 | 135 | - loopback version update 136 | 137 | - GH-118 138 | 139 | 140 | - [](https://github.com/sourcefuse/loopback4-notifications/issues/) :- [feat(provider): send notifications/messages to whatsapp or as text SMS using Twilio ](https://github.com/sourcefuse/loopback4-notifications/commit/f9d979992ae9ab2951a0a77bb94e281c2378b1af) was commited on March 10, 2023 by [sadarunnisa-sf](mailto:109595269+sadarunnisa-sf@users.noreply.github.com) 141 | 142 | - * feat(provider): send notifications/messages to whatsapp or as text SMS using 143 | 144 | - TWILIO(#88) 145 | 146 | - GH-88 147 | 148 | - * feat(provider): send notifications/messages to whatsapp or as text SMS using 149 | 150 | - TWILIO #88 151 | 152 | - GH-88 153 | 154 | - * send notifications/messages to whatsapp or as text SMS using Twilio 155 | 156 | - * send notifications/messages to whatsapp or as text SMS using Twilio 157 | 158 | - * send notifications/messages to whatsapp or as text SMS using Twilio 159 | 160 | - * send notifications/messages to whatsapp or as text SMS using Twilio 161 | 162 | - * send notifications/messages to whatsapp or as text SMS using Twilio 163 | 164 | - * send notifications/messages to whatsapp or as text SMS using Twilio 165 | 166 | 167 | - [Stale Bot missing in the repository ](https://github.com/sourcefuse/loopback4-notifications/issues/116) :- [chore(chore): add github stale bot ](https://github.com/sourcefuse/loopback4-notifications/commit/6f035a6999d222f2e65ce8f66f51c0e603f66f7e) was commited on February 27, 2023 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com) 168 | 169 | - Added stale.yml file to configure stale options 170 | 171 | - GH-116 172 | 173 | 174 | - [FCM and APNS usage missing in readme](https://github.com/sourcefuse/loopback4-notifications/issues/114) :- [docs(provider): add fcm and apns usage instructions ](https://github.com/sourcefuse/loopback4-notifications/commit/a1e07348f214eeb3fbe200e94310d7e45e2aa7d2) was commited on February 21, 2023 by [Shubham P](mailto:shubham.prajapat@sourcefuse.com) 175 | 176 | - refactored readme.md with grammer and formatting change 177 | 178 | - added the usage details for firebase 179 | 180 | - cloud messaging (fcm) and apple push 181 | 182 | - notifications (apns) 183 | 184 | - GH-114 185 | 186 | 187 | Clink on the above links to understand the changes in detail. 188 | ___ 189 | 190 | ## Release [v5.1.2](https://github.com/sourcefuse/loopback4-notifications/compare/v5.1.1..v5.1.2) February 20, 2023 191 | Welcome to the February 20, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 192 | 193 | - [Correct the changelog Format](https://github.com/sourcefuse/loopback4-notifications/issues/112) :- [fix(chore): correct the changelog format ](https://github.com/sourcefuse/loopback4-notifications/commit/d88787dfcd3b55d4d547d36449b39b20fcff9190) was commited on February 20, 2023 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com) 194 | 195 | - now issue description will be visible 196 | 197 | - GH-112 198 | 199 | 200 | - [Package Update - loopback4-notifications](https://github.com/sourcefuse/loopback4-notifications/issues/109) :- [fix(chore): remove all current vilnenrability of loopback4-notifications ](https://github.com/sourcefuse/loopback4-notifications/commit/4ee18e991cbd2fccf544057ed436c7fef4fb6970) was commited on February 20, 2023 by [Sunny Tyagi](mailto:107617248+Tyagi-Sunny@users.noreply.github.com) 201 | 202 | - remove all current vilnenrability of loopback4-notifications 203 | 204 | - GH-109 205 | 206 | 207 | Clink on the above links to understand the changes in detail. 208 | ___ 209 | 210 | ## Release [v5.1.1](https://github.com/sourcefuse/loopback4-notifications/compare/v5.1.0..v5.1.1) January 15, 2023 211 | Welcome to the January 15, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 212 | 213 | - [](https://github.com/sourcefuse/loopback4-notifications/issues/) :- [](https://github.com/sourcefuse/loopback4-notifications/commit/c349d904ce8347ed9eb242ef9eda95ef0098a7d3) was commited on January 15, 2023 by [Samarpan Bhattacharya](mailto:samarpan.bhattacharya@Samarpans-MacBook-Air.local) 214 | 215 | - @parse/node-apn was a dependency but its an optional feature. So its moved as a 216 | 217 | - dev dependency and exported as optional providers. 218 | 219 | - gh-0 220 | 221 | 222 | Clink on the above links to understand the changes in detail. 223 | ___ 224 | 225 | ## Release [v5.1.0](https://github.com/sourcefuse/loopback4-notifications/compare/v5.0.3..v5.1.0) January 11, 2023 226 | Welcome to the January 11, 2023 release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 227 | 228 | - [](https://github.com/sourcefuse/loopback4-notifications/issues/-105) :- [feat(chore): generate detailed and informative changelog ](https://github.com/sourcefuse/loopback4-notifications/commit/c6bfb98e3b41d34146d3f3b6c1e979582e468c6a) was commited on January 11, 2023 by [yeshamavani](mailto:83634146+yeshamavani@users.noreply.github.com) 229 | 230 | - Using Customizable npm package to generate changelog 231 | 232 | - GH-105 233 | 234 | 235 | - [](https://github.com/sourcefuse/loopback4-notifications/issues/-103) :- [chore(deps): loopback version update ](https://github.com/sourcefuse/loopback4-notifications/commit/8d0736e4a07f06729e65cc982226c9c5a11b4530) was commited on January 11, 2023 by [Surbhi Sharma](mailto:98279679+Surbhi-sharma1@users.noreply.github.com) 236 | 237 | - Updated version of the lb4 dependencies to the latest. 238 | 239 | - GH-103 240 | 241 | 242 | Clink on the above links to understand the changes in detail. 243 | ___ 244 | 245 | ## [5.0.3](https://github.com/sourcefuse/loopback4-notifications/compare/v5.0.2...v5.0.3) (2022-12-02) 246 | 247 | ## [5.0.2](https://github.com/sourcefuse/loopback4-notifications/compare/v5.0.1...v5.0.2) (2022-10-31) 248 | 249 | ## [5.0.1](https://github.com/sourcefuse/loopback4-notifications/compare/v5.0.0...v5.0.1) (2022-09-09) 250 | 251 | # [5.0.0](https://github.com/sourcefuse/loopback4-notifications/compare/v4.0.1...v5.0.0) (2022-07-18) 252 | 253 | 254 | ### Bug Fixes 255 | 256 | * **deps:** replace vulnerable node-apn ([#87](https://github.com/sourcefuse/loopback4-notifications/issues/87)) ([e0bddab](https://github.com/sourcefuse/loopback4-notifications/commit/e0bddab344b9bc681b5c30eeaba31e4ca94ed107)), closes [#83](https://github.com/sourcefuse/loopback4-notifications/issues/83) 257 | 258 | 259 | ### BREAKING CHANGES 260 | 261 | * **deps:** replace vulnerable node-apn 262 | 263 | ## [4.0.1](https://github.com/sourcefuse/loopback4-notifications/compare/v4.0.0...v4.0.1) (2022-07-08) 264 | 265 | 266 | ### Bug Fixes 267 | 268 | * **provider:** payloadtype path in pubnub provider ([#79](https://github.com/sourcefuse/loopback4-notifications/issues/79)) ([7c4f04a](https://github.com/sourcefuse/loopback4-notifications/commit/7c4f04a750a4b0de276de313e75cb414e5955ea4)), closes [#0](https://github.com/sourcefuse/loopback4-notifications/issues/0) 269 | 270 | # [4.0.0](https://github.com/sourcefuse/loopback4-notifications/compare/v3.1.1...v4.0.0) (2022-06-29) 271 | 272 | 273 | ### Bug Fixes 274 | 275 | * **provider:** fix import error for aws, pubnub and nodemailer ([#52](https://github.com/sourcefuse/loopback4-notifications/issues/52)) ([40eee0b](https://github.com/sourcefuse/loopback4-notifications/commit/40eee0b85a59e0c3f3b39db8d9e724f644d41967)), closes [#30](https://github.com/sourcefuse/loopback4-notifications/issues/30) 276 | 277 | 278 | ### BREAKING CHANGES 279 | 280 | * **provider:** change import path for specific providers 281 | 282 | ## [3.1.1](https://github.com/sourcefuse/loopback4-notifications/compare/v3.1.0...v3.1.1) (2022-06-17) 283 | 284 | # [3.1.0](https://github.com/sourcefuse/loopback4-notifications/compare/v3.0.4...v3.1.0) (2022-05-26) 285 | 286 | 287 | ### Bug Fixes 288 | 289 | * **deps:** remove vulnerabilities ([#72](https://github.com/sourcefuse/loopback4-notifications/issues/72)) ([f3e6999](https://github.com/sourcefuse/loopback4-notifications/commit/f3e6999ae615f41fbb11522c3857bc7eb5f00241)), closes [#71](https://github.com/sourcefuse/loopback4-notifications/issues/71) [#71](https://github.com/sourcefuse/loopback4-notifications/issues/71) 290 | 291 | 292 | ### Features 293 | 294 | * **provider:** apns provider added to notification service ([#64](https://github.com/sourcefuse/loopback4-notifications/issues/64)) ([ca6b120](https://github.com/sourcefuse/loopback4-notifications/commit/ca6b120d754827e89ec5dacc4e9905338312b7e7)), closes [#46](https://github.com/sourcefuse/loopback4-notifications/issues/46) [#46](https://github.com/sourcefuse/loopback4-notifications/issues/46) -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developer's Guide 2 | 3 | We use Visual Studio Code for developing LoopBack and recommend the same to our 4 | users. 5 | 6 | ## VSCode setup 7 | 8 | Install the following extensions: 9 | 10 | - [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 11 | - [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 12 | 13 | ## Setup Commit Hooks 14 | 15 | Run the following script to prepare husky after the first time install - 16 | 17 | `npm run prepare` 18 | 19 | ## Development workflow 20 | 21 | ### Visual Studio Code 22 | 23 | 1. Start the build task (Cmd+Shift+B) to run TypeScript compiler in the 24 | background, watching and recompiling files as you change them. Compilation 25 | errors will be shown in the VSCode's "PROBLEMS" window. 26 | 27 | 2. Execute "Run Rest Task" from the Command Palette (Cmd+Shift+P) to re-run the 28 | test suite and lint the code for both programming and style errors. Linting 29 | errors will be shown in VSCode's "PROBLEMS" window. Failed tests are printed 30 | to terminal output only. 31 | 32 | ### Other editors/IDEs 33 | 34 | 1. Open a new terminal window/tab and start the continuous build process via 35 | `npm run build:watch`. It will run TypeScript compiler in watch mode, 36 | recompiling files as you change them. Any compilation errors will be printed 37 | to the terminal. 38 | 39 | 2. In your main terminal window/tab, run `npm run test:dev` to re-run the test 40 | suite and lint the code for both programming and style errors. You should run 41 | this command manually whenever you have new changes to test. Test failures 42 | and linter errors will be printed to the terminal. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [SourceFuse] 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 | ARC By SourceFuse logo 2 | 3 | # [loopback4-notifications](https://github.com/sourcefuse/loopback4-notifications) 4 | 5 |

6 | 7 | npm version 8 | 9 | 10 | Sonar Quality Gate 11 | 12 | 13 | Synk Status 14 | 15 | 16 | GitHub contributors 17 | 18 | 19 | downloads 20 | 21 | 22 | License 23 | 24 | 25 | Powered By LoopBack 4 26 | 27 |

28 | 29 | ## Overview 30 | 31 | This is a loopback-next extension for adding different notification mechanisms vis-à-vis, Push, SMS, Email to any loopback 4 based REST API application or microservice. 32 | 33 | It provides a generic provider-based framework to add your own implementation or implement any external service provider to achieve the same. There are 3 different providers available to be injected namely, PushProvider, SMSProvider and EmailProvider. It also provides support for 7 very popular external services for sending notifications. 34 | 35 | 1. [AWS Simple Email Service](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html) - It's one of the EmailProvider for sending email messages. 36 | 2. [AWS Simple Notification Service](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SNS.html) - It's one of the SMSProvider for sending SMS notifications. 37 | 3. [Pubnub](https://www.pubnub.com/docs/nodejs-javascript/pubnub-javascript-sdk) - It's one of the PushProvider for sending realtime push notifications to mobile applications as well as web applications. 38 | 4. [Socket.IO](https://socket.io/docs/) - It's one of the PushProvider for sending realtime push notifications to mobile applications as well as web applications. 39 | 5. [FCM](https://firebase.google.com/docs/cloud-messaging) - It's one of the PushProvider for sending realtime push notifications to mobile applications as well as web applications. 40 | 6. [Nodemailer](https://nodemailer.com/about/) - It's one of the EmailProvider for sending email messages. 41 | 7. [Apple Push Notification service](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1) - It's one of the push notification providers that integrates notification service created by Apple Inc. that enables third party application developers to send notification data to applications installed on Apple devices. 42 | 8. [Twilio SMS Service](https://www.twilio.com/docs/sms) - Twilio is a modern communication API Used by developers for establishing communications. Twilio can be used for sending SMS or [Whatapp notifications](https://www.twilio.com/docs/whatsapp). 43 | You can use one of these services or add your own implementation or integration using the same interfaces and attach it as a provider for that specific type. 44 | 45 | You can use one of these services or add your own implementation or integration using the same interfaces and attach it as a provider for that specific type. 46 | 47 | ## Installation 48 | 49 | ```sh 50 | npm install loopback4-notifications 51 | ``` 52 | 53 | ## Usage 54 | 55 | In order to use this component into your LoopBack application, please follow below steps. 56 | 57 | Add component to application. 58 | 59 | ```ts 60 | // application.ts 61 | import {NotificationsComponent} from 'loopback4-notifications'; 62 | 63 | export class NotificationServiceApplication extends BootMixin( 64 | ServiceMixin(RepositoryMixin(RestApplication)), 65 | ) { 66 | constructor(options: ApplicationConfig = {}) { 67 | // ... 68 | 69 | this.component(NotificationsComponent); 70 | // ... 71 | } 72 | } 73 | ``` 74 | 75 | After the above, you need to configure one of the notification provider at least. Based upon the requirement, please choose and configure the respective provider for sending notifications. See below. 76 | 77 | ### Email Notifications using Amazon Simple Email Service 78 | 79 | This extension provides in-built support of AWS Simple Email Service integration for sending emails from the application. In order to use it, run `npm install aws-sdk`, and then bind the SesProvider as below in `application.ts`. 80 | 81 | ```ts 82 | import { 83 | NotificationsComponent, 84 | NotificationBindings, 85 | } from 'loopback4-notifications'; 86 | import {SesProvider} from 'loopback4-notifications/ses'; 87 | 88 | export class NotificationServiceApplication extends BootMixin( 89 | ServiceMixin(RepositoryMixin(RestApplication)), 90 | ) { 91 | constructor(options: ApplicationConfig = {}) { 92 | // ... 93 | 94 | this.component(NotificationsComponent); 95 | this.bind(NotificationBindings.EmailProvider).toProvider(SesProvider); 96 | // ... 97 | } 98 | } 99 | ``` 100 | 101 | There are some additional configurations needed in order to allow SES to connect to AWS. You need to add them as below. Make sure these are added before the provider binding. 102 | 103 | ```ts 104 | import { 105 | NotificationsComponent, 106 | NotificationBindings, 107 | } from 'loopback4-notifications'; 108 | import {SesProvider, SESBindings} from 'loopback4-notifications/ses'; 109 | 110 | export class NotificationServiceApplication extends BootMixin( 111 | ServiceMixin(RepositoryMixin(RestApplication)), 112 | ) { 113 | constructor(options: ApplicationConfig = {}) { 114 | // ... 115 | 116 | this.component(NotificationsComponent); 117 | this.bind(SESBindings.Config).to({ 118 | accessKeyId: process.env.SES_ACCESS_KEY_ID, 119 | secretAccessKey: process.env.SES_SECRET_ACCESS_KEY, 120 | region: process.env.SES_REGION, 121 | }); 122 | this.bind(NotificationBindings.EmailProvider).toProvider(SesProvider); 123 | // ... 124 | } 125 | } 126 | ``` 127 | 128 | All the configurations as specified by AWS docs [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html#constructor-property) are supported in above `SESBindings.Config` key. 129 | 130 | In addition to this, some general configurations can also be done, like below. 131 | 132 | ```ts 133 | import { 134 | NotificationsComponent, 135 | NotificationBindings, 136 | } from 'loopback4-notifications'; 137 | import {SesProvider, SESBindings} from 'loopback4-notifications/ses'; 138 | 139 | export class NotificationServiceApplication extends BootMixin( 140 | ServiceMixin(RepositoryMixin(RestApplication)), 141 | ) { 142 | constructor(options: ApplicationConfig = {}) { 143 | // ... 144 | 145 | this.component(NotificationsComponent); 146 | this.bind(NotificationBindings.Config).to({ 147 | sendToMultipleReceivers: false, 148 | senderEmail: 'support@myapp.com', 149 | }); 150 | this.bind(SESBindings.Config).to({ 151 | accessKeyId: process.env.SES_ACCESS_KEY_ID, 152 | secretAccessKey: process.env.SES_SECRET_ACCESS_KEY, 153 | region: process.env.SES_REGION, 154 | }); 155 | this.bind(NotificationBindings.EmailProvider).toProvider(SesProvider); 156 | // ... 157 | } 158 | } 159 | ``` 160 | 161 | Possible configuration options for the above are mentioned below. 162 | 163 | | Option | Type | Description | 164 | | ----------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 165 | | sendToMultipleReceivers | boolean | If set to true, single email will be sent to all receivers mentioned in payload. If set to false, multiple emails will be sent for each receiver mentioned in payload. | 166 | | senderEmail | string | This will be used as from email header in sent email. | 167 | 168 | If you wish to use any other service provider of your choice, you can create a provider for the same, similar to SesProvider we have. Add that provider in place of SesProvider. Refer to the implementation [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/src/providers/email/ses/). 169 | 170 | ```ts 171 | this.bind(NotificationBindings.EmailProvider).toProvider(MyOwnProvider); 172 | ``` 173 | 174 | ### Email Notifications Using Nodemailer 175 | 176 | This extension provides in-built support of Nodemailer integration for sending emails from the application. In order to use it, run `npm install nodemailer`, and then bind the NodemailerProvider as below in `application.ts`. 177 | 178 | ```ts 179 | import { 180 | NotificationsComponent, 181 | NotificationBindings, 182 | } from 'loopback4-notifications'; 183 | import {NodemailerProvider} from 'loopback4-notifications/nodemailer'; 184 | 185 | export class NotificationServiceApplication extends BootMixin( 186 | ServiceMixin(RepositoryMixin(RestApplication)), 187 | ) { 188 | constructor(options: ApplicationConfig = {}) { 189 | // ... 190 | 191 | this.component(NotificationsComponent); 192 | this.bind(NotificationBindings.EmailProvider).toProvider( 193 | NodemailerProvider, 194 | ); 195 | // ... 196 | } 197 | } 198 | ``` 199 | 200 | There are some additional configurations needed in order to allow NodeMailer to works. You need to add them as below. Make sure these are added before the provider binding. 201 | 202 | ```ts 203 | import { 204 | NotificationsComponent, 205 | NotificationBindings, 206 | } from 'loopback4-notifications'; 207 | import { 208 | NodemailerProvider, 209 | NodemailerBindings, 210 | } from 'loopback4-notifications/nodemailer'; 211 | 212 | export class NotificationServiceApplication extends BootMixin( 213 | ServiceMixin(RepositoryMixin(RestApplication)), 214 | ) { 215 | constructor(options: ApplicationConfig = {}) { 216 | // ... 217 | 218 | this.component(NotificationsComponent); 219 | this.bind(NodemailerBindings.Config).to({ 220 | pool: true, 221 | maxConnections: 100, 222 | url: '', 223 | host: 'smtp.example.com', 224 | port: 80, 225 | secure: false, 226 | auth: { 227 | user: 'username', 228 | pass: 'password', 229 | }, 230 | tls: { 231 | rejectUnauthorized: true, 232 | }, 233 | }); 234 | this.bind(NotificationBindings.EmailProvider).toProvider( 235 | NodemailerProvider, 236 | ); 237 | // ... 238 | } 239 | } 240 | ``` 241 | 242 | All the configurations as specified by Nodemailer docs for SMTP transport [here](https://nodemailer.com/smtp/) are supported in above NodemailerBindings.Config key. 243 | 244 | In addition to this, some general configurations can also be done, like below. 245 | 246 | ```ts 247 | import { 248 | NotificationsComponent, 249 | NotificationBindings, 250 | } from 'loopback4-notifications'; 251 | import { 252 | NodemailerProvider, 253 | NodemailerBindings, 254 | } from 'loopback4-notifications/nodemailer'; 255 | 256 | export class NotificationServiceApplication extends BootMixin( 257 | ServiceMixin(RepositoryMixin(RestApplication)), 258 | ) { 259 | constructor(options: ApplicationConfig = {}) { 260 | // ... 261 | 262 | this.component(NotificationsComponent); 263 | this.bind(NotificationBindings.Config).to({ 264 | sendToMultipleReceivers: false, 265 | senderEmail: 'support@myapp.com', 266 | }); 267 | this.bind(NodemailerBindings.Config).to({ 268 | pool: true, 269 | maxConnections: 100, 270 | url: '', 271 | host: 'smtp.example.com', 272 | port: 80, 273 | secure: false, 274 | auth: { 275 | user: 'username', 276 | pass: 'password', 277 | }, 278 | tls: { 279 | rejectUnauthorized: true, 280 | }, 281 | }); 282 | this.bind(NotificationBindings.EmailProvider).toProvider( 283 | NodemailerProvider, 284 | ); 285 | // ... 286 | } 287 | } 288 | ``` 289 | 290 | Possible configuration options for the above are mentioned below. 291 | 292 | | Option | Type | Description | 293 | | ----------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 294 | | sendToMultipleReceivers | boolean | If set to true, single email will be sent to all receivers mentioned in payload. If set to false, multiple emails will be sent for each receiver mentioned in payload. | 295 | | senderEmail | string | This will be used as from email header in sent email. | 296 | 297 | If you wish to use any other service provider of your choice, you can create a provider for the same, similar to NodemailerProvider we have. Add that provider in place of NodemailerProvider. Refer to the implementation [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/src/providers/email/nodemailer/). 298 | 299 | ```ts 300 | this.bind(NotificationBindings.EmailProvider).toProvider(MyOwnProvider); 301 | ``` 302 | 303 | ### SMS Notifications using AWS SNS 304 | 305 | This extension provides in-built support of AWS Simple Notification Service integration for sending SMS from the application. In order to use it, run `npm install aws-sdk`, and then bind the SnsProvider as below in `application.ts`. 306 | 307 | ```ts 308 | import { 309 | NotificationsComponent, 310 | NotificationBindings, 311 | } from 'loopback4-notifications'; 312 | import {SnsProvider} from 'loopback4-notification/sns'; 313 | // ... 314 | 315 | export class NotificationServiceApplication extends BootMixin( 316 | ServiceMixin(RepositoryMixin(RestApplication)), 317 | ) { 318 | constructor(options: ApplicationConfig = {}) { 319 | // ... 320 | 321 | this.component(NotificationsComponent); 322 | this.bind(NotificationBindings.SMSProvider).toProvider(SnsProvider); 323 | // ... 324 | } 325 | } 326 | ``` 327 | 328 | There are some additional configurations needed in order to allow SNS to connect to AWS. You need to add them as below. Make sure these are added before the provider binding. 329 | 330 | ```ts 331 | import { 332 | NotificationsComponent, 333 | NotificationBindings, 334 | } from 'loopback4-notifications'; 335 | import {SNSBindings, SnsProvider} from 'loopback4-notification/sns'; 336 | 337 | export class NotificationServiceApplication extends BootMixin( 338 | ServiceMixin(RepositoryMixin(RestApplication)), 339 | ) { 340 | constructor(options: ApplicationConfig = {}) { 341 | // ... 342 | 343 | this.component(NotificationsComponent); 344 | this.bind(SNSBindings.Config).to({ 345 | accessKeyId: process.env.SNS_ACCESS_KEY_ID, 346 | secretAccessKey: process.env.SNS_SECRET_ACCESS_KEY, 347 | region: process.env.SNS_REGION, 348 | }); 349 | this.bind(NotificationBindings.SMSProvider).toProvider(SnsProvider); 350 | // ... 351 | } 352 | } 353 | ``` 354 | 355 | All the configurations as specified by AWS docs [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SNS.html#constructor-property) are supported in above SNSBindings.Config key. 356 | 357 | If you wish to use any other service provider of your choice, you can create a provider for the same, similar to SnsProvider we have. Add that provider in place of SnsProvider.Refer to the implementation [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/src/providers/sms/sns/). 358 | 359 | ```ts 360 | this.bind(NotificationBindings.SMSProvider).toProvider(MyOwnProvider); 361 | ``` 362 | 363 | ### SMS / Whatsapp Notifications using Twilio 364 | 365 | This extension provides in-built support of Twilio integration for sending SMS / whatsapp notifications from the application. In order to use it, run `npm install twilio`, and then bind the TwilioProvider as below in application.ts. 366 | 367 | ```ts 368 | import { 369 | NotificationsComponent, 370 | NotificationBindings, 371 | } from 'loopback4-notifications'; 372 | import { 373 | TwilioProvider 374 | } from 'loopback4-notification/twilio'; 375 | .... 376 | 377 | export class NotificationServiceApplication extends BootMixin( 378 | ServiceMixin(RepositoryMixin(RestApplication)), 379 | ) { 380 | constructor(options: ApplicationConfig = {}) { 381 | .... 382 | 383 | this.component(NotificationsComponent); 384 | this.bind(NotificationBindings.SMSProvider).toProvider(TwilioProvider); 385 | .... 386 | } 387 | } 388 | ``` 389 | 390 | There are some additional configurations needed in order to connect to Twilio. You need to add them as below. Make sure these are added before the provider binding. 391 | 392 | ```ts 393 | import { 394 | NotificationsComponent, 395 | NotificationBindings, 396 | } from 'loopback4-notifications'; 397 | import { 398 | TwilioBindings, 399 | TwilioProvider 400 | } from 'loopback4-notification/twilio'; 401 | .... 402 | 403 | export class NotificationServiceApplication extends BootMixin( 404 | ServiceMixin(RepositoryMixin(RestApplication)), 405 | ) { 406 | constructor(options: ApplicationConfig = {}) { 407 | .... 408 | 409 | this.component(NotificationsComponent); 410 | this.bind(TwilioBindings.Config).to({ 411 | accountSid: process.env.TWILIO_ACCOUNT_SID, 412 | authToken: process.env.TWILIO_AUTH_TOKEN, 413 | waFrom: process.env.TWILIO_WA_FROM, 414 | smsFrom: process.env.TWILIO_SMS_FROM, 415 | waStatusCallback:process.env.TWILIO_WA_STATUS_CALLBACK, 416 | smsStatusCallback:process.env.TWILIO_SMS_STATUS_CALLBACK, 417 | }); 418 | this.bind(NotificationBindings.SMSProvider).toProvider(TwilioProvider); 419 | .... 420 | } 421 | } 422 | ``` 423 | 424 | All the configurations as specified by Twilio docs and console are supported in above TwilioBindings Config key. smsFrom could be messaging service id, twilio number or short code. waFrom could be whats app number or number associated to channel. 425 | 426 | ### Push Notifications with Pubnub 427 | 428 | This extension provides in-built support of Pubnub integration for sending realtime push notifications from the application. In order to use it, run `npm install pubnub`, and then bind the PushProvider as below in `application.ts`. 429 | 430 | ```ts 431 | import { 432 | NotificationsComponent, 433 | NotificationBindings, 434 | } from 'loopback4-notifications'; 435 | import {PubNubProvider} from 'loopback4-notifications/pubnub'; 436 | 437 | export class NotificationServiceApplication extends BootMixin( 438 | ServiceMixin(RepositoryMixin(RestApplication)), 439 | ) { 440 | constructor(options: ApplicationConfig = {}) { 441 | // ... 442 | 443 | this.component(NotificationsComponent); 444 | this.bind(NotificationBindings.PushProvider).toProvider(PubNubProvider); 445 | // ... 446 | } 447 | } 448 | ``` 449 | 450 | There are some additional configurations needed in order to allow Pubnub connection. You need to add them as below. Make sure these are added before the provider binding. 451 | 452 | ```ts 453 | import { 454 | NotificationsComponent, 455 | NotificationBindings, 456 | } from 'loopback4-notifications'; 457 | import {PubnubBindings, PubNubProvider} from 'loopback4-notifications/pubnub'; 458 | 459 | export class NotificationServiceApplication extends BootMixin( 460 | ServiceMixin(RepositoryMixin(RestApplication)), 461 | ) { 462 | constructor(options: ApplicationConfig = {}) { 463 | // ... 464 | 465 | this.component(NotificationsComponent); 466 | this.bind(PubNubProvider.Config).to({ 467 | subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY, 468 | publishKey: process.env.PUBNUB_PUBLISH_KEY, 469 | secretKey: process.env.PUBNUB_SECRET_KEY, 470 | ssl: true, 471 | logVerbosity: true, 472 | uuid: 'my-app', 473 | cipherKey: process.env.PUBNUB_CIPHER_KEY, 474 | apns2Env: 'production', 475 | apns2BundleId: 'com.app.myapp', 476 | }); 477 | this.bind(NotificationBindings.PushProvider).toProvider(PubNubProvider); 478 | // ... 479 | } 480 | } 481 | ``` 482 | 483 | All the configurations as specified by Pubnub docs [here](https://www.pubnub.com/docs/web-javascript/api-reference-configuration) are supported in above PubNubProvider.Config key. 484 | 485 | Additionally, PubNubProvider also supports Pubnub Access Manager integration. Refer [docs](https://www.pubnub.com/docs/platform/security/access-control#overview) here for details. 486 | 487 | For PAM support, PubNubProvider exposes two more methods - grantAccess and revokeAccess. These can be used to grant auth tokens and revoke them from Pubnub. 488 | 489 | If you wish to use any other service provider of your choice, you can create a provider for the same, similar to PubNubProvider we have. Add that provider in place of PubNubProvider. Refer to the implementation [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/src/providers/push/pubnub/). 490 | 491 | ```ts 492 | this.bind(NotificationBindings.PushProvider).toProvider(MyOwnProvider); 493 | ``` 494 | 495 | ### Push Notifications With Socket.io 496 | 497 | This extension provides in-built support of Socket.io integration for sending realtime notifications from the application. In order to use it, run `npm install socket.io-client`, and bind the PushProvider as below in `application.ts`. 498 | 499 | This provider sends the message to the channel passed via config (or while publishing) and accepts a fix interface to interact with. 500 | The interface could be imported into the project by the name SocketMessage. 501 | 502 | ```ts 503 | import { 504 | NotificationsComponent, 505 | NotificationBindings, 506 | } from 'loopback4-notifications'; 507 | import {SocketIOProvider} from 'loopback4-notifications/socketio'; 508 | 509 | export class NotificationServiceApplication extends BootMixin( 510 | ServiceMixin(RepositoryMixin(RestApplication)), 511 | ) { 512 | constructor(options: ApplicationConfig = {}) { 513 | // ... 514 | 515 | this.component(NotificationsComponent); 516 | this.bind(NotificationBindings.PushProvider).toProvider(SocketIOProvider); 517 | // ... 518 | } 519 | } 520 | ``` 521 | 522 | There are some additional configurations needed in order to allow Socket connection. You need to add them as below. Make sure these are added before the provider binding. 523 | 524 | ```ts 525 | import { 526 | NotificationsComponent, 527 | NotificationBindings, 528 | } from 'loopback4-notifications'; 529 | import { 530 | SocketBindings, 531 | SocketIOProvider, 532 | } from 'loopback4-notifications/socketio'; 533 | 534 | export class NotificationServiceApplication extends BootMixin( 535 | ServiceMixin(RepositoryMixin(RestApplication)), 536 | ) { 537 | constructor(options: ApplicationConfig = {}) { 538 | // ... 539 | 540 | this.component(NotificationsComponent); 541 | this.bind(SocketBindings.Config).to({ 542 | url: process.env.SOCKETIO_SERVER_URL, 543 | }); 544 | this.bind(NotificationBindings.PushProvider).toProvider(SocketIOProvider); 545 | // ... 546 | } 547 | } 548 | ``` 549 | 550 | If you wish to use any other service provider of your choice, you can create a provider for the same, similar to SocketIOProvider we have. Add that provider in place of SocketIOProvider. Refer to the implementation [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/src/providers/push/socketio/). 551 | 552 | ```ts 553 | this.bind(NotificationBindings.PushProvider).toProvider(MyOwnProvider); 554 | ``` 555 | 556 | ### Push Notifications With FCM 557 | 558 | This extension provides in-built support of Firebase Cloud Messaging integration for sending realtime push notifications from the application. In order to use it, run `npm i firebase-admin`, and then bind the PushProvider as below in `application.ts`. 559 | 560 | ```ts 561 | import { 562 | NotificationsComponent, 563 | NotificationBindings, 564 | } from 'loopback4-notifications'; 565 | import {FcmProvider} from 'loopback4-notifications/fcm'; 566 | export class MyApplication extends BootMixin( 567 | ServiceMixin(RepositoryMixin(RestApplication)), 568 | ) { 569 | constructor(options: ApplicationConfig = {}) { 570 | // ... 571 | this.component(NotificationsComponent); 572 | this.bind(NotificationBindings.PushProvider).toProvider(FcmProvider); 573 | // ... 574 | } 575 | } 576 | ``` 577 | 578 | There are some additional configurations needed in order to use Firebase Cloud Messaging. You need to add them as below. Make sure these are added before the provider binding. 579 | 580 | ```ts 581 | import { 582 | NotificationsComponent, 583 | NotificationBindings, 584 | } from 'loopback4-notifications'; 585 | import {FcmProvider, FcmBindings} from 'loopback4-notifications/fcm'; 586 | export class MyApplication extends BootMixin( 587 | ServiceMixin(RepositoryMixin(RestApplication)), 588 | ) { 589 | constructor(options: ApplicationConfig = {}) { 590 | // ... 591 | this.component(NotificationsComponent); 592 | this.bind(FcmBindings.Config).to({ 593 | apiKey: 'API_KEY', 594 | authDomain: 'PROJECT_ID.firebaseapp.com', 595 | // The value of `databaseURL` depends on the location of the database 596 | databaseURL: 'https://DATABASE_NAME.firebaseio.com', 597 | projectId: 'PROJECT_ID', 598 | storageBucket: 'PROJECT_ID.appspot.com', 599 | messagingSenderId: 'SENDER_ID', 600 | appId: 'APP_ID', 601 | // For Firebase JavaScript SDK v7.20.0 and later, `measurementId` is an optional field 602 | measurementId: 'G-MEASUREMENT_ID', 603 | }); 604 | this.bind(NotificationBindings.PushProvider).toProvider(FcmProvider); 605 | // ... 606 | } 607 | } 608 | ``` 609 | 610 | If you wish to use any other service provider of your choice, you can create a provider for the same, similar to FcmProvider we have. Add that provider in place of FcmProvider. Refer to the implementation [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/src/providers/push/fcm/). 611 | 612 | ```ts 613 | this.bind(NotificationBindings.PushProvider).toProvider(MyOwnProvider); 614 | ``` 615 | 616 | ### Push Notifications With APNs 617 | 618 | This extension provides in-built support of Apple Push Notification service for sending notification to applications installed on Apple devices. In order to use it bind the PushProvider as below in `application.ts`. 619 | 620 | ```ts 621 | import { 622 | NotificationsComponent, 623 | NotificationBindings, 624 | } from 'loopback4-notifications'; 625 | import {ApnsProvider} from 'loopback4-notifications/apns'; 626 | export class MyApplication extends BootMixin( 627 | ServiceMixin(RepositoryMixin(RestApplication)), 628 | ) { 629 | constructor(options: ApplicationConfig = {}) { 630 | // ... 631 | this.component(NotificationsComponent); 632 | this.bind(NotificationBindings.PushProvider).toProvider(ApnsProvider); 633 | // ... 634 | } 635 | } 636 | ``` 637 | 638 | There are some additional configurations needed in order to use Apple Push Notification service. You need to add them as below. Make sure these are added before the provider binding. 639 | 640 | ```ts 641 | import { 642 | NotificationsComponent, 643 | NotificationBindings, 644 | } from 'loopback4-notifications'; 645 | import {ApnsProvider, ApnsBinding} from 'loopback4-notifications/apns'; 646 | export class MyApplication extends BootMixin( 647 | ServiceMixin(RepositoryMixin(RestApplication)), 648 | ) { 649 | constructor(options: ApplicationConfig = {}) { 650 | // ... 651 | this.component(NotificationsComponent); 652 | this.bind(ApnsBinding.Config).to({ 653 | providerOptions: { 654 | /* APNs Connection options, see below. */ 655 | }; 656 | options: { 657 | badge: 1, // optional 658 | topic: "string" 659 | }; 660 | }); 661 | this.bind(NotificationBindings.PushProvider).toProvider(ApnsProvider); 662 | // ... 663 | } 664 | } 665 | ``` 666 | 667 | For more information about `providerOptions` check: [provider documentation](https://github.com/parse-community/node-apn/blob/master/doc/provider.markdown#apnprovideroptions) 668 | 669 | If you wish to use any other service provider of your choice, you can create a provider for the same, similar to ApnsProvider we have. Add that provider in place of ApnsProvider. Refer to the implementation [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/src/providers/push/apns/). 670 | 671 | ```ts 672 | this.bind(NotificationBindings.PushProvider).toProvider(MyOwnProvider); 673 | ``` 674 | 675 | ### Controller Usage 676 | 677 | Once the providers are set, the implementation of notification is very easy. Just add an entity implementing the Message interface provided by the component. For specific type, you can also implement specific interfaces like, SMSMessage, PushMessage, EmailMessage. See example below. 678 | 679 | ```ts 680 | import {Entity, model, property} from '@loopback/repository'; 681 | import { 682 | Message, 683 | Receiver, 684 | MessageType, 685 | MessageOptions, 686 | } from 'loopback4-notifications'; 687 | 688 | @model({ 689 | name: 'notifications', 690 | }) 691 | export class Notification extends Entity implements Message { 692 | @property({ 693 | type: 'string', 694 | id: true, 695 | }) 696 | id?: string; 697 | 698 | @property({ 699 | type: 'string', 700 | jsonSchema: { 701 | nullable: true, 702 | }, 703 | }) 704 | subject?: string; 705 | 706 | @property({ 707 | type: 'string', 708 | required: true, 709 | }) 710 | body: string; 711 | 712 | @property({ 713 | type: 'object', 714 | required: true, 715 | }) 716 | receiver: Receiver; 717 | 718 | @property({ 719 | type: 'number', 720 | required: true, 721 | }) 722 | type: MessageType; 723 | 724 | @property({ 725 | type: 'date', 726 | name: 'sent', 727 | }) 728 | sentDate: Date; 729 | 730 | @property({ 731 | type: 'object', 732 | }) 733 | options?: MessageOptions; 734 | 735 | constructor(data?: Partial) { 736 | super(data); 737 | } 738 | } 739 | ``` 740 | 741 | After this, you can publish notification from controller API methods as below. You don't need to invoke different methods for different notification. Same publish method will take care of it based on message type sent in request body. 742 | 743 | ```ts 744 | export class NotificationController { 745 | constructor( 746 | // ... 747 | @inject(NotificationBindings.NotificationProvider) 748 | private readonly notifProvider: INotification, 749 | ) {} 750 | 751 | @post('/notifications', { 752 | responses: { 753 | [STATUS_CODE.OK]: { 754 | description: 'Notification model instance', 755 | content: { 756 | [CONTENT_TYPE.JSON]: {schema: getModelSchemaRef(Notification)}, 757 | }, 758 | }, 759 | }, 760 | }) 761 | async create( 762 | @requestBody({ 763 | content: { 764 | [CONTENT_TYPE.JSON]: { 765 | schema: getModelSchemaRef(Notification, {exclude: ['id']}), 766 | }, 767 | }, 768 | }) 769 | notification: Omit, 770 | ): Promise { 771 | await this.notifProvider.publish(notification); 772 | } 773 | } 774 | ``` 775 | 776 | As you can see above, one controller method can now cater to all the different type of notifications. 777 | 778 | ## Feedback 779 | 780 | If you've noticed a bug or have a question or have a feature request, [search the issue tracker](https://github.com/sourcefuse/loopback4-notifications/issues) to see if someone else in the community has already created a ticket. 781 | If not, go ahead and [make one](https://github.com/sourcefuse/loopback4-notifications/issues/new/choose)! 782 | All feature requests are welcome. Implementation time may vary. Feel free to contribute the same, if you can. 783 | If you think this extension is useful, please [star](https://help.github.com/en/articles/about-stars) it. Appreciation really helps in keeping this project alive. 784 | 785 | ## Contributing 786 | 787 | Please read [CONTRIBUTING.md](https://github.com/sourcefuse/loopback4-notifications/blob/master/.github/CONTRIBUTING.md) for details on the process for submitting pull requests to us. 788 | 789 | ## Code of conduct 790 | 791 | Code of conduct guidelines [here](https://github.com/sourcefuse/loopback4-notifications/blob/master/.github/CODE_OF_CONDUCT.md). 792 | 793 | ## License 794 | 795 | [MIT](https://github.com/sourcefuse/loopback4-notifications/blob/master/LICENSE) 796 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: loopback4-notifications 5 | annotations: 6 | github.com/project-slug: sourcefuse/loopback4-notifications 7 | backstage.io/techdocs-ref: dir:. 8 | namespace: arc 9 | description: LoopBack 4 extension for setting up various notification mechanisms in loopback4 application, vis-a-vis, Push notification, SMS notification, Email notification. 10 | tags: 11 | - notifications 12 | - email 13 | - sms 14 | - loopback 15 | - extension 16 | links: 17 | - url: https://npmjs.com/package/loopback4-notifications 18 | title: NPM Package 19 | - url: https://loopback.io/doc/en/lb4/Extending-LoopBack-4.html#overview 20 | title: Extending LoopBack 21 | spec: 22 | type: component 23 | lifecycle: production 24 | owner: sourcefuse 25 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'header-max-length': [2, 'always', 100], 5 | 'body-leading-blank': [2, 'always'], 6 | 'footer-leading-blank': [0, 'always'], 7 | 'references-empty': [2, 'never'], 8 | 'body-empty': [2, 'never'], 9 | }, 10 | parserPreset: { 11 | parserOpts: { 12 | issuePrefixes: ['GH-'], 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: loopback4-notifications 2 | site_description: loopback4-notifications 3 | 4 | plugins: 5 | - techdocs-core 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback4-notifications", 3 | "version": "9.0.1", 4 | "description": "An extension for setting up various notification mechanisms in loopback4 application, vis-a-vis, Push notification, SMS notification, Email notification", 5 | "keywords": [ 6 | "loopback-extension", 7 | "loopback" 8 | ], 9 | "main": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | ".": "./dist/index.js", 13 | "./nodemailer": { 14 | "type": "./dist/providers/email/nodemailer/index.d.ts", 15 | "default": "./dist/providers/email/nodemailer/index.js" 16 | }, 17 | "./ses": { 18 | "type": "./dist/providers/email/ses/index.d.ts", 19 | "default": "./dist/providers/email/ses/index.js" 20 | }, 21 | "./apns": { 22 | "type": "./dist/providers/push/apns/index.d.ts", 23 | "default": "./dist/providers/push/apns/index.js" 24 | }, 25 | "./fcm": { 26 | "type": "./dist/providers/push/fcm/index.d.ts", 27 | "default": "./dist/providers/push/fcm/index.js" 28 | }, 29 | "./pubnub": { 30 | "type": "./dist/providers/push/pubnub/index.d.ts", 31 | "default": "./dist/providers/push/pubnub/index.js" 32 | }, 33 | "./socketio": { 34 | "type": "./dist/providers/push/socketio/index.d.ts", 35 | "default": "./dist/providers/push/socketio/index.js" 36 | }, 37 | "./sns": { 38 | "type": "./dist/providers/sms/sns/index.d.ts", 39 | "default": "./dist/providers/sms/sns/index.js" 40 | }, 41 | "./twilio": { 42 | "type": "./dist/providers/sms/twilio/index.d.ts", 43 | "default": "./dist/providers/sms/twilio/index.js" 44 | } 45 | }, 46 | "typesVersions": { 47 | "*": { 48 | "nodemailer": [ 49 | "./dist/providers/email/nodemailer/index.d.ts" 50 | ], 51 | "ses": [ 52 | "./dist/providers/email/ses/index.d.ts" 53 | ], 54 | "apns": [ 55 | "./dist/providers/push/apns/index.d.ts" 56 | ], 57 | "fcm": [ 58 | "./dist/providers/push/fcm/index.d.ts" 59 | ], 60 | "pubnub": [ 61 | "./dist/providers/push/pubnub/index.d.ts" 62 | ], 63 | "socketio": [ 64 | "./dist/providers/push/socketio/index.d.ts" 65 | ], 66 | "sns": [ 67 | "./dist/providers/sms/sns/index.d.ts" 68 | ], 69 | "twilio": [ 70 | "./dist/providers/sms/twilio/index.d.ts" 71 | ] 72 | } 73 | }, 74 | "engines": { 75 | "node": ">=18" 76 | }, 77 | "scripts": { 78 | "build": "lb-tsc", 79 | "build:watch": "lb-tsc --watch", 80 | "lint": "npm run prettier:check && npm run eslint", 81 | "lint:fix": "npm run eslint:fix && npm run prettier:fix", 82 | "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", 83 | "prettier:check": "npm run prettier:cli -- -l", 84 | "prettier:fix": "npm run prettier:cli -- --write", 85 | "eslint": "lb-eslint --report-unused-disable-directives .", 86 | "eslint:fix": "npm run eslint -- --fix", 87 | "pretest": "npm run clean && npm run build", 88 | "test": "lb-mocha --allow-console-logs \"dist/__tests__\"", 89 | "posttest": "npm run lint", 90 | "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", 91 | "clean": "lb-clean dist *.tsbuildinfo .eslintcache", 92 | "prepublishOnly": "npm run test && npm run lint", 93 | "prepare": "husky install", 94 | "coverage": "nyc npm run test" 95 | }, 96 | "repository": { 97 | "type": "git", 98 | "url": "https://github.com/sourcefuse/loopback4-notifications" 99 | }, 100 | "author": "Sourcefuse", 101 | "license": "MIT", 102 | "files": [ 103 | "README.md", 104 | "dist", 105 | "src", 106 | "!*/__tests__" 107 | ], 108 | "dependencies": { 109 | "@loopback/boot": "^7.0.8", 110 | "@loopback/context": "^7.0.8", 111 | "@loopback/core": "^6.1.5", 112 | "@loopback/rest": "^14.0.8", 113 | "tslib": "^2.0.0" 114 | }, 115 | "devDependencies": { 116 | "@commitlint/cli": "^17.7.1", 117 | "@commitlint/config-conventional": "^17.7.0", 118 | "@loopback/build": "^11.0.7", 119 | "@loopback/eslint-config": "^15.0.4", 120 | "@loopback/testlab": "^7.0.7", 121 | "@parse/node-apn": "^5.1.3", 122 | "@semantic-release/changelog": "^6.0.1", 123 | "@semantic-release/commit-analyzer": "^9.0.2", 124 | "@semantic-release/git": "^10.0.1", 125 | "@semantic-release/npm": "^9.0.1", 126 | "@semantic-release/release-notes-generator": "^10.0.3", 127 | "@types/node": "^16.18.119", 128 | "@types/nodemailer": "^6.4.4", 129 | "@types/proxyquire": "^1.3.28", 130 | "@types/pubnub": "^7.4.2", 131 | "@types/socket.io-client": "^1.4.33", 132 | "aws-sdk": "^2.1360.0", 133 | "commitizen": "^4.2.4", 134 | "cz-conventional-changelog": "^3.3.0", 135 | "cz-customizable": "^6.3.0", 136 | "cz-customizable-ghooks": "^2.0.0", 137 | "eslint": "^8.57.0", 138 | "firebase-admin": "^12.1.1", 139 | "git-release-notes": "^5.0.0", 140 | "husky": "^7.0.4", 141 | "jsdom": "^21.0.0", 142 | "nodemailer": "^6.7.5", 143 | "nyc": "^17.1.0", 144 | "proxyquire": "^2.1.3", 145 | "pubnub": "^8.2.5", 146 | "semantic-release": "^19.0.3", 147 | "simple-git": "^3.15.1", 148 | "socket.io-client": "^4.5.1", 149 | "source-map-support": "^0.5.21", 150 | "typescript": "~5.2.2", 151 | "twilio": "^3.82.0" 152 | }, 153 | "overrides": { 154 | "@parse/node-apn": { 155 | "jsonwebtoken": "9.0.0" 156 | }, 157 | "twilio": { 158 | "jsonwebtoken": "9.0.0", 159 | "axios": "1.7.9" 160 | }, 161 | "body-parser": { 162 | "debug": "^4.3.4" 163 | }, 164 | "express": { 165 | "debug": "^4.3.4", 166 | "finalhandler": "^1.2.0", 167 | "send": "^1.1.0", 168 | "serve-static": "^1.15.0" 169 | }, 170 | "git-release-notes": { 171 | "ejs": "^3.1.8", 172 | "yargs": "^17.6.2" 173 | }, 174 | "@semantic-release/npm": { 175 | "npm": "^9.4.2" 176 | } 177 | }, 178 | "publishConfig": { 179 | "registry": "https://registry.npmjs.org/" 180 | }, 181 | "config": { 182 | "commitizen": { 183 | "path": "./node_modules/cz-customizable" 184 | } 185 | }, 186 | "release": { 187 | "branches": [ 188 | "master" 189 | ], 190 | "plugins": [ 191 | [ 192 | "@semantic-release/commit-analyzer", 193 | { 194 | "preset": "angular", 195 | "releaseRules": [ 196 | { 197 | "type": "chore", 198 | "scope": "deps", 199 | "release": "patch" 200 | } 201 | ] 202 | } 203 | ], 204 | "@semantic-release/release-notes-generator", 205 | "@semantic-release/npm", 206 | [ 207 | "@semantic-release/git", 208 | { 209 | "assets": [ 210 | "package.json", 211 | "CHANGELOG.md" 212 | ], 213 | "message": "chore(release): ${nextRelease.version} semantic" 214 | } 215 | ], 216 | "@semantic-release/github" 217 | ], 218 | "repositoryUrl": "git@github.com:sourcefuse/loopback4-notifications.git" 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/__tests__/acceptance/README.md: -------------------------------------------------------------------------------- 1 | # Acceptance tests 2 | -------------------------------------------------------------------------------- /src/__tests__/integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests 2 | -------------------------------------------------------------------------------- /src/__tests__/mock-sdk.ts: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | 3 | import Pubnub from 'pubnub'; 4 | import twilio, {Twilio} from 'twilio'; 5 | import {TwilioAuthConfig, TwilioMessage} from '../providers'; 6 | import Mail = require('nodemailer/lib/mailer'); 7 | import SMTPTransport = require('nodemailer/lib/smtp-transport'); 8 | 9 | export class MockSES { 10 | constructor(config: AWS.SES.Types.ClientConfiguration) { 11 | /* do nothing */ 12 | } 13 | 14 | async sendEmail(emailReq: AWS.SES.SendEmailRequest) { 15 | /* do nothing */ 16 | } 17 | } 18 | 19 | export class MockSNS { 20 | constructor(config: AWS.SNS.ClientConfiguration) { 21 | /* do nothing */ 22 | } 23 | 24 | async publish(message: AWS.SNS.PublishInput) { 25 | /* do nothing */ 26 | } 27 | } 28 | 29 | export class MockSocketIo { 30 | constructor( 31 | url: string, 32 | options?: { 33 | [key: string]: string; 34 | }, 35 | ) { 36 | /* do nothing */ 37 | } 38 | 39 | async emit(path: string, message: string) { 40 | /* do nothing */ 41 | } 42 | } 43 | 44 | export class MockMail { 45 | constructor(config: SMTPTransport.Options) { 46 | /* do nothing */ 47 | } 48 | 49 | async sendMail(message: Mail.Options) { 50 | /* do nothing */ 51 | } 52 | } 53 | 54 | export class MockPubnub { 55 | constructor(config: Pubnub.PubNubConfiguration) { 56 | /* do nothing */ 57 | } 58 | 59 | grant(grantConfig: Pubnub.PAM.GrantParameters) { 60 | /* do nothing */ 61 | } 62 | async publish(publishConfig: Pubnub.Publish.PublishParameters) { 63 | /* do nothing */ 64 | } 65 | } 66 | 67 | export class MockTwilio { 68 | twilioService: Twilio; 69 | constructor(config: TwilioAuthConfig) { 70 | this.twilioService = twilio(config.accountSid, config.authToken); 71 | } 72 | // sonarignore:start 73 | // this is intensional 74 | async publish(message: TwilioMessage) { 75 | /* do nothing */ 76 | } 77 | // sonarignore:end 78 | } 79 | -------------------------------------------------------------------------------- /src/__tests__/unit/README.md: -------------------------------------------------------------------------------- 1 | # Unit tests 2 | -------------------------------------------------------------------------------- /src/__tests__/unit/apns.provider.unit.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from '@loopback/context'; 2 | import {expect, sinon} from '@loopback/testlab'; 3 | import {ProviderOptions} from '@parse/node-apn'; 4 | import proxyquire from 'proxyquire'; 5 | import {ApnsProvider} from '../../providers'; 6 | import {ApnsMessage} from '../../providers/push/apns/types'; 7 | 8 | describe('Apns Service', () => { 9 | let ApnsMockProvider: Constructor; 10 | beforeEach(setupMockApns); 11 | describe('apns configuration addition', () => { 12 | it('returns error message when no apns config', async () => { 13 | try { 14 | /*eslint-disable @typescript-eslint/no-unused-vars*/ 15 | const apnsProvider = new ApnsProvider(); 16 | } catch (err) { 17 | const result = err.message; 18 | expect(result).which.eql('Apns Config missing !'); 19 | } 20 | }); 21 | it('returns error message on passing reciever length as zero', async () => { 22 | const apnsProvider = new ApnsMockProvider({ 23 | token: { 24 | key: '.', 25 | keyId: 'key-id', 26 | teamId: 'developer-team-id', 27 | }, 28 | debug: true, 29 | production: false, 30 | options: { 31 | topic: 'dummy topic', 32 | }, 33 | }).value(); 34 | 35 | const message: ApnsMessage = { 36 | receiver: { 37 | to: [], 38 | }, 39 | body: 'test', 40 | sentDate: new Date(), 41 | type: 0, 42 | options: {}, 43 | }; 44 | try { 45 | const result = apnsProvider.publish(message); 46 | } catch (err) { 47 | expect(err.message).which.eql( 48 | 'Message receiver, topic not found in request !', 49 | ); 50 | } 51 | }); 52 | it('returns error message on passing reciever length as zero in value function', async () => { 53 | const apnsProvider = new ApnsMockProvider({ 54 | token: { 55 | key: '.', 56 | keyId: 'key-id', 57 | teamId: 'developer-team-id', 58 | }, 59 | debug: true, 60 | production: false, 61 | options: { 62 | topic: 'dummy topic', 63 | }, 64 | }).value(); 65 | const message: ApnsMessage = { 66 | receiver: { 67 | to: [], 68 | }, 69 | body: 'test', 70 | sentDate: new Date(), 71 | type: 0, 72 | options: {}, 73 | }; 74 | try { 75 | const result = apnsProvider.publish(message); 76 | } catch (err) { 77 | expect(err.message).which.eql( 78 | 'Message receiver, topic not found in request !', 79 | ); 80 | } 81 | }); 82 | it('returns error message on having no message subject', async () => { 83 | const apnsProvider = new ApnsMockProvider({ 84 | token: { 85 | key: '.', 86 | keyId: 'key-id', 87 | teamId: 'developer-team-id', 88 | }, 89 | debug: true, 90 | production: false, 91 | options: { 92 | topic: 'dummy topic', 93 | }, 94 | }); 95 | const message: ApnsMessage = { 96 | receiver: { 97 | to: [ 98 | { 99 | id: 'dummy', 100 | type: 0, 101 | }, 102 | ], 103 | }, 104 | body: 'test', 105 | sentDate: new Date(), 106 | type: 0, 107 | options: {}, 108 | }; 109 | try { 110 | const result = apnsProvider.initialValidations(message); 111 | } catch (err) { 112 | expect(err.message).which.eql('Message title not found !'); 113 | } 114 | }); 115 | 116 | it('returns error message on having no message subject using value function', async () => { 117 | const apnsProvider = new ApnsMockProvider({ 118 | token: { 119 | key: '.', 120 | keyId: 'key-id', 121 | teamId: 'developer-team-id', 122 | }, 123 | debug: true, 124 | production: false, 125 | options: { 126 | topic: 'dummy topic', 127 | }, 128 | }).value(); 129 | const message: ApnsMessage = { 130 | receiver: { 131 | to: [ 132 | { 133 | id: 'dummy', 134 | type: 0, 135 | }, 136 | ], 137 | }, 138 | body: 'test', 139 | sentDate: new Date(), 140 | type: 0, 141 | options: {}, 142 | }; 143 | try { 144 | const result = apnsProvider.publish(message); 145 | } catch (err) { 146 | expect(err.message).which.eql('Message title not found !'); 147 | } 148 | }); 149 | it('returns a note object which will sent as a payload', async () => { 150 | const apnsProvider = new ApnsMockProvider({ 151 | token: { 152 | key: '.', 153 | keyId: 'key-id', 154 | teamId: 'developer-team-id', 155 | }, 156 | debug: true, 157 | production: false, 158 | options: { 159 | topic: 'dummy topic', 160 | }, 161 | }); 162 | const message: ApnsMessage = { 163 | receiver: { 164 | to: [ 165 | { 166 | id: 'dummy', 167 | type: 0, 168 | }, 169 | ], 170 | }, 171 | body: 'test', 172 | sentDate: new Date(), 173 | type: 0, 174 | options: {}, 175 | subject: 'test', 176 | }; 177 | 178 | const result = apnsProvider.getMainNote(message); 179 | expect(result).to.have.Object(); 180 | }).timeout(5000); 181 | }); 182 | it('returns promise of response', async () => { 183 | const apnsProvider = new ApnsMockProvider({ 184 | token: { 185 | key: '.', 186 | keyId: 'key-id', 187 | teamId: 'developer-team-id', 188 | }, 189 | debug: true, 190 | production: false, 191 | options: { 192 | topic: 'dummy topic', 193 | }, 194 | }); 195 | const message: ApnsMessage = { 196 | receiver: { 197 | to: [ 198 | { 199 | id: 'dummy', 200 | type: 0, 201 | }, 202 | ], 203 | }, 204 | body: 'test', 205 | sentDate: new Date(), 206 | type: 0, 207 | options: {}, 208 | subject: 'test', 209 | }; 210 | const result = apnsProvider.sendingPushToReceiverTokens(message); 211 | expect(result).to.have.Promise(); 212 | }).timeout(5000); 213 | function setupMockApns() { 214 | const MockApns = sinon.stub(); 215 | MockApns.prototype.apns = sinon.stub().returns(true); 216 | MockApns.prototype.apns.prototype.Provider = sinon.stub().returns(true); 217 | /* eslint-disable */ 218 | ApnsMockProvider = proxyquire('../../providers/push/apns/apns.provider', { 219 | '@parse/node-apn': { 220 | Provider: function (config: ProviderOptions) { 221 | return {}; 222 | }, 223 | Notification: function () { 224 | return { 225 | expiry: 0, 226 | badge: 0, 227 | alert: 'dummy alert', 228 | payload: {}, 229 | topic: 'dummy topic', 230 | }; 231 | }, 232 | }, 233 | }).ApnsProvider; 234 | /* eslint-enable */ 235 | } 236 | }); 237 | -------------------------------------------------------------------------------- /src/__tests__/unit/fcm.provider.unit.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-shadow */ 2 | import {expect} from '@loopback/testlab'; 3 | import * as admin from 'firebase-admin'; 4 | import {FcmMessage, FcmProvider} from '../../providers'; 5 | 6 | describe('FCM Service', () => { 7 | const app = admin.initializeApp(); 8 | const fcmProvider = new FcmProvider(app); 9 | 10 | describe('fcm configration addition', () => { 11 | it('returns error message when no firebase config', () => { 12 | try { 13 | /* eslint-disable @typescript-eslint/no-unused-vars */ 14 | const fcmProvider = new FcmProvider(); 15 | } catch (err) { 16 | const result = err.message; 17 | expect(result).which.eql('Firebase Config missing !'); 18 | } 19 | }); 20 | 21 | it('returns error message on passing reciever length as zero', () => { 22 | const message: FcmMessage = { 23 | receiver: { 24 | to: [], 25 | }, 26 | body: 'test', 27 | sentDate: new Date(), 28 | type: 0, 29 | options: {}, 30 | }; 31 | try { 32 | const result = fcmProvider.initialValidations(message); 33 | } catch (err) { 34 | expect(err.message).which.eql( 35 | 'Message receiver, topic or condition not found in request !', 36 | ); 37 | } 38 | }); 39 | 40 | it('returns error message on passing reciever length as zero in value function', () => { 41 | const message: FcmMessage = { 42 | receiver: { 43 | to: [], 44 | }, 45 | body: 'test', 46 | sentDate: new Date(), 47 | type: 0, 48 | options: {}, 49 | }; 50 | try { 51 | const result = fcmProvider.value().publish(message); 52 | } catch (err) { 53 | expect(err.message).which.eql( 54 | 'Message receiver, topic or condition not found in request !', 55 | ); 56 | } 57 | }); 58 | 59 | it('returns error message on having no message subject', () => { 60 | const message: FcmMessage = { 61 | receiver: { 62 | to: [ 63 | { 64 | id: 'dummy', 65 | type: 0, 66 | }, 67 | ], 68 | }, 69 | body: 'test', 70 | sentDate: new Date(), 71 | type: 0, 72 | options: {}, 73 | }; 74 | try { 75 | const result = fcmProvider.initialValidations(message); 76 | } catch (err) { 77 | expect(err.message).which.eql('Message title not found !'); 78 | } 79 | }); 80 | 81 | it('returns error message on having no message subject using value function', () => { 82 | const message: FcmMessage = { 83 | receiver: { 84 | to: [ 85 | { 86 | id: 'dummy', 87 | type: 0, 88 | }, 89 | ], 90 | }, 91 | body: 'test', 92 | sentDate: new Date(), 93 | type: 0, 94 | options: {}, 95 | }; 96 | try { 97 | const result = fcmProvider.value().publish(message); 98 | } catch (err) { 99 | expect(err.message).which.eql('Message title not found !'); 100 | } 101 | }); 102 | 103 | it('returns array for sending push to conditions', () => { 104 | const message: FcmMessage = { 105 | receiver: { 106 | to: [ 107 | { 108 | id: 'dummy', 109 | type: 0, 110 | }, 111 | ], 112 | }, 113 | body: 'test', 114 | sentDate: new Date(), 115 | type: 0, 116 | options: {}, 117 | subject: 'test', 118 | }; 119 | 120 | const generalMessageObj = { 121 | notification: { 122 | title: 'test', 123 | body: 'test', 124 | }, 125 | }; 126 | const result = fcmProvider.sendingPushToConditions( 127 | message, 128 | generalMessageObj, 129 | ); 130 | expect(result).which.eql([]); 131 | }).timeout(5000); 132 | 133 | it('returns array for sending push to receive tokens', () => { 134 | const message: FcmMessage = { 135 | receiver: { 136 | to: [ 137 | { 138 | id: 'dummy', 139 | type: 0, 140 | }, 141 | ], 142 | }, 143 | body: 'test', 144 | sentDate: new Date(), 145 | type: 0, 146 | options: {}, 147 | subject: 'test', 148 | }; 149 | 150 | const generalMessageObj = { 151 | notification: { 152 | title: 'test', 153 | body: 'test', 154 | }, 155 | }; 156 | const result = fcmProvider.sendingPushToReceiverTokens( 157 | message, 158 | generalMessageObj, 159 | ); 160 | expect(result).to.have.Array(); 161 | }).timeout(5000); 162 | 163 | it('returns array for sending push to topics', () => { 164 | const message: FcmMessage = { 165 | receiver: { 166 | to: [ 167 | { 168 | id: 'dummy', 169 | type: 0, 170 | }, 171 | ], 172 | }, 173 | body: 'test', 174 | sentDate: new Date(), 175 | type: 0, 176 | options: {}, 177 | subject: 'test', 178 | }; 179 | 180 | const generalMessageObj = { 181 | notification: { 182 | title: 'test', 183 | body: 'test', 184 | }, 185 | }; 186 | const result = fcmProvider.sendingPushToTopics( 187 | message, 188 | generalMessageObj, 189 | ); 190 | expect(result).which.eql([]); 191 | }).timeout(5000); 192 | 193 | it('returns array for sending in value function', () => { 194 | const message: FcmMessage = { 195 | receiver: { 196 | to: [ 197 | { 198 | id: 'dummy', 199 | type: 0, 200 | }, 201 | ], 202 | }, 203 | body: 'test', 204 | sentDate: new Date(), 205 | type: 0, 206 | options: {}, 207 | subject: 'test', 208 | }; 209 | const result = fcmProvider.value().publish(message); 210 | expect(result).to.have.Promise(); 211 | }).timeout(5000); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /src/__tests__/unit/nodemailer.provider.unit.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from '@loopback/core'; 2 | import {expect, sinon} from '@loopback/testlab'; 3 | import proxyquire from 'proxyquire'; 4 | import {NodemailerMessage, NodemailerProvider} from '../../providers'; 5 | 6 | describe('Nodemailer Service', () => { 7 | let NodemailerProviderMock: Constructor; 8 | const nodemailerConfig = { 9 | service: 'test', 10 | url: 'test url', 11 | }; 12 | beforeEach(setupMockNodemailer); 13 | describe('nodemailer configration addition', () => { 14 | it('return error when config is not passed', () => { 15 | try { 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | const nodemailerProvider = new NodemailerProviderMock(); 18 | } catch (err) { 19 | const result = err.message; 20 | expect(result).which.eql('Nodemailer Config missing !'); 21 | } 22 | }); 23 | it('returns error message on having no sender', async () => { 24 | const nodeConfig = { 25 | sendToMultipleReceivers: false, 26 | }; 27 | 28 | const nodemailerProvider = new NodemailerProviderMock( 29 | nodeConfig, 30 | nodemailerConfig, 31 | ).value(); 32 | const message: NodemailerMessage = { 33 | receiver: { 34 | to: [], 35 | }, 36 | body: 'test', 37 | sentDate: new Date(), 38 | type: 0, 39 | }; 40 | const result = await nodemailerProvider 41 | .publish(message) 42 | .catch(err => err.message); 43 | expect(result).which.eql('Message sender not found in request'); 44 | }); 45 | 46 | it('returns error message on passing reciever length as zero', async () => { 47 | const nodeConfig = { 48 | sendToMultipleReceivers: false, 49 | senderEmail: 'test@test.com', 50 | }; 51 | 52 | const nodemailerProvider = new NodemailerProviderMock( 53 | nodeConfig, 54 | nodemailerConfig, 55 | ).value(); 56 | const message: NodemailerMessage = { 57 | receiver: { 58 | to: [], 59 | }, 60 | body: 'test', 61 | sentDate: new Date(), 62 | type: 0, 63 | }; 64 | const result = await nodemailerProvider 65 | .publish(message) 66 | .catch(err => err.message); 67 | expect(result).which.eql('Message receiver not found in request'); 68 | }); 69 | 70 | it('returns error message when message is not complete', async () => { 71 | const nodeConfig = { 72 | sendToMultipleReceivers: false, 73 | senderEmail: 'test@test.com', 74 | }; 75 | 76 | const nodemailerProvider = new NodemailerProviderMock( 77 | nodeConfig, 78 | nodemailerConfig, 79 | ).value(); 80 | const message: NodemailerMessage = { 81 | receiver: { 82 | to: [ 83 | { 84 | id: 'dummy', 85 | }, 86 | ], 87 | }, 88 | body: 'test', 89 | sentDate: new Date(), 90 | type: 0, 91 | }; 92 | const result = await nodemailerProvider 93 | .publish(message) 94 | .catch(err => err.message); 95 | expect(result).which.eql('Message data incomplete'); 96 | }); 97 | 98 | it('returns Promise to be fulfilled for individual users', async () => { 99 | const nodeConfig = { 100 | sendToMultipleReceivers: false, 101 | senderEmail: 'test@test.com', 102 | }; 103 | 104 | const nodemailerProvider = new NodemailerProviderMock( 105 | nodeConfig, 106 | nodemailerConfig, 107 | ).value(); 108 | const message: NodemailerMessage = { 109 | receiver: { 110 | to: [ 111 | { 112 | id: 'dummy', 113 | }, 114 | ], 115 | }, 116 | body: 'test', 117 | sentDate: new Date(), 118 | type: 0, 119 | subject: 'test', 120 | }; 121 | const result = nodemailerProvider.publish(message); 122 | await expect(result).to.be.fulfilled(); 123 | }); 124 | 125 | it('returns Promise to be fulfilled for multiple users', async () => { 126 | const nodeConfig = { 127 | sendToMultipleReceivers: true, 128 | senderEmail: 'test@test.com', 129 | }; 130 | 131 | const nodemailerProvider = new NodemailerProviderMock( 132 | nodeConfig, 133 | nodemailerConfig, 134 | ).value(); 135 | const message: NodemailerMessage = { 136 | receiver: { 137 | to: [ 138 | { 139 | id: 'dummy', 140 | }, 141 | ], 142 | }, 143 | body: 'test', 144 | sentDate: new Date(), 145 | type: 0, 146 | subject: 'test', 147 | }; 148 | const result = nodemailerProvider.publish(message); 149 | await expect(result).to.be.fulfilled(); 150 | }); 151 | }); 152 | 153 | function setupMockNodemailer() { 154 | const mockNodemailer = sinon.stub().returns({ 155 | sendMail: sinon.stub().returns({}), 156 | }); 157 | 158 | NodemailerProviderMock = proxyquire( 159 | '../../providers/email/nodemailer/nodemailer.provider', 160 | { 161 | nodemailer: { 162 | createTransport: mockNodemailer, 163 | }, 164 | }, 165 | ).NodemailerProvider; 166 | } 167 | }); 168 | -------------------------------------------------------------------------------- /src/__tests__/unit/pubnub.provider.unit.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from '@loopback/core'; 2 | import {expect, sinon} from '@loopback/testlab'; 3 | import proxyquire from 'proxyquire'; 4 | import {PubnubConfig, PubNubMessage, PubNubProvider} from '../../providers'; 5 | import {Config} from '../../types'; 6 | 7 | describe('Pubnub Service', () => { 8 | let PubnubProviderMock: Constructor; 9 | const pubnubConfig: PubnubConfig = { 10 | uuid: '1', 11 | subscribeKey: 'test', 12 | }; 13 | beforeEach(setupMockPubnub); 14 | describe('pubnub configration addition', () => { 15 | it('returns error message on passing reciever length as zero', async () => { 16 | const pubnubProvider = new PubnubProviderMock(pubnubConfig).value(); 17 | const message: PubNubMessage = { 18 | receiver: { 19 | to: [], 20 | }, 21 | body: 'test', 22 | sentDate: new Date(), 23 | type: 0, 24 | }; 25 | 26 | const result = await pubnubProvider 27 | .publish(message) 28 | .catch(err => err.message); 29 | expect(result).which.eql('Message receiver not found in request'); 30 | }); 31 | 32 | it('returns a Promise to be fulfilled for publish', async () => { 33 | const pubnubProvider = new PubnubProviderMock(pubnubConfig).value(); 34 | const message: PubNubMessage = { 35 | receiver: { 36 | to: [ 37 | { 38 | type: 0, 39 | id: 'dummyId', 40 | }, 41 | ], 42 | }, 43 | body: 'test', 44 | sentDate: new Date(), 45 | type: 0, 46 | }; 47 | const result = pubnubProvider.publish(message); 48 | await expect(result).to.be.fulfilled(); 49 | }); 50 | 51 | it('returns error message when no pubnub config', async () => { 52 | try { 53 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 54 | const pubnubProvider = new PubnubProviderMock(); 55 | } catch (err) { 56 | const result = err.message; 57 | expect(result).which.eql('Pubnub Config missing !'); 58 | } 59 | }); 60 | 61 | it('returns error for grant access when token or ttl is not sent', async () => { 62 | const pubnubProvider = new PubnubProviderMock(pubnubConfig).value(); 63 | const config: Config = { 64 | receiver: { 65 | to: [ 66 | { 67 | id: 'dummy', 68 | type: 0, 69 | }, 70 | ], 71 | }, 72 | type: 0, 73 | }; 74 | const result = await pubnubProvider 75 | .grantAccess(config) 76 | .catch(err => err.message); 77 | expect(result).which.eql( 78 | 'Authorization token or ttl not found in request', 79 | ); 80 | }); 81 | 82 | it('returns error for revoke access when token is not sent', async () => { 83 | const pubnubProvider = new PubnubProviderMock(pubnubConfig).value(); 84 | const config: Config = { 85 | receiver: { 86 | to: [ 87 | { 88 | id: 'dummy', 89 | type: 0, 90 | }, 91 | ], 92 | }, 93 | type: 0, 94 | }; 95 | const result = await pubnubProvider 96 | .revokeAccess(config) 97 | .catch(err => err.message); 98 | expect(result).which.eql('Authorization token not found in request'); 99 | }); 100 | 101 | it('returns success for revoking the access', async () => { 102 | const pubnubProvider = new PubnubProviderMock(pubnubConfig).value(); 103 | const config: Config = { 104 | receiver: { 105 | to: [ 106 | { 107 | id: 'dummy', 108 | type: 0, 109 | }, 110 | ], 111 | }, 112 | type: 0, 113 | options: { 114 | ['token']: 'dummy', 115 | }, 116 | }; 117 | const result = await pubnubProvider.revokeAccess(config); 118 | expect(result).to.be.eql({success: true}); 119 | }); 120 | 121 | it('returns success for granting the access', async () => { 122 | const pubnubProvider = new PubnubProviderMock(pubnubConfig).value(); 123 | const config: Config = { 124 | receiver: { 125 | to: [ 126 | { 127 | id: 'dummy', 128 | type: 0, 129 | }, 130 | ], 131 | }, 132 | type: 0, 133 | options: { 134 | ['token']: 'dummy', 135 | ['ttl']: 'dummy', 136 | }, 137 | }; 138 | const result = await pubnubProvider.grantAccess(config); 139 | expect(result).to.be.eql({ttl: 'dummy'}); 140 | }); 141 | }); 142 | function setupMockPubnub() { 143 | const mockPubnub = sinon.stub(); 144 | mockPubnub.prototype.publish = sinon.stub().returns(Promise.resolve()); 145 | mockPubnub.prototype.grant = sinon.stub().returns(Promise.resolve()); 146 | PubnubProviderMock = proxyquire( 147 | '../../providers/push/pubnub/pubnub.provider', 148 | { 149 | pubnub: mockPubnub, 150 | }, 151 | ).PubNubProvider; 152 | } 153 | }); 154 | -------------------------------------------------------------------------------- /src/__tests__/unit/ses.provider.unit.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from '@loopback/core'; 2 | import {expect, sinon} from '@loopback/testlab'; 3 | import proxyquire from 'proxyquire'; 4 | import {SESMessage, SesProvider} from '../../providers'; 5 | 6 | describe('Ses Service', () => { 7 | let SesMockProvider: Constructor; 8 | beforeEach(setUpMockSES); 9 | describe('ses configration addition', () => { 10 | const sesConfig = { 11 | accessKeyId: '', 12 | secretAccessKey: '', 13 | region: 'us-east-1', 14 | }; 15 | 16 | it('returns error message on having no sender', async () => { 17 | const Config = { 18 | sendToMultipleReceivers: false, 19 | }; 20 | const sesProvider = new SesMockProvider(Config, sesConfig).value(); 21 | 22 | const message: SESMessage = { 23 | receiver: { 24 | to: [], 25 | }, 26 | body: 'test', 27 | sentDate: new Date(), 28 | type: 0, 29 | }; 30 | const result = await sesProvider 31 | .publish(message) 32 | .catch(err => err.message); 33 | expect(result).which.eql('Message sender not found in request'); 34 | }); 35 | 36 | it('returns error message on passing reciever length as zero', async () => { 37 | const Config = { 38 | sendToMultipleReceivers: false, 39 | senderEmail: 'test@test.com', 40 | }; 41 | 42 | const sesProvider = new SesMockProvider(Config, sesConfig).value(); 43 | const message: SESMessage = { 44 | receiver: { 45 | to: [], 46 | }, 47 | body: 'test', 48 | sentDate: new Date(), 49 | type: 0, 50 | }; 51 | const result = await sesProvider 52 | .publish(message) 53 | .catch(err => err.message); 54 | expect(result).which.eql('Message receiver not found in request'); 55 | }); 56 | 57 | it('returns error message when message is not complete', async () => { 58 | const Config = { 59 | sendToMultipleReceivers: false, 60 | senderEmail: 'test@test.com', 61 | }; 62 | 63 | const sesProvider = new SesMockProvider(Config, sesConfig).value(); 64 | const message: SESMessage = { 65 | receiver: { 66 | to: [ 67 | { 68 | id: 'dummy', 69 | }, 70 | ], 71 | }, 72 | body: 'test', 73 | sentDate: new Date(), 74 | type: 0, 75 | }; 76 | const result = await sesProvider 77 | .publish(message) 78 | .catch(err => err.message); 79 | expect(result).which.eql('Message data incomplete'); 80 | }); 81 | 82 | it('returns error message when no ses config', async () => { 83 | try { 84 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 85 | const sesProvider = new SesMockProvider(); 86 | } catch (err) { 87 | const result = err.message; 88 | expect(result).which.eql('AWS SES Config missing !'); 89 | } 90 | }); 91 | 92 | it('returns a Promise after sending message to individual user', async () => { 93 | const Config = { 94 | sendToMultipleReceivers: false, 95 | senderEmail: 'test@gmail.com', 96 | }; 97 | 98 | const sesProvider = new SesMockProvider(Config, sesConfig).value(); 99 | const message: SESMessage = { 100 | receiver: { 101 | to: [ 102 | { 103 | id: 'dummy', 104 | }, 105 | ], 106 | }, 107 | body: 'test', 108 | sentDate: new Date(), 109 | type: 0, 110 | subject: 'test', 111 | }; 112 | const result = sesProvider.publish(message); 113 | await expect(result).to.be.fulfilled(); 114 | }); 115 | 116 | it('returns a Promise after sending message to multiple user', async () => { 117 | const Config = { 118 | sendToMultipleReceivers: true, 119 | senderEmail: 'test@gmail.com', 120 | }; 121 | const sesProvider = new SesMockProvider(Config, sesConfig).value(); 122 | const message: SESMessage = { 123 | receiver: { 124 | to: [ 125 | { 126 | id: 'dummy', 127 | }, 128 | ], 129 | }, 130 | body: 'test', 131 | sentDate: new Date(), 132 | type: 0, 133 | subject: 'test', 134 | }; 135 | const result = sesProvider.publish(message); 136 | await expect(result).to.be.fulfilled(); 137 | }); 138 | }); 139 | 140 | function setUpMockSES() { 141 | const mockSES = sinon.stub(); 142 | mockSES.prototype.sendEmail = sinon 143 | .stub() 144 | .returns({promise: () => Promise.resolve()}); 145 | SesMockProvider = proxyquire('../../providers/email/ses/ses.provider', { 146 | 'aws-sdk': { 147 | SES: mockSES, 148 | }, 149 | }).SesProvider; 150 | } 151 | }); 152 | -------------------------------------------------------------------------------- /src/__tests__/unit/sns.provider.unit.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from '@loopback/core'; 2 | import {expect, sinon} from '@loopback/testlab'; 3 | import proxyquire from 'proxyquire'; 4 | import {SNSMessage, SnsProvider} from '../../providers'; 5 | 6 | describe('Sns Service', () => { 7 | const message: SNSMessage = { 8 | receiver: { 9 | to: [], 10 | }, 11 | body: 'test', 12 | sentDate: new Date(), 13 | type: 0, 14 | subject: undefined, 15 | }; 16 | const message1: SNSMessage = { 17 | receiver: { 18 | to: [ 19 | { 20 | id: 'dummy', 21 | type: 0, 22 | }, 23 | ], 24 | }, 25 | body: 'test', 26 | sentDate: new Date(), 27 | type: 0, 28 | subject: undefined, 29 | }; 30 | const configration = { 31 | apiVersion: 'test', 32 | accessKeyId: '', 33 | secretAccessKey: '', 34 | region: 'us-east-1', 35 | }; 36 | 37 | let SnsProviderMock: Constructor; 38 | beforeEach(setupMockSNS); 39 | describe('sns configration addition', () => { 40 | it('returns error message on passing reciever length as zero', async () => { 41 | const snsProvider = new SnsProviderMock(configration).value(); 42 | const result = await snsProvider 43 | .publish(message) 44 | .catch(err => err.message); 45 | expect(result).which.eql('Message receiver not found in request'); 46 | }); 47 | 48 | it('returns error message when no sns config', async () => { 49 | try { 50 | /* eslint-disable @typescript-eslint/no-unused-vars */ 51 | const snsProvider = new SnsProvider(); 52 | } catch (err) { 53 | const result = err.message; 54 | expect(result).which.eql('AWS SNS Config missing !'); 55 | } 56 | }); 57 | 58 | it('returns the message', async () => { 59 | const snsProvider = new SnsProviderMock(configration).value(); 60 | const result = snsProvider.publish(message1); 61 | await expect(result).to.be.fulfilled(); 62 | }); 63 | }); 64 | 65 | function setupMockSNS() { 66 | const mockSNS = sinon.stub(); 67 | mockSNS.prototype.publish = sinon 68 | .stub() 69 | .returns({promise: () => Promise.resolve()}); 70 | SnsProviderMock = proxyquire('../../providers/sms/sns/sns.provider', { 71 | 'aws-sdk': { 72 | SNS: mockSNS, 73 | }, 74 | }).SnsProvider; 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /src/__tests__/unit/socketio.provider.unit.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from '@loopback/core'; 2 | import {expect, sinon} from '@loopback/testlab'; 3 | import proxyquire from 'proxyquire'; 4 | import {SocketIOProvider} from '../../providers'; 5 | import {SocketMessage} from '../../providers/push/socketio/types'; 6 | 7 | describe('Socketio Service', () => { 8 | let SocketMockProvider: Constructor; 9 | const configration = { 10 | url: 'dummyurl', 11 | defaultPath: 'default', 12 | options: { 13 | path: 'custompath', 14 | }, 15 | }; 16 | beforeEach(setupMockSocketIo); 17 | describe('socketio configration addition', () => { 18 | it('returns error message when no socketio config', async () => { 19 | try { 20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 21 | const socketioProvider = new SocketMockProvider(); 22 | } catch (err) { 23 | const result = err.message; 24 | expect(result).which.eql('Socket Config missing !'); 25 | } 26 | }); 27 | it('returs error message when receiver not found', async () => { 28 | const socketioProvider = new SocketMockProvider(configration).value(); 29 | const message: SocketMessage = { 30 | body: 'dummy', 31 | sentDate: new Date(), 32 | type: 0, 33 | receiver: { 34 | to: [], 35 | }, 36 | }; 37 | 38 | const result = await socketioProvider 39 | .publish(message) 40 | .catch(err => err.message); 41 | expect(result).which.eql('Message receiver not found'); 42 | }); 43 | it('returns a promise to be fulfilled', async () => { 44 | const message: SocketMessage = { 45 | body: 'dummy', 46 | sentDate: new Date(), 47 | type: 0, 48 | receiver: { 49 | to: [ 50 | { 51 | id: 'dummy', 52 | type: 0, 53 | }, 54 | ], 55 | }, 56 | }; 57 | const socketioProvider = new SocketMockProvider(configration).value(); 58 | const result = socketioProvider.publish(message); 59 | await expect(result).to.be.fulfilled(); 60 | }); 61 | }); 62 | function setupMockSocketIo() { 63 | const mockSocket = sinon.stub().returns({ 64 | emit: sinon.stub().returns({}), 65 | }); 66 | SocketMockProvider = proxyquire( 67 | '../../providers/push/socketio/socketio.provider', 68 | { 69 | 'socket.io-client': mockSocket, 70 | }, 71 | ).SocketIOProvider; 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/__tests__/unit/twilio.provider.unit.ts: -------------------------------------------------------------------------------- 1 | import {Constructor} from '@loopback/core'; 2 | import {expect, sinon} from '@loopback/testlab'; 3 | import proxyquire from 'proxyquire'; 4 | import {TwilioMessage, TwilioProvider} from '../../providers'; 5 | 6 | describe('Twilio Service', () => { 7 | const message: TwilioMessage = { 8 | receiver: { 9 | to: [], 10 | }, 11 | body: 'test', 12 | sentDate: new Date(), 13 | type: 0, 14 | subject: undefined, 15 | }; 16 | const messageText: TwilioMessage = { 17 | receiver: { 18 | to: [ 19 | { 20 | id: 'XXXXXXXXXXX', 21 | type: 1, 22 | }, 23 | ], 24 | }, 25 | body: 'Test SMS Text Notification', 26 | sentDate: new Date(), 27 | type: 2, 28 | subject: undefined, 29 | }; 30 | const messageTextMedia: TwilioMessage = { 31 | receiver: { 32 | to: [ 33 | { 34 | id: 'XXXXXXXXXXX', 35 | type: 1, 36 | }, 37 | ], 38 | }, 39 | body: 'Test SMS Notification with media', 40 | mediaUrl: ['https://demo.twilio.com/owl.png'], 41 | sentDate: new Date(), 42 | type: 2, 43 | subject: undefined, 44 | }; 45 | const messageWhatsApp: TwilioMessage = { 46 | receiver: { 47 | to: [ 48 | { 49 | id: 'XXXXXXXXXXX', 50 | type: 0, 51 | }, 52 | ], 53 | }, 54 | body: 'Test Whatsapp Notification', 55 | sentDate: new Date(), 56 | type: 2, 57 | subject: undefined, 58 | }; 59 | const messageWAMedia: TwilioMessage = { 60 | receiver: { 61 | to: [ 62 | { 63 | id: 'XXXXXXXXXXX', 64 | type: 0, 65 | }, 66 | ], 67 | }, 68 | body: 'Test Whatsapp message with media', 69 | mediaUrl: ['https://demo.twilio.com/owl.png'], 70 | sentDate: new Date(), 71 | type: 2, 72 | subject: undefined, 73 | }; 74 | const configration = { 75 | accountSid: 'ACTSIDDUMMY', 76 | authToken: 'AUTHDUMMY', 77 | waFrom: '', //Ex. whatsapp:+XXXXXXXXXXX 78 | smsFrom: '', 79 | opts: {dummy: true}, //Change dummy value to false when using unit test 80 | }; 81 | 82 | let TwilioProviderMock: Constructor; 83 | beforeEach(setupMockTwilio); 84 | describe('twilio configration addition', () => { 85 | it('returns error message on passing reciever length as zero', async () => { 86 | const twilioProvider = new TwilioProviderMock(configration).value(); 87 | const result = await twilioProvider 88 | .publish(message) 89 | .catch(err => err.message); 90 | expect(result).which.eql('Message receiver not found in request'); 91 | }); 92 | 93 | it('returns error message when no twilio config', async () => { 94 | try { 95 | // NOSONAR 96 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 97 | const twilioProvider = new TwilioProvider(); 98 | } catch (err) { 99 | const result = err.message; 100 | expect(result).which.eql('Twilio Config missing !'); 101 | } 102 | }); 103 | 104 | it('returns the message (SMS text)', async () => { 105 | const twilioProvider = new TwilioProviderMock(configration).value(); 106 | const result = twilioProvider.publish(messageText); 107 | if (configration.opts?.dummy) { 108 | expect(result).to.have.Promise(); 109 | } else { 110 | await expect(result).to.be.fulfilled(); 111 | } 112 | }); 113 | 114 | it('returns the message (SMS with media)', async () => { 115 | const twilioProvider = new TwilioProviderMock(configration).value(); 116 | const result = twilioProvider.publish(messageTextMedia); 117 | if (configration.opts?.dummy) { 118 | expect(result).to.have.Promise(); 119 | } else { 120 | await expect(result).to.be.fulfilled(); 121 | } 122 | }); 123 | 124 | it('returns the message (Whatsapp)', async () => { 125 | const twilioProvider = new TwilioProviderMock(configration).value(); 126 | const result = twilioProvider.publish(messageWhatsApp); 127 | if (configration.opts?.dummy) { 128 | expect(result).to.have.Promise(); 129 | } else { 130 | await expect(result).to.be.fulfilled(); 131 | } 132 | }); 133 | 134 | it('returns the message (Whatsapp with Media)', async () => { 135 | const twilioProvider = new TwilioProviderMock(configration).value(); 136 | const result = twilioProvider.publish(messageWAMedia); 137 | if (configration.opts?.dummy) { 138 | expect(result).to.have.Promise(); 139 | } else { 140 | await expect(result).to.be.fulfilled(); 141 | } 142 | }); 143 | }); 144 | 145 | function setupMockTwilio() { 146 | const mockTwilio = sinon.stub(); 147 | mockTwilio.prototype.publish = sinon.stub().returns(Promise.resolve()); 148 | TwilioProviderMock = proxyquire( 149 | '../../providers/sms/twilio/twilio.provider', 150 | { 151 | 'twilio.twilio': mockTwilio, 152 | }, 153 | ).TwilioProvider; 154 | } 155 | }); 156 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import {Binding, Component, ProviderMap} from '@loopback/core'; 2 | import {NotificationBindings} from './keys'; 3 | import {SESBindings} from './providers/email/ses/keys'; 4 | import {NotificationProvider} from './providers/notification.provider'; 5 | import {ApnsBinding} from './providers/push/apns/keys'; 6 | import {FcmBindings} from './providers/push/fcm/keys'; 7 | import {PubnubBindings} from './providers/push/pubnub/keys'; 8 | import {SocketBindings} from './providers/push/socketio/keys'; 9 | import {SNSBindings} from './providers/sms/sns/keys'; 10 | 11 | export class NotificationsComponent implements Component { 12 | constructor() { 13 | // Intentionally left empty 14 | } 15 | 16 | providers?: ProviderMap = { 17 | [NotificationBindings.NotificationProvider.key]: NotificationProvider, 18 | }; 19 | 20 | bindings?: Binding[] = [ 21 | Binding.bind(NotificationBindings.Config.key).to(null), 22 | Binding.bind(SESBindings.Config.key).to(null), 23 | Binding.bind(SNSBindings.Config.key).to(null), 24 | Binding.bind(PubnubBindings.Config.key).to(null), 25 | Binding.bind(SocketBindings.Config.key).to(null), 26 | Binding.bind(ApnsBinding.Config.key).to(null), 27 | Binding.bind(FcmBindings.Config.key).to(null), 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /src/controllers/README.md: -------------------------------------------------------------------------------- 1 | # Controllers 2 | 3 | This directory contains source files for the controllers exported by this 4 | extension. 5 | 6 | For more information, see . 7 | -------------------------------------------------------------------------------- /src/decorators/README.md: -------------------------------------------------------------------------------- 1 | # Decorators 2 | 3 | ## Overview 4 | 5 | Decorators provide annotations for class methods and arguments. Decorators use 6 | the form `@decorator` where `decorator` is the name of the function that will be 7 | called at runtime. 8 | 9 | ## Basic Usage 10 | 11 | ### txIdFromHeader 12 | 13 | This simple decorator allows you to annotate a `Controller` method argument. The 14 | decorator will annotate the method argument with the value of the header 15 | `X-Transaction-Id` from the request. 16 | 17 | **Example** 18 | 19 | ```ts 20 | class MyController { 21 | @get('/') 22 | getHandler(@txIdFromHeader() txId: string) { 23 | return `Your transaction id is: ${txId}`; 24 | } 25 | } 26 | ``` 27 | 28 | ## Related Resources 29 | 30 | You can check out the following resource to learn more about decorators and how 31 | they are used in LoopBack Next. 32 | 33 | - [TypeScript Handbook: Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) 34 | - [Decorators in LoopBack](http://loopback.io/doc/en/lb4/Decorators.html) 35 | -------------------------------------------------------------------------------- /src/error-keys.ts: -------------------------------------------------------------------------------- 1 | export const enum NotificationError { 2 | ProviderNotFound = 'ProviderNotFound', 3 | } 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component'; 2 | export * from './error-keys'; 3 | export * from './keys'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import {INotification, INotificationConfig} from './types'; 3 | 4 | export namespace NotificationBindings { 5 | export const NotificationProvider = 6 | BindingKey.create('sf.notification'); 7 | export const SMSProvider = BindingKey.create( 8 | 'sf.notification.sms', 9 | ); 10 | export const PushProvider = BindingKey.create( 11 | 'sf.notification.push', 12 | ); 13 | export const EmailProvider = BindingKey.create( 14 | 'sf.notification.email', 15 | ); 16 | 17 | export const Config = BindingKey.create( 18 | 'sf.notification.config', 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/mixins/README.md: -------------------------------------------------------------------------------- 1 | # Mixins 2 | 3 | This directory contains source files for the mixins exported by this extension. 4 | 5 | ## Overview 6 | 7 | Sometimes it's helpful to write partial classes and then combining them together 8 | to build more powerful classes. This pattern is called Mixins (mixing in partial 9 | classes) and is supported by LoopBack 4. 10 | 11 | LoopBack 4 supports mixins at an `Application` level. Your partial class can 12 | then be mixed into the `Application` class. A mixin class can modify or override 13 | existing methods of the class or add new ones! It is also possible to mixin 14 | multiple classes together as needed. 15 | 16 | ### High level example 17 | 18 | ```ts 19 | class MyApplication extends MyMixinClass(Application) { 20 | // Your code 21 | } 22 | 23 | // Multiple Classes mixed together 24 | class MyApp extends MyMixinClass(MyMixinClass2(Application)) { 25 | // Your code 26 | } 27 | ``` 28 | 29 | ## Getting Started 30 | 31 | For hello-extensions we write a simple Mixin that allows the `Application` class 32 | to bind a `Logger` class from ApplicationOptions, Components, or `.logger()` 33 | method that is mixed in. `Logger` instances are bound to the key 34 | `loggers.${Logger.name}`. Once a Logger has been bound, the user can retrieve it 35 | by using 36 | [Dependency Injection](http://loopback.io/doc/en/lb4/Dependency-injection.html) 37 | and the key for the `Logger`. 38 | 39 | ### What is a Logger? 40 | 41 | > A Logger class is provides a mechanism for logging messages of varying 42 | > priority by providing an implementation for `Logger.info()` & 43 | > `Logger.error()`. An example of a Logger is `console` which has 44 | > `console.log()` and `console.error()`. 45 | 46 | #### An example Logger 47 | 48 | ```ts 49 | class ColorLogger implements Logger { 50 | log(...args: LogArgs) { 51 | console.log('log :', ...args); 52 | } 53 | 54 | error(...args: LogArgs) { 55 | // log in red color 56 | console.log('\x1b[31m error: ', ...args, '\x1b[0m'); 57 | } 58 | } 59 | ``` 60 | 61 | ## LoggerMixin 62 | 63 | A complete & functional implementation can be found in `logger.mixin.ts`. _Here 64 | are some key things to keep in mind when writing your own Mixin_. 65 | 66 | ### constructor() 67 | 68 | A Mixin constructor must take an array of any type as it's argument. This would 69 | represent `ApplicationOptions` for our base class `Application` as well as any 70 | properties we would like for our Mixin. 71 | 72 | It is also important for the constructor to call `super(args)` so `Application` 73 | continues to work as expected. 74 | 75 | ```ts 76 | constructor(...args: any[]) { 77 | super(args); 78 | } 79 | ``` 80 | 81 | ### Binding via `ApplicationOptions` 82 | 83 | As mentioned earlier, since our `args` represents `ApplicationOptions`, we can 84 | make it possible for users to pass in their `Logger` implementations in a 85 | `loggers` array on `ApplicationOptions`. We can then read the array and 86 | automatically bind these for the user. 87 | 88 | #### Example user experience 89 | 90 | ```ts 91 | class MyApp extends LoggerMixin(Application) { 92 | constructor(...args: any[]) { 93 | super(...args); 94 | } 95 | } 96 | 97 | const app = new MyApp({ 98 | loggers: [ColorLogger], 99 | }); 100 | ``` 101 | 102 | #### Example Implementation 103 | 104 | To implement this, we would check `this.options` to see if it has a `loggers` 105 | array and if so, bind it by calling the `.logger()` method. (More on that 106 | below). 107 | 108 | ```ts 109 | if (this.options.loggers) { 110 | for (const logger of this.options.loggers) { 111 | this.logger(logger); 112 | } 113 | } 114 | ``` 115 | 116 | ### Binding via `.logger()` 117 | 118 | As mentioned earlier, we can add a new function to our `Application` class 119 | called `.logger()` into which a user would pass in their `Logger` implementation 120 | so we can bind it to the `loggers.*` key for them. We just add this new method 121 | on our partial Mixin class. 122 | 123 | ```ts 124 | logger(logClass: Logger) { 125 | const loggerKey = `loggers.${logClass.name}`; 126 | this.bind(loggerKey).toClass(logClass); 127 | } 128 | ``` 129 | 130 | ### Binding a `Logger` from a `Component` 131 | 132 | Our base class of `Application` already has a method that binds components. We 133 | can modify this method to continue binding a `Component` as usual but also 134 | binding any `Logger` instances provided by that `Component`. When modifying 135 | behavior of an existing method, we can ensure existing behavior by calling the 136 | `super.method()`. In our case the method is `.component()`. 137 | 138 | ```ts 139 | component(component: Constructor) { 140 | super.component(component); // ensures existing behavior from Application 141 | this.mountComponentLoggers(component); 142 | } 143 | ``` 144 | 145 | We have now modified `.component()` to do it's thing and then call our method 146 | `mountComponentLoggers()`. In this method is where we check for `Logger` 147 | implementations declared by the component in a `loggers` array by retrieving the 148 | instance of the `Component`. Then if `loggers` array exists, we bind the 149 | `Logger` instances as normal (by leveraging our `.logger()` method). 150 | 151 | ```ts 152 | mountComponentLoggers(component: Constructor) { 153 | const componentKey = `components.${component.name}`; 154 | const compInstance = this.getSync(componentKey); 155 | 156 | if (compInstance.loggers) { 157 | for (const logger of compInstance.loggers) { 158 | this.logger(logger); 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | ## Retrieving the Logger instance 165 | 166 | Now that we have bound a Logger to our Application via one of the many ways made 167 | possible by `LoggerMixin`, we need to be able to retrieve it so we can use it. 168 | Let's say we want to use it in a controller. Here's an example to retrieving it 169 | so we can use it. 170 | 171 | ```ts 172 | class MyController { 173 | constructor(@inject('loggers.ColorLogger') protected log: Logger) {} 174 | 175 | helloWorld() { 176 | this.log.log('hello log'); 177 | this.log.error('hello error'); 178 | } 179 | } 180 | ``` 181 | 182 | ## Examples for using LoggerMixin 183 | 184 | ### Using the app's `.logger()` method 185 | 186 | ```ts 187 | class LoggingApplication extends LoggerMixin(Application) { 188 | constructor(...args: any[]) { 189 | super(...args); 190 | this.logger(ColorLogger); 191 | } 192 | } 193 | ``` 194 | 195 | ### Using the app's constructor 196 | 197 | ```ts 198 | class LoggerApplication extends LoggerMixin(Application) { 199 | constructor() { 200 | super({ 201 | loggers: [ColorLogger], 202 | }); 203 | } 204 | } 205 | ``` 206 | 207 | ### Binding a Logger provided by a component 208 | 209 | ```ts 210 | class LoggingComponent implements Component { 211 | loggers: [ColorLogger]; 212 | } 213 | 214 | const app = new LoggingApplication(); 215 | app.component(LoggingComponent); // Logger from MyComponent will be bound to loggers.ColorLogger 216 | ``` 217 | -------------------------------------------------------------------------------- /src/providers/README.md: -------------------------------------------------------------------------------- 1 | # Providers 2 | 3 | This directory contains providers contributing additional bindings, for example 4 | custom sequence actions. 5 | 6 | ## Overview 7 | 8 | A [provider](http://loopback.io/doc/en/lb4/Creating-components.html#providers) 9 | is a class that provides a `value()` function. This function is called `Context` 10 | when another entity requests a value to be injected. 11 | 12 | Here we create a provider for a logging function that can be used as a new 13 | action in a custom [sequence](http://loopback.io/doc/en/lb4/Sequence.html). 14 | 15 | The logger will log the URL, the parsed request parameters, and the result. The 16 | logger is also capable of timing the sequence if you start a timer at the start 17 | of the sequence using `this.logger.startTimer()`. 18 | 19 | ## Basic Usage 20 | 21 | ### TimerProvider 22 | 23 | TimerProvider is automatically bound to your Application's 24 | [Context](http://loopback.io/doc/en/lb4/Context.html) using the LogComponent 25 | which exports this provider with a binding key of `extension-starter.timer`. You 26 | can learn more about components in the 27 | [related resources section](#related-resources). 28 | 29 | This provider makes availble to your application a timer function which given a 30 | start time _(given as an array [seconds, nanoseconds])_ can give you a total 31 | time elapsed since the start in milliseconds. The timer can also start timing if 32 | no start time is given. This is used by LogComponent to allow a user to time a 33 | Sequence. 34 | 35 | _NOTE:_ _You can get the start time in the required format by using 36 | `this.logger.startTimer()`._ 37 | 38 | You can provide your own implementation of the elapsed time function by binding 39 | it to the binding key (accessible via `ExtensionStarterBindings`) as follows: 40 | 41 | ```ts 42 | app.bind(ExtensionStarterBindings.TIMER).to(timerFn); 43 | ``` 44 | 45 | ### LogProvider 46 | 47 | LogProvider can automatically be bound to your Application's Context using the 48 | LogComponent which exports the provider with a binding key of 49 | `extension-starter.actions.log`. 50 | 51 | The key can be accessed by importing `ExtensionStarterBindings` as follows: 52 | 53 | **Example: Binding Keys** 54 | 55 | ```ts 56 | import {ExtensionStarterBindings} from 'HelloExtensions'; 57 | // Key can be accessed as follows now 58 | const key = ExtensionStarterBindings.LOG_ACTION; 59 | ``` 60 | 61 | LogProvider gives us a seuqence action and a `startTimer` function. In order to 62 | use the sequence action, you must define your own sequence as shown below. 63 | 64 | **Example: Sequence** 65 | 66 | ```ts 67 | class LogSequence implements SequenceHandler { 68 | constructor( 69 | @inject(coreSequenceActions.FIND_ROUTE) protected findRoute: FindRoute, 70 | @inject(coreSequenceActions.PARSE_PARAMS) 71 | protected parseParams: ParseParams, 72 | @inject(coreSequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, 73 | @inject(coreSequenceActions.SEND) protected send: Send, 74 | @inject(coreSequenceActions.REJECT) protected reject: Reject, 75 | // We get the logger injected by the LogProvider here 76 | @inject(ExtensionStarterBindings.LOG_ACTION) protected logger: LogFn, 77 | ) {} 78 | 79 | async handle(context: RequestContext) { 80 | const {request, response} = context; 81 | 82 | // We define these variable outside so they can be accessed by logger. 83 | let args: any = []; 84 | let result: any; 85 | 86 | // Optionally start timing the sequence using the timer 87 | // function available via LogFn 88 | const start = this.logger.startTimer(); 89 | 90 | try { 91 | const route = this.findRoute(request); 92 | args = await this.parseParams(request, route); 93 | result = await this.invoke(route, args); 94 | this.send(response, result); 95 | } catch (error) { 96 | result = error; // so we can log the error message in the logger 97 | this.reject(context, error); 98 | } 99 | 100 | // We call the logger function given to us by LogProvider 101 | this.logger(request, args, result, start); 102 | } 103 | } 104 | ``` 105 | 106 | Once a sequence has been written, we can just use that in our Application as 107 | follows: 108 | 109 | **Example: Application** 110 | 111 | ```ts 112 | const app = new Application({ 113 | sequence: LogSequence, 114 | }); 115 | app.component(LogComponent); 116 | 117 | // Now all requests handled by our sequence will be logged. 118 | ``` 119 | 120 | ## Related Resources 121 | 122 | You can check out the following resource to learn more about providers, 123 | components, sequences, and binding keys. 124 | 125 | - [Providers](http://loopback.io/doc/en/lb4/Creating-components.html#providers) 126 | - [Creating Components](http://loopback.io/doc/en/lb4/Creating-components.html) 127 | - [Using Components](http://loopback.io/doc/en/lb4/Using-components.html) 128 | - [Sequence](http://loopback.io/doc/en/lb4/Sequence.html) 129 | - [Binding Keys](http://loopback.io/doc/en/lb4/Decorators.html) 130 | -------------------------------------------------------------------------------- /src/providers/email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nodemailer'; 2 | export * from './ses'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/email/nodemailer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './nodemailer.provider'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/email/nodemailer/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import SMTPTransport = require('nodemailer/lib/smtp-transport'); 3 | 4 | export namespace NodemailerBindings { 5 | /** 6 | * A sample config looks like 7 | * pool: true, 8 | * maxConnections: 100, 9 | * url:"", 10 | * host: "smtp.example.com", 11 | * port: 80, 12 | * secure: false, 13 | * auth: { 14 | * user: "username", 15 | * pass: "password" 16 | * }, 17 | * tls: { 18 | * // do not fail on invalid certs 19 | * rejectUnauthorized: true 20 | * } 21 | */ 22 | export const Config = BindingKey.create( 23 | 'sf.notification.config.nodemailer', 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/providers/email/nodemailer/nodemailer.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {HttpErrors} from '@loopback/rest'; 3 | import * as nodemailer from 'nodemailer'; 4 | import {NotificationBindings} from '../../../keys'; 5 | import {INotificationConfig} from '../../../types'; 6 | import {NodemailerBindings} from './keys'; 7 | import {NodemailerMessage, NodemailerNotification} from './types'; 8 | import SMTPTransport = require('nodemailer/lib/smtp-transport'); 9 | import Mail = require('nodemailer/lib/mailer'); 10 | 11 | export class NodemailerProvider implements Provider { 12 | constructor( 13 | @inject(NotificationBindings.Config, { 14 | optional: true, 15 | }) 16 | private readonly config?: INotificationConfig, 17 | @inject(NodemailerBindings.Config, { 18 | optional: true, 19 | }) 20 | private readonly nodemailerConfig?: SMTPTransport.Options, 21 | ) { 22 | if (this.nodemailerConfig) { 23 | this.transporter = nodemailer.createTransport({ 24 | ...this.nodemailerConfig, 25 | }); 26 | } else { 27 | throw new HttpErrors.PreconditionFailed('Nodemailer Config missing !'); 28 | } 29 | } 30 | 31 | transporter: Mail; 32 | 33 | value() { 34 | return { 35 | publish: async (message: NodemailerMessage) => { 36 | const fromEmail = message.options?.from ?? this.config?.senderEmail; 37 | 38 | if (!fromEmail) { 39 | throw new HttpErrors.BadRequest( 40 | 'Message sender not found in request', 41 | ); 42 | } 43 | 44 | if (message.receiver.to.length === 0) { 45 | throw new HttpErrors.BadRequest( 46 | 'Message receiver not found in request', 47 | ); 48 | } 49 | if (!message.subject || !message.body) { 50 | throw new HttpErrors.BadRequest('Message data incomplete'); 51 | } 52 | 53 | if (this.config?.sendToMultipleReceivers) { 54 | const receivers = message.receiver.to.map(receiver => receiver.id); 55 | const emailReq: Mail.Options = { 56 | ...message.options, 57 | from: fromEmail || '', 58 | to: receivers, 59 | subject: message.options?.subject ?? message.subject, 60 | text: message.options?.text ?? message.body, 61 | html: message.options?.html, 62 | attachments: message.options?.attachments, 63 | }; 64 | await this.transporter.sendMail(emailReq); 65 | } else { 66 | const publishes = message.receiver.to.map(receiver => { 67 | const emailReq: Mail.Options = { 68 | ...message.options, 69 | from: fromEmail || '', 70 | to: receiver.id, 71 | subject: message.options?.subject ?? message.subject, 72 | text: message.options?.text ?? message.body, 73 | html: message.options?.html, 74 | attachments: message.options?.attachments, 75 | }; 76 | return this.transporter.sendMail(emailReq); 77 | }); 78 | 79 | await Promise.all(publishes); 80 | } 81 | }, 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/providers/email/nodemailer/types.ts: -------------------------------------------------------------------------------- 1 | import {EmailMessage, EmailNotification, EmailSubscriber} from '../types'; 2 | import Mail = require('nodemailer/lib/mailer'); 3 | 4 | export interface NodemailerNotification extends EmailNotification { 5 | publish(message: NodemailerMessage): Promise; 6 | } 7 | 8 | export interface NodemailerMessage extends EmailMessage { 9 | /** 10 | * sample message with pdf attachment looks like 11 | * 12 | * { 13 | * from: 'foo@bar.com', 14 | * to: 'bar@foo.com', 15 | * subject: 'An Attached File', 16 | * text: 'Check out this attached pdf file', 17 | * attachments: [{ 18 | * filename: 'file.pdf', 19 | * path: 'C:/Users/Username/Desktop/somefile.pdf', 20 | * contentType: 'application/pdf' 21 | * }] 22 | * } 23 | */ 24 | receiver: NodemailerReceiver; 25 | /** 26 | * subject and to fields will be populated from main message, 27 | * but this will override those values if passed 28 | * 29 | * reciever will be extracted from main message, 30 | * to column in options won't be considered 31 | * 32 | * 'from' will be a mandatory field without which this will be considered wrong 33 | * 34 | * if you want to pass hrml as body, append it to the options 35 | */ 36 | options?: Mail.Options; 37 | } 38 | 39 | export interface NodemailerReceiver { 40 | to: EmailSubscriber[]; 41 | } 42 | -------------------------------------------------------------------------------- /src/providers/email/ses/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './ses.provider'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/email/ses/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import {SES} from 'aws-sdk'; 3 | 4 | export namespace SESBindings { 5 | export const Config = BindingKey.create( 6 | 'sf.notification.config.ses', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/email/ses/ses.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {HttpErrors} from '@loopback/rest'; 3 | import {SES} from 'aws-sdk'; 4 | import {NotificationBindings} from '../../../keys'; 5 | import {INotificationConfig} from '../../../types'; 6 | import {SESBindings} from './keys'; 7 | import {SESMessage, SESNotification} from './types'; 8 | 9 | export class SesProvider implements Provider { 10 | constructor( 11 | @inject(NotificationBindings.Config, { 12 | optional: true, 13 | }) 14 | private readonly config?: INotificationConfig, 15 | @inject(SESBindings.Config, { 16 | optional: true, 17 | }) 18 | private readonly sesConfig?: SES.Types.ClientConfiguration, 19 | ) { 20 | if (this.sesConfig) { 21 | this.sesService = new SES(this.sesConfig); 22 | } else { 23 | throw new HttpErrors.PreconditionFailed('AWS SES Config missing !'); 24 | } 25 | } 26 | 27 | sesService: SES; 28 | 29 | value() { 30 | return { 31 | publish: async (message: SESMessage) => { 32 | const fromEmail = 33 | message.options?.fromEmail ?? this.config?.senderEmail; 34 | 35 | if (!fromEmail) { 36 | throw new HttpErrors.BadRequest( 37 | 'Message sender not found in request', 38 | ); 39 | } 40 | 41 | if (message.receiver.to.length === 0) { 42 | throw new HttpErrors.BadRequest( 43 | 'Message receiver not found in request', 44 | ); 45 | } 46 | if (!message.subject || !message.body) { 47 | throw new HttpErrors.BadRequest('Message data incomplete'); 48 | } 49 | 50 | if (this.config?.sendToMultipleReceivers) { 51 | const receivers = message.receiver.to.map(receiver => receiver.id); 52 | const emailReq: SES.SendEmailRequest = { 53 | Source: fromEmail ?? '', 54 | Destination: { 55 | ToAddresses: receivers, 56 | }, 57 | Message: { 58 | Subject: { 59 | Data: message.subject ?? '', 60 | }, 61 | Body: { 62 | Html: { 63 | Data: message.body || '', 64 | }, 65 | }, 66 | }, 67 | }; 68 | await this.sesService.sendEmail(emailReq).promise(); 69 | } else { 70 | const publishes = message.receiver.to.map(receiver => { 71 | const emailReq: SES.SendEmailRequest = { 72 | Source: fromEmail ?? '', 73 | Destination: { 74 | ToAddresses: [receiver.id], 75 | }, 76 | Message: { 77 | Subject: { 78 | Data: message.subject ?? '', 79 | }, 80 | Body: { 81 | Html: { 82 | Data: message.body || '', 83 | }, 84 | }, 85 | }, 86 | }; 87 | return this.sesService.sendEmail(emailReq).promise(); 88 | }); 89 | 90 | await Promise.all(publishes); 91 | } 92 | }, 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/providers/email/ses/types.ts: -------------------------------------------------------------------------------- 1 | import {EmailMessage, EmailNotification, EmailSubscriber} from '../types'; 2 | 3 | export interface SESNotification extends EmailNotification { 4 | publish(message: SESMessage): Promise; 5 | } 6 | 7 | export interface SESMessage extends EmailMessage { 8 | receiver: SESReceiver; 9 | } 10 | 11 | export interface SESReceiver { 12 | to: EmailSubscriber[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/providers/email/types.ts: -------------------------------------------------------------------------------- 1 | import {INotification, Message, Receiver, Subscriber} from '../../types'; 2 | 3 | export interface EmailNotification extends INotification { 4 | publish(message: EmailMessage): Promise; 5 | } 6 | 7 | export interface EmailMessage extends Message { 8 | receiver: EmailReceiver; 9 | } 10 | 11 | export interface EmailReceiver extends Receiver { 12 | to: EmailSubscriber[]; 13 | cc?: EmailSubscriber[]; 14 | bcc?: EmailSubscriber; 15 | } 16 | 17 | export interface EmailSubscriber extends Subscriber {} 18 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './email'; 2 | export * from './notification.provider'; 3 | export * from './push'; 4 | export * from './sms'; 5 | -------------------------------------------------------------------------------- /src/providers/notification.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {HttpErrors} from '@loopback/rest'; 3 | import {NotificationError} from '../error-keys'; 4 | import {NotificationBindings} from '../keys'; 5 | import {INotification, Message, MessageType} from '../types'; 6 | 7 | export class NotificationProvider implements Provider { 8 | constructor( 9 | @inject(NotificationBindings.SMSProvider, {optional: true}) 10 | private readonly smsProvider?: INotification, 11 | @inject(NotificationBindings.EmailProvider, {optional: true}) 12 | private readonly emailProvider?: INotification, 13 | @inject(NotificationBindings.PushProvider, {optional: true}) 14 | private readonly pushProvider?: INotification, 15 | ) {} 16 | 17 | publish(data: Message) { 18 | if (data.type === MessageType.SMS && this.smsProvider) { 19 | return this.smsProvider.publish(data); 20 | } else if (data.type === MessageType.Email && this.emailProvider) { 21 | return this.emailProvider.publish(data); 22 | } else if (data.type === MessageType.Push && this.pushProvider) { 23 | return this.pushProvider.publish(data); 24 | } else { 25 | throw new HttpErrors.UnprocessableEntity( 26 | NotificationError.ProviderNotFound, 27 | ); 28 | } 29 | } 30 | 31 | value() { 32 | return { 33 | publish: async (message: Message) => this.publish(message), 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/providers/push/apns/apns.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {AnyObject} from '@loopback/repository'; 3 | import {HttpErrors} from '@loopback/rest'; 4 | import apns from '@parse/node-apn'; 5 | import {ApnsBinding} from './keys'; 6 | import {ApnsConfigType, ApnsMessage, ApnsSubscriberType} from './types'; 7 | 8 | export class ApnsProvider implements Provider { 9 | constructor( 10 | @inject(ApnsBinding.Config, { 11 | optional: true, 12 | }) 13 | private readonly apnsConfig?: ApnsConfigType, 14 | ) { 15 | if (this.apnsConfig) { 16 | try { 17 | if (!this.apnsConfig.options.topic) { 18 | throw new HttpErrors.PreconditionFailed('Topic missing !'); 19 | } 20 | this.apnsService = new apns.Provider(this.apnsConfig.providerOptions); 21 | } catch (err) { 22 | throw new HttpErrors.PreconditionFailed(err); 23 | } 24 | } else { 25 | throw new HttpErrors.PreconditionFailed('Apns Config missing !'); 26 | } 27 | } 28 | apnsService: apns.Provider; 29 | initialValidations(message: ApnsMessage) { 30 | if (!!message.options.messageFrom) { 31 | throw new HttpErrors.BadRequest('Message From not found in request !'); 32 | } 33 | if (!message.receiver.to.length) { 34 | throw new HttpErrors.BadRequest( 35 | 'Message receiver not found in request !', 36 | ); 37 | } 38 | 39 | const maxReceivers = 500; 40 | if (message.receiver.to.length > maxReceivers) { 41 | throw new HttpErrors.BadRequest( 42 | 'Message receiver count cannot exceed 500 !', 43 | ); 44 | } 45 | if (!message.subject) { 46 | throw new HttpErrors.BadRequest('Message title not found !'); 47 | } 48 | } 49 | getMainNote(message: ApnsMessage) { 50 | const expiresIn = 3600; // seconds 51 | const floor = 1000; 52 | const defaultBadgeCount = 3; 53 | const note = new apns.Notification(); 54 | note.expiry = Math.floor(Date.now() / floor) + expiresIn; // Expires 1 hour from now. 55 | note.badge = this.apnsConfig?.options.badge ?? defaultBadgeCount; 56 | note.alert = message.body; 57 | note.payload = {messageFrom: message.options.messageFrom}; 58 | // The topic is usually the bundle identifier of your application. 59 | note.topic = String(this.apnsConfig?.options.topic); 60 | return note; 61 | } 62 | async sendingPushToReceiverTokens(message: ApnsMessage): Promise { 63 | const receiverTokens = message.receiver.to.filter( 64 | item => item.type === ApnsSubscriberType.RegistrationToken || !item.type, 65 | ); 66 | if (receiverTokens.length >= 1) { 67 | const tokens = receiverTokens.map(item => item.id); 68 | await this.apnsService.send(this.getMainNote(message), tokens); 69 | } 70 | } 71 | value() { 72 | return { 73 | publish: async (message: ApnsMessage) => { 74 | this.initialValidations(message); 75 | await this.sendingPushToReceiverTokens(message); 76 | }, 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/providers/push/apns/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './apns.provider'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/push/apns/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import * as apns from '@parse/node-apn'; 3 | 4 | export namespace ApnsBinding { 5 | // sonarignore:start 6 | export const Config = BindingKey.create( 7 | 'sf.notification.config.apns', 8 | ); 9 | // sonarignore:end 10 | } 11 | -------------------------------------------------------------------------------- /src/providers/push/apns/types.ts: -------------------------------------------------------------------------------- 1 | import {AnyObject} from '@loopback/repository'; 2 | import {ProviderOptions} from '@parse/node-apn'; 3 | import { 4 | PushMessage, 5 | PushNotification, 6 | PushReceiver, 7 | PushSubscriber, 8 | } from '../types'; 9 | 10 | export interface ApnsNotification extends PushNotification { 11 | publish(message: ApnsMessage): Promise; 12 | } 13 | export interface ApnsConfigType { 14 | providerOptions: ProviderOptions; 15 | 16 | options: { 17 | badge?: number; 18 | topic: string; 19 | }; 20 | } 21 | 22 | export interface ApnsMessage extends PushMessage { 23 | receiver: ApnsReceiver; 24 | options: AnyObject; 25 | } 26 | export interface ApnsReceiver extends PushReceiver { 27 | to: ApnsSubscriber[]; 28 | } 29 | 30 | export interface ApnsSubscriber extends PushSubscriber { 31 | type: ApnsSubscriberType; 32 | id: string; 33 | } 34 | 35 | export const enum ApnsSubscriberType { 36 | RegistrationToken, 37 | } 38 | -------------------------------------------------------------------------------- /src/providers/push/fcm/fcm.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {HttpErrors} from '@loopback/rest'; 3 | import * as admin from 'firebase-admin'; 4 | import {GeneralMessage} from '.'; 5 | import {FcmBindings} from './keys'; 6 | import {FcmMessage, FcmNotification, FcmSubscriberType} from './types'; 7 | 8 | export class FcmProvider implements Provider { 9 | constructor( 10 | @inject(FcmBindings.Config, { 11 | optional: true, 12 | }) 13 | private readonly fcmInstance?: admin.app.App, 14 | ) { 15 | if (this.fcmInstance) { 16 | this.fcmService = this.fcmInstance; 17 | } else { 18 | throw new HttpErrors.PreconditionFailed('Firebase Config missing !'); 19 | } 20 | } 21 | 22 | fcmService: admin.app.App; 23 | 24 | initialValidations(message: FcmMessage) { 25 | const maxReceivers = 500; 26 | if ( 27 | message.receiver.to.length === 0 && 28 | !message.options.topic && 29 | !message.options.condition 30 | ) { 31 | throw new HttpErrors.BadRequest( 32 | 'Message receiver, topic or condition not found in request !', 33 | ); 34 | } 35 | if (message.receiver.to.length > maxReceivers) { 36 | throw new HttpErrors.BadRequest( 37 | 'Message receiver count cannot exceed 500 !', 38 | ); 39 | } 40 | 41 | if (!message.subject) { 42 | throw new HttpErrors.BadRequest('Message title not found !'); 43 | } 44 | } 45 | 46 | sendingPushToReceiverTokens( 47 | message: FcmMessage, 48 | generalMessageObj: GeneralMessage, 49 | ) { 50 | const promises: Promise[] = []; 51 | /**Partial 52 | * These are the registration tokens for all devices which this message 53 | * is intended for. 54 | * 55 | * If receiver does not hold information for type, then it is considered 56 | * as devce token. 57 | */ 58 | const receiverTokens = message.receiver.to.filter( 59 | item => item.type === FcmSubscriberType.RegistrationToken || !item.type, 60 | ); 61 | 62 | /** 63 | * if the receivers are of type 64 | * */ 65 | if (receiverTokens.length >= 1) { 66 | const tokens = receiverTokens.map(item => item.id); 67 | const msgToTransfer = { 68 | tokens: tokens, 69 | ...generalMessageObj, 70 | data: {...message.options.data}, 71 | }; 72 | 73 | const dryRun = message.options.dryRun ?? false; 74 | const sendPromise = this.fcmService 75 | .messaging() 76 | .sendMulticast(msgToTransfer, dryRun); 77 | promises.push(sendPromise); 78 | } 79 | return promises; 80 | } 81 | 82 | sendingPushToTopics(message: FcmMessage, generalMessageObj: GeneralMessage) { 83 | const promises: Promise[] = []; 84 | const topics = message.receiver.to.filter( 85 | item => item.type === FcmSubscriberType.FCMTopic, 86 | ); 87 | 88 | if (topics.length > 0) { 89 | // Messages to multiple Topics is not allowed in single transaction. 90 | 91 | topics.forEach(topic => { 92 | const msgToTransfer = { 93 | topic: topic.id, 94 | ...generalMessageObj, 95 | data: {...message.options.data}, 96 | }; 97 | 98 | const dryRun = message.options.dryRun ?? false; 99 | const sendPromise = this.fcmService 100 | .messaging() 101 | .send(msgToTransfer, dryRun); 102 | promises.push(sendPromise); 103 | }); 104 | } 105 | 106 | return promises; 107 | } 108 | 109 | sendingPushToConditions( 110 | message: FcmMessage, 111 | generalMessageObj: GeneralMessage, 112 | ) { 113 | const promises: Promise[] = []; 114 | const conditions = message.receiver.to.filter( 115 | item => item.type === FcmSubscriberType.FCMCondition, 116 | ); 117 | 118 | if (conditions.length > 0) { 119 | // Condition message 120 | 121 | conditions.forEach(condition => { 122 | const msgToTransfer = { 123 | condition: condition.id, 124 | ...generalMessageObj, 125 | data: {...message.options.data}, 126 | }; 127 | const dryRun = message.options.dryRun ?? false; 128 | const sendPromise = this.fcmService 129 | .messaging() 130 | .send(msgToTransfer, dryRun); 131 | promises.push(sendPromise); 132 | }); 133 | } 134 | 135 | return promises; 136 | } 137 | 138 | value() { 139 | return { 140 | publish: async (message: FcmMessage) => { 141 | /** 142 | * validating the initial request 143 | */ 144 | this.initialValidations(message); 145 | 146 | /** 147 | * This method is responsible to send all the required data to mobile application 148 | * The mobile device will recieve push notification. 149 | * Push will be sent to the devices with registration token sent in receiver 150 | * Notification object holds title, body and imageUrl 151 | * FCM message must contain 2 attributes, i.e title and body 152 | * 153 | */ 154 | 155 | const promises: Promise[] = []; 156 | 157 | const standardNotifForFCM: admin.messaging.Notification = { 158 | body: message.body, 159 | title: message.subject, 160 | imageUrl: message.options.imageUrl, 161 | }; 162 | 163 | /** 164 | * Message attributes for all kinds of messages 165 | * 166 | * If android configurations are sent in options, it will take the 167 | * precedence over normal notification 168 | * 169 | */ 170 | const generalMessageObj = { 171 | notification: standardNotifForFCM, 172 | android: message.options.android, 173 | webpush: message.options.webpush, 174 | apns: message.options.apns, 175 | fcmOptions: message.options.fcmOptions, 176 | }; 177 | 178 | /** 179 | * Sending messages for all the tokens in the request 180 | */ 181 | promises.push( 182 | ...this.sendingPushToReceiverTokens(message, generalMessageObj), 183 | ); 184 | 185 | /** 186 | * Sending messages for all the topics in the request 187 | */ 188 | promises.push(...this.sendingPushToTopics(message, generalMessageObj)); 189 | 190 | /** 191 | * Sending messages for all the conditions in the request 192 | */ 193 | promises.push( 194 | ...this.sendingPushToConditions(message, generalMessageObj), 195 | ); 196 | 197 | await Promise.all(promises); 198 | }, 199 | }; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/providers/push/fcm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fcm.provider'; 2 | export * from './keys'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/push/fcm/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import * as admin from 'firebase-admin'; 3 | 4 | export namespace FcmBindings { 5 | export const Config = BindingKey.create( 6 | 'sf.notification.config.fcm', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/push/fcm/types.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin'; 2 | import { 3 | PushMessage, 4 | PushNotification, 5 | PushReceiver, 6 | PushSubscriber, 7 | } from '../types'; 8 | 9 | export interface FcmNotification extends PushNotification { 10 | publish(message: FcmMessage): Promise; 11 | } 12 | 13 | export interface FcmMessage extends PushMessage { 14 | /** 15 | * If the requirement is to send push on topic or condition, 16 | * send receiver as empty array 17 | */ 18 | receiver: FcmReceiver; 19 | options: { 20 | /** 21 | * URL of an image to be displayed in the notification. 22 | */ 23 | imageUrl?: string; 24 | /** 25 | * @param dryRun Whether to send the message in the dry-run 26 | * (validation only) mode. 27 | * 28 | * Whether or not the message should actually be sent. When set to `true`, 29 | * allows developers to test a request without actually sending a message. When 30 | * set to `false`, the message will be sent. 31 | * 32 | * **Default value:** `false` 33 | */ 34 | dryRun?: boolean; 35 | android?: admin.messaging.AndroidConfig; 36 | webpush?: admin.messaging.WebpushConfig; 37 | apns?: admin.messaging.ApnsConfig; 38 | fcmOptions?: admin.messaging.FcmOptions; 39 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 40 | [key: string]: any; //NOSONAR 41 | }; 42 | } 43 | 44 | export interface FcmReceiver extends PushReceiver { 45 | to: FcmSubscriber[]; 46 | } 47 | 48 | export interface FcmSubscriber extends PushSubscriber { 49 | type: FcmSubscriberType; 50 | id: string; 51 | } 52 | 53 | /** 54 | * The topic name can be optionally prefixed with "/topics/". 55 | * 56 | * the following condition will send messages to devices that are subscribed 57 | * to TopicA and either TopicB or TopicC 58 | * 59 | * "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)" 60 | * 61 | * 62 | * topic?: string; 63 | * 64 | * FCM first evaluates any conditions in parentheses, and then evaluates the 65 | * expression from left to right. In the above expression, a user subscribed 66 | * to any single topic does not receive the message. Likewise, a user who does 67 | * not subscribe to TopicA does not receive the message. 68 | * 69 | * You can include up to five topics in your conditional expression. 70 | * 71 | * example" 72 | * "'stock-GOOG' in topics || 'industry-tech' in topics" 73 | * 74 | * condition?: string; 75 | */ 76 | 77 | export const enum FcmSubscriberType { 78 | RegistrationToken, 79 | FCMTopic, 80 | FCMCondition, 81 | } 82 | 83 | export interface FcmConfig { 84 | dbUrl: string; 85 | serviceAccountPath: string; 86 | } 87 | export interface GeneralMessage { 88 | notification: admin.messaging.Notification; 89 | android?: admin.messaging.AndroidConfig; 90 | webpush?: admin.messaging.WebpushConfig; 91 | apns?: admin.messaging.ApnsConfig; 92 | fcmOptions?: admin.messaging.FcmOptions; 93 | } 94 | -------------------------------------------------------------------------------- /src/providers/push/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apns'; 2 | export * from './fcm'; 3 | export * from './pubnub'; 4 | export * from './socketio'; 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /src/providers/push/pubnub/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './pubnub.provider'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/push/pubnub/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import {PubnubConfig} from './types'; 3 | 4 | export namespace PubnubBindings { 5 | export const Config = BindingKey.create( 6 | 'sf.notification.config.pubnub', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/push/pubnub/pubnub.provider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import {inject, Provider} from '@loopback/core'; 3 | import {HttpErrors} from '@loopback/rest'; 4 | 5 | import Pubnub from 'pubnub'; 6 | import {Aps, MessageConfig, PnApns, TargetsType} from '.'; 7 | import {Config} from '../../../types'; 8 | import {PubnubBindings} from './keys'; 9 | import {PubNubMessage, PubNubNotification, PubNubSubscriberType} from './types'; 10 | export class PubNubProvider implements Provider { 11 | constructor( 12 | @inject(PubnubBindings.Config, { 13 | optional: true, 14 | }) 15 | private readonly pnConfig?: Pubnub.PubNubConfiguration, 16 | ) { 17 | if (this.pnConfig) { 18 | this.pubnubService = new Pubnub(this.pnConfig); 19 | } else { 20 | throw new HttpErrors.PreconditionFailed('Pubnub Config missing !'); 21 | } 22 | } 23 | 24 | pubnubService: Pubnub; 25 | getGeneralMessageObject(message: PubNubMessage) { 26 | const commonDataNotification: MessageConfig = Object.assign( 27 | { 28 | title: message.subject ?? '', 29 | description: message.body, 30 | body: message.body, 31 | }, 32 | message.options, 33 | ); 34 | const pnFcm = { 35 | data: { 36 | ...commonDataNotification, 37 | }, 38 | notification: { 39 | title: message.subject ?? '', 40 | body: message.body, 41 | }, 42 | }; 43 | const apsData: Aps = { 44 | alert: commonDataNotification, 45 | key: message.subject, 46 | sound: message?.options?.sound ? message.options.sound : 'default', 47 | }; 48 | const targetTypeData: TargetsType[] = [ 49 | { 50 | targets: [ 51 | { 52 | environment: process.env.PUBNUB_APNS2_ENV, 53 | topic: process.env.PUBNUB_APNS2_BUNDLE_ID, 54 | }, 55 | ], 56 | version: 'v2', 57 | }, 58 | ]; 59 | const pnApns: PnApns = { 60 | aps: apsData, 61 | pn_push: targetTypeData, 62 | }; 63 | return { 64 | pn_fcm: Object.assign(pnFcm), 65 | pn_apns: Object.assign(pnApns, message.options), 66 | }; 67 | } 68 | getPublishConfig(message: PubNubMessage) { 69 | const generalMessageObj = this.getGeneralMessageObject(message); 70 | const publishConfig: Pubnub.Publish.PublishParameters = { 71 | channel: '', 72 | message: { 73 | title: message.subject ?? '', 74 | description: message.body, 75 | ...generalMessageObj, 76 | }, 77 | }; 78 | return publishConfig; 79 | } 80 | 81 | value() { 82 | return { 83 | publish: async (message: PubNubMessage) => { 84 | if (message.receiver.to.length === 0) { 85 | throw new HttpErrors.BadRequest( 86 | 'Message receiver not found in request', 87 | ); 88 | } 89 | const publishConfig = this.getPublishConfig(message); 90 | const publishes = message.receiver.to.map(receiver => { 91 | if (receiver.type === PubNubSubscriberType.Channel) { 92 | publishConfig.channel = receiver.id; 93 | } 94 | 95 | return this.pubnubService.publish(publishConfig); 96 | }); 97 | 98 | await Promise.all(publishes); 99 | }, 100 | grantAccess: async (config: Config) => { 101 | if (config.options?.token && config.options.ttl) { 102 | const publishConfig: Pubnub.PAM.GrantParameters = { 103 | authKeys: [config.options.token], 104 | channels: config.receiver.to.map(receiver => receiver.id), 105 | read: config.options.allowRead ?? true, 106 | write: config.options.allowWrite ?? false, 107 | ttl: config.options.ttl, 108 | }; 109 | await this.pubnubService.grant(publishConfig); 110 | return { 111 | ttl: config.options.ttl, 112 | }; 113 | } 114 | throw new HttpErrors.BadRequest( 115 | 'Authorization token or ttl not found in request', 116 | ); 117 | }, 118 | revokeAccess: async (config: Config) => { 119 | if (config.options?.token) { 120 | const publishConfig: Pubnub.PAM.GrantParameters = { 121 | channels: config.receiver.to.map(receiver => receiver.id), 122 | authKeys: [config.options.token], 123 | read: false, 124 | write: false, 125 | }; 126 | await this.pubnubService.grant(publishConfig); 127 | return { 128 | success: true, 129 | }; 130 | } 131 | throw new HttpErrors.BadRequest( 132 | 'Authorization token not found in request', 133 | ); 134 | }, 135 | }; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/providers/push/pubnub/types.ts: -------------------------------------------------------------------------------- 1 | import Pubnub from 'pubnub'; 2 | import {Config} from '../../../types'; 3 | import { 4 | PushMessage, 5 | PushNotification, 6 | PushReceiver, 7 | PushSubscriber, 8 | } from '../types'; 9 | 10 | export interface PubNubNotification extends PushNotification { 11 | publish(message: PubNubMessage): Promise; 12 | grantAccess(config: Config): Promise<{}>; 13 | revokeAccess(config: Config): Promise<{}>; 14 | } 15 | 16 | export interface PubNubMessage extends PushMessage { 17 | receiver: PubNubReceiver; 18 | } 19 | 20 | export enum PayloadType { 21 | Data, 22 | Notification, 23 | } 24 | 25 | export interface PubNubGrantRequest extends Config { 26 | receiver: PubNubReceiver; 27 | options?: { 28 | token?: string; 29 | ttl?: number; 30 | }; 31 | } 32 | 33 | export interface PubNubReceiver extends PushReceiver { 34 | to: PubNubSubscriber[]; 35 | } 36 | 37 | export interface PubNubSubscriber extends PushSubscriber { 38 | type: PubNubSubscriberType; 39 | id: string; 40 | } 41 | 42 | export const enum PubNubSubscriberType { 43 | Channel, 44 | } 45 | 46 | export interface PubnubAPNSConfig { 47 | apns2Env?: string; 48 | apns2BundleId?: string; 49 | } 50 | 51 | export interface PnFcm { 52 | data?: MessageConfig; 53 | notification?: MessageConfig; 54 | } 55 | 56 | export interface PnApns { 57 | aps: Aps; 58 | // eslint-disable-next-line @typescript-eslint/naming-convention 59 | pn_push: TargetsType[]; 60 | } 61 | 62 | export interface Aps { 63 | alert: MessageConfig; 64 | key?: string; 65 | sound: string; 66 | } 67 | 68 | export interface TargetsType { 69 | version: string; 70 | targets: { 71 | environment?: string; 72 | topic?: string; 73 | }[]; 74 | } 75 | export interface MessageConfig { 76 | title?: string; 77 | description: string; 78 | } 79 | 80 | export interface GeneralMessageType { 81 | pnApns: PnFcm; 82 | } 83 | export type PubnubConfig = PubnubAPNSConfig & Pubnub.PubNubConfiguration; 84 | -------------------------------------------------------------------------------- /src/providers/push/socketio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './socketio.provider'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/push/socketio/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import {SocketConfig} from './types'; 3 | 4 | export namespace SocketBindings { 5 | export const Config = BindingKey.create( 6 | 'sf.notification.config.socketio', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/push/socketio/socketio.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {HttpErrors} from '@loopback/rest'; 3 | import io from 'socket.io-client'; 4 | import {SocketBindings} from './keys'; 5 | import {SocketConfig, SocketMessage, SocketNotification} from './types'; 6 | 7 | export class SocketIOProvider implements Provider { 8 | constructor( 9 | @inject(SocketBindings.Config, { 10 | optional: true, 11 | }) 12 | private readonly socketConfig?: SocketConfig, 13 | ) { 14 | if (this.socketConfig?.url) { 15 | this.socketService = io(this.socketConfig.url, socketConfig?.options); 16 | } else { 17 | throw new HttpErrors.PreconditionFailed('Socket Config missing !'); 18 | } 19 | } 20 | 21 | socketService: SocketIOClient.Socket; 22 | 23 | value() { 24 | return { 25 | publish: async (message: SocketMessage) => { 26 | if (message?.receiver?.to?.length > 0) { 27 | /** 28 | * This method is responsible to send all the required data to socket server 29 | * The socket server needs to parse the data and send the message to intended 30 | * user. 31 | * 32 | * emitting a message to channel passed via config 33 | */ 34 | 35 | // eslint-disable-next-line @typescript-eslint/prefer-optional-chain 36 | if (!this.socketConfig || !this.socketConfig.defaultPath) { 37 | throw new HttpErrors.PreconditionFailed( 38 | 'Channel info is missing !', 39 | ); 40 | } 41 | this.socketService.emit( 42 | message.options?.path ?? this.socketConfig.defaultPath, 43 | JSON.stringify(message), 44 | ); 45 | } else { 46 | throw new HttpErrors.BadRequest('Message receiver not found'); 47 | } 48 | }, 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/providers/push/socketio/types.ts: -------------------------------------------------------------------------------- 1 | import {AnyObject} from '@loopback/repository'; 2 | import { 3 | PushMessage, 4 | PushNotification, 5 | PushReceiver, 6 | PushSubscriber, 7 | } from '../types'; 8 | 9 | export interface SocketNotification extends PushNotification { 10 | publish(message: SocketMessage): Promise; 11 | } 12 | 13 | export interface SocketMessage extends PushMessage { 14 | receiver: SocketReceiver; 15 | } 16 | 17 | export interface SocketReceiver extends PushReceiver { 18 | to: SocketSubscriber[]; 19 | } 20 | 21 | export interface SocketSubscriber extends PushSubscriber { 22 | type: SocketSubscriberType; 23 | id: string; 24 | } 25 | 26 | export const enum SocketSubscriberType { 27 | Channel, 28 | } 29 | 30 | export interface SocketConfig { 31 | url: string; 32 | /** 33 | * Path represents the default socket server endpoint 34 | */ 35 | defaultPath: string; 36 | options: AnyObject; 37 | } 38 | -------------------------------------------------------------------------------- /src/providers/push/types.ts: -------------------------------------------------------------------------------- 1 | import {INotification, Message, Receiver, Subscriber} from '../../types'; 2 | 3 | export interface PushNotification extends INotification { 4 | publish(message: PushMessage): Promise; 5 | } 6 | 7 | export interface PushMessage extends Message { 8 | receiver: PushReceiver; 9 | } 10 | 11 | export interface PushReceiver extends Receiver { 12 | to: PushSubscriber[]; 13 | } 14 | 15 | export interface PushSubscriber extends Subscriber {} 16 | -------------------------------------------------------------------------------- /src/providers/sms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sns'; 2 | export * from './twilio'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/sms/sns/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './sns.provider'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/sms/sns/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import {SNS} from 'aws-sdk'; 3 | 4 | export namespace SNSBindings { 5 | export const Config = BindingKey.create( 6 | 'sf.notification.config.sns', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/sms/sns/sns.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {HttpErrors} from '@loopback/rest'; 3 | import {SNS} from 'aws-sdk'; 4 | import {SNSBindings} from './keys'; 5 | import {SNSMessage, SNSNotification, SNSSubscriberType} from './types'; 6 | 7 | export class SnsProvider implements Provider { 8 | constructor( 9 | @inject(SNSBindings.Config, { 10 | optional: true, 11 | }) 12 | private readonly snsConfig?: SNS.ClientConfiguration, 13 | ) { 14 | if (this.snsConfig) { 15 | this.snsService = new SNS(this.snsConfig); 16 | } else { 17 | throw new HttpErrors.PreconditionFailed('AWS SNS Config missing !'); 18 | } 19 | } 20 | 21 | snsService: SNS; 22 | 23 | value() { 24 | return { 25 | publish: async (message: SNSMessage) => { 26 | if (message.receiver.to.length === 0) { 27 | throw new HttpErrors.BadRequest( 28 | 'Message receiver not found in request', 29 | ); 30 | } 31 | 32 | const publishes = message.receiver.to.map(receiver => { 33 | const msg: SNS.PublishInput = { 34 | Message: message.body, 35 | Subject: message.subject, 36 | }; 37 | if (message.options?.smsType) { 38 | msg.MessageAttributes = { 39 | 'AWS.SNS.SMS.SMSType': { 40 | DataType: 'String', 41 | StringValue: message.options.smsType, 42 | }, 43 | }; 44 | } 45 | if (receiver.type === SNSSubscriberType.PhoneNumber) { 46 | msg.PhoneNumber = receiver.id; 47 | } else if (receiver.type === SNSSubscriberType.Topic) { 48 | msg.TopicArn = receiver.id; 49 | } else { 50 | // Do nothing 51 | } 52 | 53 | return this.snsService.publish(msg).promise(); 54 | }); 55 | 56 | await Promise.all(publishes); 57 | }, 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/providers/sms/sns/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SMSMessage, 3 | SMSNotification, 4 | SMSReceiver, 5 | SMSSubscriber, 6 | } from '../types'; 7 | 8 | export interface SNSNotification extends SMSNotification { 9 | publish(message: SNSMessage): Promise; 10 | } 11 | 12 | export interface SNSMessage extends SMSMessage { 13 | receiver: SNSReceiver; 14 | } 15 | 16 | export interface SNSReceiver extends SMSReceiver { 17 | to: SNSSubscriber[]; 18 | } 19 | 20 | export interface SNSSubscriber extends SMSSubscriber { 21 | type: SNSSubscriberType; 22 | } 23 | 24 | export const enum SNSSubscriberType { 25 | PhoneNumber, 26 | Topic, 27 | } 28 | 29 | export const enum SNSSMSType { 30 | Promotional = 'Promotional', 31 | Transactional = 'Transactional', 32 | } 33 | -------------------------------------------------------------------------------- /src/providers/sms/twilio/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keys'; 2 | export * from './twilio.provider'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/providers/sms/twilio/keys.ts: -------------------------------------------------------------------------------- 1 | import {BindingKey} from '@loopback/core'; 2 | import {TwilioAuthConfig} from '../twilio/types'; 3 | 4 | export namespace TwilioBindings { 5 | export const Config = BindingKey.create( 6 | 'sf.notification.config.twilio', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/providers/sms/twilio/twilio.provider.ts: -------------------------------------------------------------------------------- 1 | import {inject, Provider} from '@loopback/core'; 2 | import {HttpErrors} from '@loopback/rest'; 3 | import twilio, {Twilio} from 'twilio'; 4 | import {TwilioBindings} from './keys'; 5 | 6 | import { 7 | TwilioAuthConfig, 8 | TwilioCreateMessageParams, 9 | TwilioMessage, 10 | TwilioNotification, 11 | TwilioSubscriberType, 12 | } from '../twilio/types'; 13 | 14 | export class TwilioProvider implements Provider { 15 | twilioService: Twilio; 16 | constructor( 17 | @inject(TwilioBindings.Config, { 18 | optional: true, 19 | }) 20 | private readonly twilioConfig?: TwilioAuthConfig, 21 | ) { 22 | if (this.twilioConfig) { 23 | this.twilioService = twilio( 24 | this.twilioConfig.accountSid, 25 | this.twilioConfig.authToken, 26 | ); 27 | } else { 28 | throw new HttpErrors.PreconditionFailed('Twilio Config missing !'); 29 | } 30 | } 31 | 32 | value() { 33 | return { 34 | publish: async (message: TwilioMessage) => { 35 | if (message.receiver.to.length === 0) { 36 | throw new HttpErrors.BadRequest( 37 | 'Message receiver not found in request', 38 | ); 39 | } 40 | const publishes = message.receiver.to.map(async receiver => { 41 | const msg: string = message.body; 42 | const twilioMsgObj: TwilioCreateMessageParams = { 43 | body: msg, 44 | from: 45 | receiver.type && 46 | receiver.type === TwilioSubscriberType.TextSMSUser 47 | ? String(this.twilioConfig?.smsFrom) 48 | : String(this.twilioConfig?.waFrom), 49 | to: 50 | receiver.type && 51 | receiver.type === TwilioSubscriberType.TextSMSUser 52 | ? `+${receiver.id}` 53 | : `whatsapp:+${receiver.id}`, 54 | }; 55 | 56 | // eslint-disable-next-line no-unused-expressions 57 | message.mediaUrl && (twilioMsgObj.mediaUrl = message.mediaUrl); 58 | 59 | // eslint-disable-next-line no-unused-expressions 60 | receiver.type && 61 | receiver.type === TwilioSubscriberType.TextSMSUser && 62 | this.twilioConfig?.smsStatusCallback && 63 | (twilioMsgObj.statusCallback = 64 | this.twilioConfig?.smsStatusCallback); 65 | 66 | // eslint-disable-next-line no-unused-expressions 67 | !receiver.type && 68 | this.twilioConfig?.waStatusCallback && 69 | (twilioMsgObj.statusCallback = this.twilioConfig?.waStatusCallback); 70 | 71 | return this.twilioService.messages.create(twilioMsgObj); 72 | }); 73 | await Promise.all(publishes); 74 | }, 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/providers/sms/twilio/types.ts: -------------------------------------------------------------------------------- 1 | import {Twilio} from 'twilio'; 2 | import { 3 | SMSMessage, 4 | SMSMessageOptions, 5 | SMSNotification, 6 | SMSReceiver, 7 | SMSSubscriber, 8 | } from '../types'; 9 | 10 | export interface TwilioNotification extends SMSNotification { 11 | publish(message: TwilioMessage): Promise; 12 | } 13 | 14 | export interface TwilioMessage extends SMSMessage { 15 | receiver: TwilioReceiver; 16 | mediaUrl?: Array; 17 | } 18 | 19 | export interface TwilioReceiver extends SMSReceiver { 20 | to: TwilioSubscriber[]; 21 | } 22 | 23 | export interface TwilioSubscriber extends SMSSubscriber { 24 | type: TwilioSubscriberType; 25 | } 26 | 27 | export const enum TwilioSubscriberType { 28 | WhatsappUser = 0, 29 | TextSMSUser = 1, 30 | } 31 | 32 | export const enum TwilioSMSType { 33 | Whatapp = 'Whatapp', 34 | TextSMS = 'TextSMS', 35 | } 36 | 37 | export interface TwilioAuthConfig extends Twilio.TwilioClientOptions { 38 | authToken?: string; 39 | waFrom?: string; //Whatsapp channel or phone number 40 | smsFrom?: string; //From address of SMS twilio number or messaging SID 41 | waStatusCallback?: string; //Status callback url to get WA message status 42 | smsStatusCallback?: string; //Status callback url to get SMS status 43 | opts?: SMSMessageOptions; 44 | } 45 | 46 | export interface TwilioCreateMessageParams { 47 | body: string; 48 | from: string; 49 | to: string; 50 | mediaUrl?: Array; //For whatsapp message with media 51 | statusCallback?: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/providers/sms/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INotification, 3 | Message, 4 | MessageOptions, 5 | Receiver, 6 | Subscriber, 7 | } from '../../types'; 8 | 9 | export interface SMSNotification extends INotification { 10 | publish(message: SMSMessage): Promise; 11 | } 12 | 13 | export interface SMSMessage extends Message { 14 | receiver: SMSReceiver; 15 | subject: undefined; 16 | } 17 | 18 | export interface SMSReceiver extends Receiver { 19 | to: SMSSubscriber[]; 20 | } 21 | 22 | export interface SMSSubscriber extends Subscriber {} 23 | export interface SMSMessageOptions extends MessageOptions {} 24 | -------------------------------------------------------------------------------- /src/release_notes/mymarkdown.ejs: -------------------------------------------------------------------------------- 1 | ## Release [<%= range.split('..')[1] %>](https://github.com/sourcefuse/loopback4-notifications/compare/<%= range %>) <%= new Date().toLocaleDateString('en-us', {year:"numeric", month:"long", day:"numeric"}) 2 | ;%> 3 | Welcome to the <%= new Date().toLocaleDateString('en-us', {year:"numeric", month:"long", day:"numeric"});%> release of loopback4-notifications. There are many updates in this version that we hope you will like, the key highlights include: 4 | <% commits.forEach(function (commit) { %> 5 | - [<%= commit.issueTitle %>](https://github.com/sourcefuse/loopback4-notifications/issues/<%= commit.issueno %>) :- [<%= commit.title %>](https://github.com/sourcefuse/loopback4-notifications/commit/<%= commit.sha1%>) was commited on <%= commit.committerDate %> by [<%= commit.authorName %>](mailto:<%= commit.authorEmail %>) 6 | <% commit.messageLines.forEach(function (message) { %> 7 | - <%= message %> 8 | <% }) %> 9 | <% }) %> 10 | Clink on the above links to understand the changes in detail. 11 | ___ 12 | 13 | -------------------------------------------------------------------------------- /src/release_notes/post-processing.js: -------------------------------------------------------------------------------- 1 | const https = require('node:https'); 2 | const jsdom = require('jsdom'); 3 | module.exports = async function (data, callback) { 4 | const rewritten = []; 5 | for (const commit of data.commits) { 6 | if (commit.title.indexOf('chore(release)') !== -1) { 7 | continue; 8 | } 9 | 10 | const commitTitle = commit.title; 11 | commit.title = commitTitle.substring(0, commitTitle.indexOf('#') - 1); 12 | 13 | commit.messageLines = commit.messageLines.filter(message => { 14 | if (message.indexOf('efs/') === -1) return message; 15 | }); 16 | 17 | commit.messageLines.forEach(message => { 18 | commit.issueno = message.includes('GH-') 19 | ? message.replace('GH-', '').trim() 20 | : null; 21 | }); 22 | 23 | const issueDesc = await getIssueDesc(commit.issueno).then(res => { 24 | return res; 25 | }); 26 | commit.issueTitle = issueDesc; 27 | 28 | commit.committerDate = new Date(commit.committerDate).toLocaleDateString( 29 | 'en-us', 30 | { 31 | year: 'numeric', 32 | month: 'long', 33 | day: 'numeric', 34 | }, 35 | ); 36 | rewritten.push(commit); 37 | } 38 | callback({ 39 | commits: rewritten.filter(Boolean), 40 | range: data.range, 41 | }); 42 | }; 43 | 44 | function getIssueDesc(issueNo) { 45 | return new Promise((resolve, reject) => { 46 | let result = ''; 47 | const req = https.get( 48 | `https://github.com/sourcefuse/loopback4-notifications/issues/${encodeURIComponent( 49 | issueNo, 50 | )}`, 51 | res => { 52 | res.setEncoding('utf8'); 53 | res.on('data', chunk => { 54 | result = result + chunk; 55 | }); 56 | res.on('end', () => { 57 | const {JSDOM} = jsdom; 58 | const dom = new JSDOM(result); 59 | const title = dom.window.document.getElementsByClassName( 60 | 'js-issue-title markdown-title', 61 | ); 62 | let issueTitle = ''; 63 | for (const ele of title) { 64 | if (ele.nodeName === 'BDI') { 65 | issueTitle = ele.innerHTML; 66 | } 67 | } 68 | resolve(issueTitle); 69 | }); 70 | }, 71 | ); 72 | req.on('error', e => { 73 | reject(e); 74 | }); 75 | req.end(); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /src/release_notes/release-notes.js: -------------------------------------------------------------------------------- 1 | const releaseNotes = require('git-release-notes'); 2 | const simpleGit = require('simple-git/promise'); 3 | const path = require('path'); 4 | const {readFile, writeFile, ensureFile} = require('fs-extra'); 5 | 6 | async function generateReleaseNotes() { 7 | try { 8 | const OPTIONS = { 9 | branch: 'master', 10 | s: './post-processing.js', 11 | }; 12 | const RANGE = await getRange(); 13 | const TEMPLATE = './mymarkdown.ejs'; 14 | 15 | const changelog = await releaseNotes(OPTIONS, RANGE, TEMPLATE); 16 | 17 | const changelogPath = path.resolve(__dirname, '../..', 'CHANGELOG.md'); 18 | await ensureFile(changelogPath); 19 | const currentFile = (await readFile(changelogPath)).toString().trim(); 20 | if (currentFile) { 21 | console.log('Update %s', changelogPath); 22 | } else { 23 | console.log('Create %s', changelogPath); 24 | } 25 | 26 | await writeFile(changelogPath, changelog); 27 | await writeFile(changelogPath, currentFile, {flag: 'a+'}); 28 | await addAndCommit().then(() => { 29 | console.log('Changelog has been updated'); 30 | }); 31 | } catch (ex) { 32 | console.error(ex); 33 | process.exit(1); 34 | } 35 | } 36 | 37 | async function getRange() { 38 | const git = simpleGit(); 39 | const tags = (await git.tag({'--sort': 'committerdate'})).split('\n'); 40 | tags.pop(); 41 | 42 | const startTag = tags.slice(-2)[0]; 43 | const endTag = tags.slice(-1)[0]; 44 | return `${startTag}..${endTag}`; 45 | } 46 | 47 | async function addAndCommit() { 48 | const git = simpleGit(); 49 | await git.add(['../../CHANGELOG.md']); 50 | await git.commit('chore(release): changelog file', { 51 | '--no-verify': null, 52 | }); 53 | await git.push('origin', 'master'); 54 | } 55 | 56 | generateReleaseNotes().catch(ex => { 57 | console.error(ex); 58 | process.exit(1); 59 | }); 60 | -------------------------------------------------------------------------------- /src/repositories/README.md: -------------------------------------------------------------------------------- 1 | # Repositories 2 | 3 | This directory contains code for repositories provided by this extension. 4 | 5 | For more information, see . 6 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {AnyObject} from '@loopback/repository'; 2 | export interface INotification { 3 | publish(message: Message): Promise; 4 | } 5 | 6 | export interface INotificationConfig { 7 | sendToMultipleReceivers: boolean; 8 | senderEmail?: string; 9 | } 10 | 11 | export interface Message { 12 | subject?: string; 13 | body: string; 14 | receiver: Receiver; 15 | sentDate: Date; 16 | type: MessageType; 17 | options?: MessageOptions; 18 | } 19 | 20 | export interface Config { 21 | receiver: Receiver; 22 | type: MessageType; 23 | options?: MessageOptions; 24 | } 25 | 26 | export type MessageOptions = AnyObject; 27 | 28 | export interface Subscriber { 29 | id: string; 30 | name?: string; 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | [key: string]: any; //NOSONAR 33 | } 34 | 35 | export interface Receiver { 36 | to: Subscriber[]; 37 | } 38 | 39 | export const enum MessageType { 40 | Push, 41 | Email, 42 | SMS, 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "@loopback/build/config/tsconfig.common.json", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"] 9 | } 10 | --------------------------------------------------------------------------------