├── .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 |
2 |
3 | # [loopback4-notifications](https://github.com/sourcefuse/loopback4-notifications)
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
--------------------------------------------------------------------------------