Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0b86d796e8
|
|||
|
1d5056b753
|
|||
|
82e7a1f299
|
|||
|
4849016784
|
83
.drone.yml
@@ -1,3 +1,86 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docs
|
||||
steps:
|
||||
# ============ BUILD STEPS ===============
|
||||
- name: build documentation
|
||||
image: glmdev/node-pnpm:latest
|
||||
commands:
|
||||
- pnpm i --silent
|
||||
- pnpm docs:build
|
||||
- cd docs && tar czf ../extollo_api_documentation.tar.gz www
|
||||
|
||||
# =============== DEPLOY STEPS ===============
|
||||
- name: copy artifacts to static host
|
||||
image: appleboy/drone-scp
|
||||
settings:
|
||||
host:
|
||||
from_secret: docs_deploy_host
|
||||
username:
|
||||
from_secret: docs_deploy_user
|
||||
key:
|
||||
from_secret: docs_deploy_key
|
||||
port: 22
|
||||
source: extollo_api_documentation.tar.gz
|
||||
target: /var/nfs/general/static/sites/extollo
|
||||
when:
|
||||
event: promote
|
||||
target: docs
|
||||
|
||||
- name: deploy artifacts on static host
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
from_secret: docs_deploy_host
|
||||
username:
|
||||
from_secret: docs_deploy_user
|
||||
key:
|
||||
from_secret: docs_deploy_key
|
||||
port: 22
|
||||
script:
|
||||
- cd /var/nfs/general/static/sites/extollo
|
||||
- rm -rf docs
|
||||
- tar xzf extollo_api_documentation.tar.gz
|
||||
- rm -rf extollo_api_documentation.tar.gz
|
||||
- mv www docs
|
||||
when:
|
||||
event: promote
|
||||
target: docs
|
||||
|
||||
# =============== BUILD NOTIFICATIONS ===============
|
||||
- name: send build success notifications
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: notify_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"title": "Drone-CI [extollo/docs @ ${DRONE_BUILD_NUMBER}]",
|
||||
"message": "Build & deploy completed successfully.",
|
||||
"priority": 4
|
||||
}
|
||||
when:
|
||||
status: success
|
||||
event:
|
||||
- promote
|
||||
|
||||
- name: send build error notifications
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
from_secret: notify_webhook_url
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"title": "Drone-CI [extollo/docs @ ${DRONE_BUILD_NUMBER}]",
|
||||
"message": "Documentation build failed!",
|
||||
"priority": 6
|
||||
}
|
||||
when:
|
||||
status: failure
|
||||
---
|
||||
|
||||
kind: pipeline
|
||||
name: default
|
||||
type: docker
|
||||
|
||||
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
lib
|
||||
dist
|
||||
113
.eslintrc.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-console": "error",
|
||||
"curly": "error",
|
||||
"eqeqeq": "error",
|
||||
"guard-for-in": "error",
|
||||
"no-alert": "error",
|
||||
"no-caller": "error",
|
||||
"no-constructor-return": "error",
|
||||
"no-eval": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-invalid-this": "error",
|
||||
"no-return-await": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-useless-call": "error",
|
||||
"radix": "error",
|
||||
"yoda": "error",
|
||||
"@typescript-eslint/no-shadow": "error",
|
||||
"brace-style": "error",
|
||||
"camelcase": "error",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"comma-style": [
|
||||
"error",
|
||||
"last"
|
||||
],
|
||||
"computed-property-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"eol-last": "error",
|
||||
"func-call-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"lines-between-class-members": "error",
|
||||
"max-params": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"new-parens": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"newline-per-chained-call": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-underscore-dangle": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"object-property-newline": "error",
|
||||
"prefer-exponentiation-operator": "error",
|
||||
"prefer-object-spread": "error",
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"prefer-const": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
www*
|
||||
31
docs/HOME.md
Normal file
@@ -0,0 +1,31 @@
|
||||
<center>
|
||||
<br>
|
||||
<img alt="The Extollo logo" src="https://static.garrettmills.dev/sites/extollo/docs/assets/logo/svg/Extollo-Icon-and-Text-LIGHT-Final.svg" height="150">
|
||||
<br><br>
|
||||
<b>extollo</b> - (v. <em>latin</em>) - to lift up, to elevate
|
||||
<br><br>
|
||||
Extollo is a <a href="https://www.gnu.org/philosophy/floss-and-foss.en.html" target="_blank">free & libre</a> application framework in TypeScript.
|
||||
</center>
|
||||
<hr>
|
||||
|
||||
Built on principles of modularity, strict-typing, inversion-of-control, and developer ergonomics, Extollo enables developers to build maintainable, scalable, and expressive applications.
|
||||
|
||||
Node.js provides an excellent platform for quickly getting an application up and running, but this loose minimalism can lead to larger, more unweildy code-bases as your application grows. Extollo fixes this by providing an opinionated, robust framework and first-party modules that provide, among other things:
|
||||
|
||||
- Type-based dependency injection
|
||||
- Strongly-typed ORM with an expressive query-builder and models
|
||||
- Customizable session & caching interfaces
|
||||
- Modular, pre-compiled, nest-able routes
|
||||
- First-party, extensible command line tools
|
||||
- Unit-based application structure
|
||||
|
||||
## Getting Started
|
||||
Writing an application with Extollo is very straightforward if you are familiar with Node.js/TypeScript, or similar frameworks like Laravel.
|
||||
|
||||
Check out the [Getting Started](https://extollo.garrettmills.dev/pages/Documentation/Getting-Started.html) page site for more information.
|
||||
|
||||
## License & Philosophy
|
||||
The Extollo project is, and will always be, free & libre software. The framework itself is open-source available [here](https://code.garrettmills.dev/Extollo), and is licensed under the terms of the MIT license. See the LICENSE file for more information.
|
||||
|
||||
## Contributing
|
||||
Have an improvement or fix to Extollo? Contributors are always welcome. See the CONTRIBUTING.md file for next steps.
|
||||
1
docs/pages/About-Extollo.md
Normal file
@@ -0,0 +1 @@
|
||||
# About the Extollo Project
|
||||
7
docs/pages/Getting-Started.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Getting Started with Extollo
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js v14 or later
|
||||
- [PNPM](https://pnpm.js.org/) (not NPM/Yarn)
|
||||
- Postgres credentials (if you want to use [@extollo/orm](../modules/orm_src.html))
|
||||
6
docs/sourcefile-map.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"pattern": "^",
|
||||
"replace": "https://code.garrettmills.dev/extollo/lib/src/branch/master/"
|
||||
}
|
||||
]
|
||||
BIN
docs/static/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 206 KiB |
19
docs/static/humans.txt
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/* PROJECT */
|
||||
|
||||
Site Name: The Extollo Framework
|
||||
Site URL: https://extollo.garrettmills.dev/
|
||||
Created: 2021/03/24
|
||||
Standards: HTML5, CSS3
|
||||
Software: TypeDoc
|
||||
|
||||
/* AUTHOR */
|
||||
|
||||
Name: Garrett Mills
|
||||
Location: Lawrence, Kansas
|
||||
Site: https://garrettmills.dev/
|
||||
Blog: https://garrettmills.dev/blog/
|
||||
Contact: https://garrettmills.dev/#contact
|
||||
|
||||
/* THANKS */
|
||||
|
||||
To Piper Mills for the excellent font, color, and logo design.
|
||||
2727
docs/theme/assets/css/main.css
vendored
Normal file
64
docs/theme/assets/css/pages.css
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
h2 code {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h3 code {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.tsd-navigation.primary ul {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tsd-navigation.primary li {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tsd-navigation li.label.pp-nav.pp-group:first-child span {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.tsd-navigation li.label.pp-nav.pp-group {
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.tsd-navigation li.label.pp-nav.pp-group span {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tsd-navigation li.pp-nav.pp-page.current {
|
||||
background-color: #f8f8f8;
|
||||
border-left: 2px solid #222;
|
||||
}
|
||||
|
||||
.tsd-navigation li.pp-nav.pp-page.current a {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tsd-navigation li.pp-nav.pp-page.pp-parent.pp-active {
|
||||
border-left: 2px solid #eee;
|
||||
}
|
||||
|
||||
.tsd-navigation li.pp-nav.pp-page.pp-child {
|
||||
border-left: 2px solid #eee;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.tsd-navigation li.pp-nav.pp-page.pp-child.current {
|
||||
border-left: 2px solid #222;
|
||||
}
|
||||
|
||||
.tsd-kind-page .tsd-kind-icon:before {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
content: "";
|
||||
background-image: url("../images/page-icon.svg");
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
#tsd-search .results span.parent {
|
||||
color: #b3b2b2 !important;
|
||||
}
|
||||
BIN
docs/theme/assets/font/Extatica-Bold.otf
vendored
Normal file
BIN
docs/theme/assets/font/Extatica-Regular.otf
vendored
Normal file
BIN
docs/theme/assets/images/icons.png
vendored
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
docs/theme/assets/images/icons@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
1
docs/theme/assets/images/page-icon.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#AA43FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 468 B |
BIN
docs/theme/assets/images/widgets.png
vendored
Normal file
|
After Width: | Height: | Size: 480 B |
BIN
docs/theme/assets/images/widgets@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 855 B |
1
docs/theme/assets/js/main.js
vendored
Normal file
BIN
docs/theme/assets/logo/png/Extollo-Icon-NO-TEXT-light-and-dark-Final.png
vendored
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Icon-and-Text-DARK-Final.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Icon-and-Text-LIGHT-Final.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Text-NO-ICON-Dark-final.png
vendored
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Text-NO-ICON-Light-final.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
1
docs/theme/assets/logo/svg/Extollo-Icon-NO-TEXT-light-and-dark-Final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 336.67 432"><defs><style>.cls-1{fill:#49686a;}.cls-2{fill:#f2c373;}.cls-3{fill:#2e5252;}.cls-4{fill:#ffe293;}</style></defs><polygon class="cls-1" points="39.28 359.8 202.72 4.89 336.67 303.76 280.63 303.76 194.94 129.42 98.43 359.8 39.28 359.8"/><polygon class="cls-2" points="335.04 310.28 308.57 366.31 252.54 366.31 279 310.28 335.04 310.28"/><polygon class="cls-3" points="246.02 363.06 272.49 307.02 194.94 145.7 165.56 215.83 246.02 363.06"/><polygon class="cls-3" points="194.58 0 140.1 0 0 298.88 31.13 354.92 194.58 0"/><ellipse class="cls-4" cx="170.63" cy="420.6" rx="122.95" ry="11.4"/></svg>
|
||||
|
After Width: | Height: | Size: 691 B |
1
docs/theme/assets/logo/svg/Extollo-Icon-and-Text-DARK-Final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 646.49 291.04"><defs><style>.cls-1{fill:#49686a;}.cls-2{fill:#f2c373;}.cls-3{fill:#2e5252;}.cls-4{fill:#ffe293;}</style></defs><polygon class="cls-1" points="26.46 242.4 136.57 3.29 226.81 204.64 189.06 204.64 131.33 87.19 66.31 242.4 26.46 242.4"/><polygon class="cls-2" points="225.72 209.03 207.89 246.79 170.13 246.79 187.96 209.03 225.72 209.03"/><polygon class="cls-3" points="165.75 244.59 183.57 206.84 131.33 98.16 111.54 145.41 165.75 244.59"/><polygon class="cls-3" points="131.09 0 94.38 0 0 201.35 20.97 239.11 131.09 0"/><ellipse class="cls-4" cx="114.95" cy="283.36" rx="82.83" ry="7.68"/><path class="cls-2" d="M290.79,131.24H344v14.32h-35.6v27.38h32.06V187.4H308.37v28.79H344v14.33H290.79Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M353.19,220.59l15.46-27-13.9-24.11V159.6h13.33l14.75,27.66,14.75-27.66h13.33v9.93L397,193.64l15.46,27v9.93H399.14L382.83,200l-16.31,30.49H353.19Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M426.93,172.94h-9.5V159.6h9.5V144h19.15L438,159.6h20v13.34H443.39V211.8a5.11,5.11,0,0,0,5.39,5.39H458v13.33H445.66q-8.37,0-13.55-5.18t-5.18-13.54Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M475,167.05q8.65-8.73,23-8.72,14.17,0,22.83,8.72T529.48,190v10.21q0,14.18-8.66,22.9T498,231.79q-14.33,0-23-8.72t-8.65-22.9V190Q466.36,175.77,475,167.05Zm12,46.24a14.85,14.85,0,0,0,11,4.18q6.81,0,10.92-4.18A14.83,14.83,0,0,0,513,202.44V187.69q0-6.81-4.11-10.93T498,172.65a15,15,0,0,0-11,4.11q-4.19,4.13-4.19,10.93v14.75A14.69,14.69,0,0,0,487,213.29Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M541.53,127.69H558V230.52H541.53Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M573,127.69h16.45V230.52H573Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M610,167.05q8.64-8.73,23-8.72t22.84,8.72q8.65,8.72,8.65,22.91v10.21q0,14.18-8.65,22.9T633,231.79q-14.32,0-23-8.72t-8.65-22.9V190Q601.38,175.77,610,167.05Zm12,46.24a14.85,14.85,0,0,0,11,4.18q6.81,0,10.92-4.18A14.8,14.8,0,0,0,648,202.44V187.69q0-6.81-4.12-10.93T633,172.65a15,15,0,0,0-11,4.11q-4.18,4.13-4.18,10.93v14.75A14.68,14.68,0,0,0,622,213.29Z" transform="translate(-18 -34.48)"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
docs/theme/assets/logo/svg/Extollo-Icon-and-Text-LIGHT-Final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 648 290.82"><defs><style>.cls-1{fill:#49686a;}.cls-2{fill:#f2c373;}.cls-3{fill:#2e5252;}.cls-4{fill:#ffe293;}</style></defs><polygon class="cls-1" points="26.44 242.22 136.47 3.29 226.64 204.49 188.92 204.49 131.23 87.12 66.26 242.22 26.44 242.22"/><polygon class="cls-2" points="225.54 208.88 207.73 246.6 170 246.6 187.82 208.88 225.54 208.88"/><polygon class="cls-3" points="165.62 244.41 183.43 206.68 131.23 98.09 111.45 145.3 165.62 244.41"/><polygon class="cls-3" points="130.99 0 94.31 0 0 201.2 20.96 238.93 130.99 0"/><ellipse class="cls-4" cx="114.87" cy="283.14" rx="82.77" ry="7.67"/><path class="cls-3" d="M292.58,131.27h53.14v14.32H310.15v27.35h32V187.4h-32v28.77h35.57v14.31H292.58Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M354.93,220.56l15.45-26.93-13.89-24.09v-9.92h13.32l14.74,27.64,14.74-27.64h13.32v9.92l-13.89,24.09,15.45,26.93v9.92H400.85L384.55,200l-16.3,30.47H354.93Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M428.62,172.94h-9.49V159.62h9.49V144h19.14l-8.08,15.59h20v13.32h-14.6v38.83a5.11,5.11,0,0,0,5.39,5.39h9.21v13.32H447.33q-8.35,0-13.53-5.17t-5.18-13.54Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M476.67,167.06q8.64-8.72,22.95-8.72t22.82,8.72q8.65,8.72,8.65,22.89v10.2q0,14.17-8.65,22.89t-22.82,8.72q-14.31,0-22.95-8.72T468,200.15V190Q468,175.78,476.67,167.06Zm12,46.2a14.84,14.84,0,0,0,11,4.18q6.81,0,10.92-4.18a14.81,14.81,0,0,0,4.11-10.84V187.68q0-6.8-4.11-10.91t-10.92-4.11a15,15,0,0,0-11,4.11q-4.18,4.11-4.18,10.91v14.74A14.66,14.66,0,0,0,488.64,213.26Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M543.13,127.73h16.44V230.48H543.13Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M574.59,127.73H591V230.48H574.59Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M611.58,167.06q8.64-8.72,23-8.72t22.81,8.72Q666,175.78,666,190v10.2q0,14.17-8.65,22.89t-22.81,8.72q-14.32,0-23-8.72t-8.65-22.89V190Q602.93,175.78,611.58,167.06Zm12,46.2a14.85,14.85,0,0,0,11,4.18q6.8,0,10.91-4.18a14.81,14.81,0,0,0,4.11-10.84V187.68q0-6.8-4.11-10.91t-10.91-4.11a15,15,0,0,0-11,4.11q-4.17,4.11-4.18,10.91v14.74A14.7,14.7,0,0,0,623.55,213.26Z" transform="translate(-18 -34.59)"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
docs/theme/assets/logo/svg/Extollo-Text-NO-ICON-Dark-final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 432 162.99"><defs><style>.cls-1{fill:#49686a;opacity:0.75;}.cls-2{fill:#f2c373;}</style></defs><ellipse class="cls-1" cx="214.64" cy="153.99" rx="180" ry="9"/><path class="cls-2" d="M5,9H66.44V25.53H25.29V57.17H62.34V73.89H25.29v33.29H66.44v16.56H5Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M77.09,112.26,95,81.11,78.9,53.23V41.76H94.31l17,32,17-32h15.41V53.23L127.75,81.11l17.88,31.15v11.48H130.21L111.36,88.49,92.5,123.74H77.09Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M162.34,57.17h-11V41.76h11v-18h22.14l-9.35,18h23.12V57.17H181.36v44.92a5.9,5.9,0,0,0,6.23,6.23h10.66v15.42H184q-9.68,0-15.66-6t-6-15.66Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M217.92,50.36q10-10.08,26.56-10.08,16.4,0,26.4,10.08t10,26.48V88.65q0,16.39-10,26.48t-26.4,10.08q-16.56,0-26.56-10.08t-10-26.48V76.84Q207.92,60.44,217.92,50.36Zm13.86,53.45q4.83,4.84,12.7,4.84t12.63-4.84q4.75-4.83,4.75-12.54v-17q0-7.87-4.75-12.62t-12.63-4.76q-7.86,0-12.7,4.76t-4.84,12.62v17A16.93,16.93,0,0,0,231.78,103.81Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M294.81,4.87h19V123.74h-19Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M331.21,4.87h19V123.74h-19Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M374,50.36q10-10.08,26.56-10.08,16.4,0,26.39,10.08t10,26.48V88.65q0,16.39-10,26.48t-26.39,10.08q-16.56,0-26.56-10.08T364,88.65V76.84Q364,60.44,374,50.36Zm13.85,53.45q4.84,4.84,12.71,4.84t12.62-4.84q4.76-4.83,4.76-12.54v-17q0-7.87-4.76-12.62t-12.62-4.76q-7.87,0-12.71,4.76T383,74.22v17A17,17,0,0,0,387.85,103.81Z" transform="translate(-4.96 -4.87)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
docs/theme/assets/logo/svg/Extollo-Text-NO-ICON-Light-final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 432 162.99"><defs><style>.cls-1{fill:#ffe293;}.cls-2{fill:#2e5252;}</style></defs><ellipse class="cls-1" cx="213.64" cy="153.99" rx="180" ry="9"/><path class="cls-2" d="M3.6,9H65.08V25.56H23.93V57.21H61V73.93h-37v33.28H65.08v16.56H3.6Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M75.73,112.3,93.61,81.14,77.54,53.27V41.79H93l17,32,17-32h15.42V53.27L126.4,81.14l17.87,31.16v11.47H128.86L110,88.52,91.15,123.77H75.73Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M161,57.21H150V41.79h11v-18h22.13l-9.34,18h23.11V57.21H180v44.92a5.9,5.9,0,0,0,6.23,6.23h10.65v15.41H182.63q-9.67,0-15.66-6t-6-15.66Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M216.57,50.4q10-10.08,26.56-10.08,16.39,0,26.39,10.08t10,26.48V88.69q0,16.4-10,26.47t-26.39,10.09q-16.56,0-26.56-10.09t-10-26.47V76.88Q206.56,60.49,216.57,50.4Zm13.85,53.45q4.83,4.84,12.71,4.84t12.62-4.84q4.75-4.83,4.75-12.54v-17q0-7.87-4.75-12.63t-12.62-4.75q-7.87,0-12.71,4.75t-4.84,12.63v17A16.93,16.93,0,0,0,230.42,103.85Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M293.45,4.9h19V123.77h-19Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M329.85,4.9h19V123.77h-19Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M372.64,50.4q10-10.08,26.56-10.08,16.4,0,26.4,10.08t10,26.48V88.69q0,16.4-10,26.47t-26.4,10.09q-16.56,0-26.56-10.09t-10-26.47V76.88Q362.64,60.49,372.64,50.4Zm13.86,53.45q4.83,4.84,12.7,4.84t12.63-4.84q4.76-4.83,4.75-12.54v-17q0-7.87-4.75-12.63T399.2,56.88q-7.87,0-12.7,4.75t-4.84,12.63v17A16.93,16.93,0,0,0,386.5,103.85Z" transform="translate(-3.6 -4.9)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
51
docs/theme/layouts/default.hbs
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
<!doctype html>
|
||||
<html class="default no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>{{#ifCond model.name '==' project.name}}{{project.name}}{{else}}{{model.name}} | {{project.name}}{{/ifCond}}</title>
|
||||
<meta name="description" content="Documentation for {{project.name}}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="{{relativeURL "assets/css/main.css"}}">
|
||||
<link rel="author" href="{{relativeURL "humans.txt"}}">
|
||||
<script async src="{{relativeURL "assets/js/search.js"}}" id="search-script"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{> header}}
|
||||
|
||||
<div class="container container-main">
|
||||
<div class="row">
|
||||
<div class="col-8 col-content">
|
||||
{{{contents}}}
|
||||
</div>
|
||||
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
|
||||
<nav class="tsd-navigation primary">
|
||||
<ul>
|
||||
{{#each navigation.children}}
|
||||
{{> navigation}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav class="tsd-navigation secondary menu-sticky">
|
||||
<ul class="before-current">
|
||||
{{#each toc.children}}
|
||||
{{> toc.root}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{> footer}}
|
||||
|
||||
<div class="overlay"></div>
|
||||
<script src="{{relativeURL "assets/js/main.js"}}"></script>
|
||||
|
||||
{{> analytics}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
35
docs/theme/partials/footer.hbs
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<h2>Legend</h2>
|
||||
<div class="tsd-legend-group">
|
||||
{{#each legend}}
|
||||
<ul class="tsd-legend">
|
||||
{{#each .}}
|
||||
<li class="{{#compact}}{{#each classes}} {{.}}{{/each}}{{/compact}}"><span class="tsd-kind-icon">{{name}}</span></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{{#unless settings.hideGenerator}}
|
||||
<div class="tsd-generator extollo-end-cap">
|
||||
<img src="{{relativeURL "assets/logo/svg/Extollo-Icon-and-Text-DARK-Final.svg"}}" style="max-height: 100px" class="svg-filter-white" alt="Extollo Logo">
|
||||
<p><b>extollo</b> (v. <em>latin</em>) - to lift up, to elevate</p>
|
||||
<p>
|
||||
Extollo is a <a href="https://www.gnu.org/philosophy/floss-and-foss.en.html" target="_blank">free & libre</a> application framework in TypeScript.
|
||||
</p>
|
||||
<p class="list-of-links">
|
||||
<ul>
|
||||
<li><a href="{{relativeURL "/"}}">Home</a></li>
|
||||
<li><a href="{{relativeURL "pages/Documentation/Getting-Started.html"}}">Getting Started</a></li>
|
||||
<li><a href="{{relativeURL "pages/Documentation/About-Extollo.html"}}">About Extollo</a></li>
|
||||
<li><a href="https://code.garrettmills.dev/Extollo" target="_blank">Source Code</a></li>
|
||||
<li><a href="https://code.garrettmills.dev/extollo/extollo/src/branch/master/CONTRIBUTING.md" target="_blank">Contributing</a></li>
|
||||
<li><a href="https://code.garrettmills.dev/Extollo/docs" target="_blank">Build These Docs</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
{{/unless}}
|
||||
71
docs/theme/partials/header.hbs
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
<header>
|
||||
<div class="tsd-page-toolbar">
|
||||
<div class="container">
|
||||
<div class="table-wrap">
|
||||
<div class="table-cell" id="tsd-search" data-index="{{relativeURL "assets/js/search.json"}}" data-base="{{relativeURL "./"}}">
|
||||
<div class="field">
|
||||
<label for="tsd-search-field" class="tsd-widget search no-caption">Search</label>
|
||||
<input id="tsd-search-field" type="text" />
|
||||
</div>
|
||||
|
||||
<ul class="results">
|
||||
<li class="state loading">Preparing search index...</li>
|
||||
<li class="state failure">The search index is not available</li>
|
||||
</ul>
|
||||
|
||||
<img src="{{relativeURL "assets/logo/svg/Extollo-Icon-NO-TEXT-light-and-dark-Final.svg"}}" alt="Extollo Icon" class="token-logo" style="max-height: 30px; margin-bottom: -10px; padding-right: 10px;">
|
||||
<a href="{{relativeURL "index.html"}}" class="title">{{project.name}}</a>
|
||||
</div>
|
||||
|
||||
<div class="table-cell" id="tsd-widgets">
|
||||
<div id="tsd-filter">
|
||||
<a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a>
|
||||
<div class="tsd-filter-group">
|
||||
<div class="tsd-select" id="tsd-filter-visibility">
|
||||
<span class="tsd-select-label">All</span>
|
||||
<ul class="tsd-select-list">
|
||||
<li data-value="public">Public</li>
|
||||
<li data-value="protected">Public/Protected</li>
|
||||
<li data-value="private" class="selected">All</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="tsd-filter-inherited" checked />
|
||||
<label class="tsd-widget" for="tsd-filter-inherited">Inherited</label>
|
||||
|
||||
{{#unless settings.excludeExternals}}
|
||||
<input type="checkbox" id="tsd-filter-externals" checked />
|
||||
<label class="tsd-widget" for="tsd-filter-externals">Externals</label>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tsd-page-title">
|
||||
<div class="container">
|
||||
{{#if model.parent}} {{! Don't show breadcrumbs on main project page, it is the root page. !}}
|
||||
<ul class="tsd-breadcrumb">
|
||||
{{#with model}}{{> breadcrumb}}{{/with}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
<h1>{{#compact}}
|
||||
{{#ifCond model.kindString "!==" "Project" }}
|
||||
{{model.kindString}}
|
||||
{{/ifCond}}
|
||||
{{model.name}}
|
||||
{{#if model.typeParameters}}
|
||||
<
|
||||
{{#each model.typeParameters}}
|
||||
{{#if @index}}, {{/if}}
|
||||
{{name}}
|
||||
{{/each}}
|
||||
>
|
||||
{{/if}}
|
||||
{{/compact}}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
3
docs/theme/templates/markdown-page.hbs
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="tsd-panel tsd-typography">
|
||||
{{#markdown}}{{{model.pagesPlugin.item.contents}}}{{/markdown}}
|
||||
</div>
|
||||
1338
package-lock.json
generated
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@extollo/lib",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"description": "The framework library that lifts up your code.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
@@ -30,14 +30,21 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"ssh2": "^1.1.0",
|
||||
"ts-node": "^9.1.1",
|
||||
"typedoc": "^0.20.36",
|
||||
"typedoc-plugin-pages-fork": "^0.0.1",
|
||||
"typedoc-plugin-sourcefile-url": "^1.0.6",
|
||||
"typescript": "^4.2.3",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prebuild": "pnpm run lint",
|
||||
"build": "tsc",
|
||||
"app": "tsc && node lib/index.js",
|
||||
"prepare": "pnpm run build"
|
||||
"prepare": "pnpm run build",
|
||||
"docs:build": "typedoc --options typedoc.json",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint --fix . --ext .ts"
|
||||
},
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
@@ -49,5 +56,10 @@
|
||||
"url": "https://code.garrettmills.dev/extollo/lib"
|
||||
},
|
||||
"author": "garrettmills <shout@garrettmills.dev>",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"eslint": "^7.27.0"
|
||||
}
|
||||
}
|
||||
|
||||
17
pagesconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"title": "Documentation",
|
||||
"pages": [
|
||||
{
|
||||
"title": "Getting Started",
|
||||
"source": "./docs/pages/Getting-Started.md"
|
||||
},
|
||||
{
|
||||
"title": "About Extollo",
|
||||
"source": "./docs/pages/About-Extollo.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1234
pnpm-lock.yaml
generated
@@ -1,10 +1,10 @@
|
||||
import {Injectable, Inject} from "../di"
|
||||
import {infer, ErrorWithContext} from "../util"
|
||||
import {CLIOption} from "./directive/options/CLIOption"
|
||||
import {PositionalOption} from "./directive/options/PositionalOption";
|
||||
import {FlagOption} from "./directive/options/FlagOption";
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
import {Logging} from "../service/Logging";
|
||||
import {Injectable, Inject} from '../di'
|
||||
import {infer, ErrorWithContext} from '../util'
|
||||
import {CLIOption} from './directive/options/CLIOption'
|
||||
import {PositionalOption} from './directive/options/PositionalOption'
|
||||
import {FlagOption} from './directive/options/FlagOption'
|
||||
import {AppClass} from '../lifecycle/AppClass'
|
||||
import {Logging} from '../service/Logging'
|
||||
|
||||
/**
|
||||
* Type alias for a definition of a command-line option.
|
||||
@@ -35,7 +35,7 @@ export abstract class Directive extends AppClass {
|
||||
protected readonly logging!: Logging
|
||||
|
||||
/** Parsed option values. */
|
||||
private _optionValues: any
|
||||
private optionValues: any
|
||||
|
||||
/**
|
||||
* Get the keyword or array of keywords that will specify this directive.
|
||||
@@ -84,8 +84,8 @@ export abstract class Directive extends AppClass {
|
||||
* @param optionValues
|
||||
* @private
|
||||
*/
|
||||
private _setOptionValues(optionValues: any) {
|
||||
this._optionValues = optionValues;
|
||||
private setOptionValues(optionValues: any) {
|
||||
this.optionValues = optionValues
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,9 +93,9 @@ export abstract class Directive extends AppClass {
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
public option(name: string, defaultValue?: any) {
|
||||
if ( name in this._optionValues ) {
|
||||
return this._optionValues[name]
|
||||
public option(name: string, defaultValue?: unknown): any {
|
||||
if ( name in this.optionValues ) {
|
||||
return this.optionValues[name]
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
@@ -110,20 +110,28 @@ export abstract class Directive extends AppClass {
|
||||
*
|
||||
* @param argv
|
||||
*/
|
||||
async invoke(argv: string[]) {
|
||||
async invoke(argv: string[]): Promise<void> {
|
||||
const options = this.getResolvedOptions()
|
||||
|
||||
if ( this.didRequestUsage(argv) ) {
|
||||
// @ts-ignore
|
||||
const positionalArguments: PositionalOption<any>[] = options.filter(opt => opt instanceof PositionalOption)
|
||||
const positionalArguments: PositionalOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof PositionalOption ) {
|
||||
positionalArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
const flagArguments: FlagOption<any>[] = options.filter(opt => opt instanceof FlagOption)
|
||||
const flagArguments: FlagOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof FlagOption ) {
|
||||
flagArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
const positionalDisplay: string = positionalArguments.map(x => `<${x.getArgumentName()}>`).join(' ')
|
||||
const flagDisplay: string = flagArguments.length ? ' [...flags]' : ''
|
||||
|
||||
console.log([
|
||||
this.nativeOutput([
|
||||
'',
|
||||
`DIRECTIVE: ${this.getMainKeyword()} - ${this.getDescription()}`,
|
||||
'',
|
||||
@@ -131,7 +139,7 @@ export abstract class Directive extends AppClass {
|
||||
].join('\n'))
|
||||
|
||||
if ( positionalArguments.length ) {
|
||||
console.log([
|
||||
this.nativeOutput([
|
||||
'',
|
||||
`POSITIONAL ARGUMENTS:`,
|
||||
...(positionalArguments.map(arg => {
|
||||
@@ -141,7 +149,7 @@ export abstract class Directive extends AppClass {
|
||||
}
|
||||
|
||||
if ( flagArguments.length ) {
|
||||
console.log([
|
||||
this.nativeOutput([
|
||||
'',
|
||||
`FLAGS:`,
|
||||
...(flagArguments.map(arg => {
|
||||
@@ -152,34 +160,34 @@ export abstract class Directive extends AppClass {
|
||||
|
||||
const help = this.getHelpText()
|
||||
if ( help ) {
|
||||
console.log('\n' + help)
|
||||
this.nativeOutput('\n' + help)
|
||||
}
|
||||
|
||||
console.log('\n')
|
||||
this.nativeOutput('\n')
|
||||
} else {
|
||||
try {
|
||||
const optionValues = this.parseOptions(options, argv)
|
||||
this._setOptionValues(optionValues)
|
||||
this.setOptionValues(optionValues)
|
||||
await this.handle(argv)
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
this.nativeOutput(e.message)
|
||||
if ( e instanceof OptionValidationError ) {
|
||||
// expecting, value, requirements
|
||||
if ( e.context.expecting ) {
|
||||
console.error(` - Expecting: ${e.context.expecting}`)
|
||||
this.nativeOutput(` - Expecting: ${e.context.expecting}`)
|
||||
}
|
||||
|
||||
if ( e.context.requirements && Array.isArray(e.context.requirements) ) {
|
||||
for ( const req of e.context.requirements ) {
|
||||
console.error(` - ${req}`)
|
||||
this.nativeOutput(` - ${req}`)
|
||||
}
|
||||
}
|
||||
|
||||
if ( e.context.value ) {
|
||||
console.error(` - ${e.context.value}`)
|
||||
this.nativeOutput(` - ${e.context.value}`)
|
||||
}
|
||||
}
|
||||
console.error('\nUse --help for more info.')
|
||||
this.nativeOutput('\nUse --help for more info.')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,9 +225,11 @@ export abstract class Directive extends AppClass {
|
||||
* Returns true if the given keyword should invoke this directive.
|
||||
* @param name
|
||||
*/
|
||||
public matchesKeyword(name: string) {
|
||||
public matchesKeyword(name: string): boolean {
|
||||
let kws = this.getKeywords()
|
||||
if ( !Array.isArray(kws) ) kws = [kws]
|
||||
if ( !Array.isArray(kws) ) {
|
||||
kws = [kws]
|
||||
}
|
||||
return kws.includes(name)
|
||||
}
|
||||
|
||||
@@ -227,7 +237,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as success text.
|
||||
* @param output
|
||||
*/
|
||||
success(output: any) {
|
||||
success(output: unknown): void {
|
||||
this.logging.success(output, true)
|
||||
}
|
||||
|
||||
@@ -235,7 +245,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as error text.
|
||||
* @param output
|
||||
*/
|
||||
error(output: any) {
|
||||
error(output: unknown): void {
|
||||
this.logging.error(output, true)
|
||||
}
|
||||
|
||||
@@ -243,7 +253,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as warning text.
|
||||
* @param output
|
||||
*/
|
||||
warn(output: any) {
|
||||
warn(output: unknown): void {
|
||||
this.logging.warn(output, true)
|
||||
}
|
||||
|
||||
@@ -251,7 +261,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as info text.
|
||||
* @param output
|
||||
*/
|
||||
info(output: any) {
|
||||
info(output: unknown): void {
|
||||
this.logging.info(output, true)
|
||||
}
|
||||
|
||||
@@ -259,7 +269,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as debugging text.
|
||||
* @param output
|
||||
*/
|
||||
debug(output: any) {
|
||||
debug(output: unknown): void {
|
||||
this.logging.debug(output, true)
|
||||
}
|
||||
|
||||
@@ -267,7 +277,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as verbose text.
|
||||
* @param output
|
||||
*/
|
||||
verbose(output: any) {
|
||||
verbose(output: unknown): void {
|
||||
this.logging.verbose(output, true)
|
||||
}
|
||||
|
||||
@@ -275,7 +285,7 @@ export abstract class Directive extends AppClass {
|
||||
* Get the flag option that signals help. Usually, this is named 'help'
|
||||
* and supports the flags '--help' and '-?'.
|
||||
*/
|
||||
getHelpOption() {
|
||||
getHelpOption(): FlagOption<any> {
|
||||
return new FlagOption('--help', '-?', 'usage information about this directive')
|
||||
}
|
||||
|
||||
@@ -283,12 +293,21 @@ export abstract class Directive extends AppClass {
|
||||
* Process the raw CLI arguments using an array of option class instances to build
|
||||
* a mapping of option names to provided values.
|
||||
*/
|
||||
parseOptions(options: CLIOption<any>[], args: string[]) {
|
||||
// @ts-ignore
|
||||
let positionalArguments: PositionalOption<any>[] = options.filter(cls => cls instanceof PositionalOption)
|
||||
parseOptions(options: CLIOption<any>[], args: string[]): {[key: string]: any} {
|
||||
let positionalArguments: PositionalOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof PositionalOption ) {
|
||||
positionalArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
const flagArguments: FlagOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof FlagOption ) {
|
||||
flagArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
const flagArguments: FlagOption<any>[] = options.filter(cls => cls instanceof FlagOption)
|
||||
const optionValue: any = {}
|
||||
|
||||
flagArguments.push(this.getHelpOption())
|
||||
@@ -325,7 +344,7 @@ export abstract class Directive extends AppClass {
|
||||
const flagArgument = flagArguments.filter(x => x.shortFlag === value)
|
||||
if ( flagArgument.length < 1 ) {
|
||||
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||
value
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
if ( flagArgument[0].argumentDescription ) {
|
||||
@@ -350,7 +369,7 @@ export abstract class Directive extends AppClass {
|
||||
} else {
|
||||
if ( positionalArguments.length < 1 ) {
|
||||
throw new OptionValidationError(`Unknown positional argument: ${value}`, {
|
||||
value
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
const inferredValue = infer(value)
|
||||
@@ -368,13 +387,13 @@ export abstract class Directive extends AppClass {
|
||||
|
||||
if ( expectingFlagArgument ) {
|
||||
throw new OptionValidationError(`Missing argument for flag: ${positionalFlagName}`, {
|
||||
expecting: positionalFlagName
|
||||
expecting: positionalFlagName,
|
||||
})
|
||||
}
|
||||
|
||||
if ( positionalArguments.length > 0 ) {
|
||||
throw new OptionValidationError(`Missing required argument: ${positionalArguments[0].getArgumentName()}`, {
|
||||
expecting: positionalArguments[0].getArgumentName()
|
||||
expecting: positionalArguments[0].getArgumentName(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -430,14 +449,18 @@ export abstract class Directive extends AppClass {
|
||||
* Determines if, at any point in the arguments, the help option's short or long flag appears.
|
||||
* @returns {boolean} - true if the help flag appeared
|
||||
*/
|
||||
didRequestUsage(argv: string[]) {
|
||||
const help_option = this.getHelpOption()
|
||||
didRequestUsage(argv: string[]): boolean {
|
||||
const helpOption = this.getHelpOption()
|
||||
for ( const arg of argv ) {
|
||||
if ( arg.trim() === help_option.longFlag || arg.trim() === help_option.shortFlag ) {
|
||||
if ( arg.trim() === helpOption.longFlag || arg.trim() === helpOption.shortFlag ) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
protected nativeOutput(...outputs: any[]): void {
|
||||
console.log(...outputs) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Directive} from "../Directive"
|
||||
import {CommandLineApplication} from "../service"
|
||||
import {Injectable} from "../../di"
|
||||
import {ErrorWithContext} from "../../util"
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {Directive} from '../Directive'
|
||||
import {CommandLineApplication} from '../service'
|
||||
import {Injectable} from '../../di'
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
|
||||
/**
|
||||
* A directive that starts the framework's final target normally.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Directive} from "../Directive"
|
||||
import * as colors from "colors/safe"
|
||||
import {Directive} from '../Directive'
|
||||
import * as colors from 'colors/safe'
|
||||
import * as repl from 'repl'
|
||||
import {DependencyKey} from "../../di";
|
||||
import {DependencyKey} from '../../di'
|
||||
|
||||
/**
|
||||
* Launch an interactive REPL shell from within the application.
|
||||
@@ -9,7 +9,7 @@ import {DependencyKey} from "../../di";
|
||||
*/
|
||||
export class ShellDirective extends Directive {
|
||||
protected options: any = {
|
||||
welcome: `powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`,
|
||||
welcome: `powered by Extollo, © ${(new Date()).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`,
|
||||
prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`,
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class ShellDirective extends Directive {
|
||||
}
|
||||
|
||||
await new Promise<void>(res => {
|
||||
console.log(this.options.welcome)
|
||||
this.nativeOutput(this.options.welcome)
|
||||
this.repl = repl.start(this.options.prompt)
|
||||
Object.assign(this.repl.context, state)
|
||||
this.repl.on('exit', () => res())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Directive, OptionDefinition} from "../Directive"
|
||||
import {PositionalOption} from "./options/PositionalOption"
|
||||
import {CommandLine} from "../service"
|
||||
import {Inject, Injectable} from "../../di";
|
||||
import {ErrorWithContext} from "../../util";
|
||||
import {Directive, OptionDefinition} from '../Directive'
|
||||
import {PositionalOption} from './options/PositionalOption'
|
||||
import {CommandLine} from '../service'
|
||||
import {Inject, Injectable} from '../../di'
|
||||
import {ErrorWithContext} from '../../util'
|
||||
|
||||
/**
|
||||
* Create a new file based on a template registered with the CommandLine service.
|
||||
@@ -46,11 +46,11 @@ export class TemplateDirective extends Directive {
|
||||
'',
|
||||
...(registeredTemplates.map(template => {
|
||||
return ` - ${template.name}: ${template.description}`
|
||||
}).all())
|
||||
}).all()),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
async handle(argv: string[]) {
|
||||
async handle(): Promise<void> {
|
||||
const templateName: string = this.option('template_name')
|
||||
const destinationName: string = this.option('file_name')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Directive} from "../Directive"
|
||||
import {Injectable, Inject} from "../../di"
|
||||
import {padRight} from "../../util"
|
||||
import {CommandLine} from "../service"
|
||||
import {Directive} from '../Directive'
|
||||
import {Injectable, Inject} from '../../di'
|
||||
import {padRight} from '../../util'
|
||||
import {CommandLine} from '../service'
|
||||
|
||||
/**
|
||||
* Directive that prints the help message and usage information about
|
||||
@@ -20,7 +20,7 @@ export class UsageDirective extends Directive {
|
||||
return 'print information about available commands'
|
||||
}
|
||||
|
||||
public handle(argv: string[]): void | Promise<void> {
|
||||
public handle(): void | Promise<void> {
|
||||
const directiveStrings = this.cli.getDirectives()
|
||||
.map(cls => this.make<Directive>(cls))
|
||||
.map<[string, string]>(dir => {
|
||||
@@ -30,15 +30,15 @@ export class UsageDirective extends Directive {
|
||||
const maxLen = directiveStrings.max<number>(x => x[0].length)
|
||||
|
||||
const printStrings = directiveStrings.map(grp => {
|
||||
return [padRight(grp[0], maxLen + 1), grp[1]]
|
||||
})
|
||||
return [padRight(grp[0], maxLen + 1), grp[1]]
|
||||
})
|
||||
.map(grp => {
|
||||
return ` ${grp[0]}: ${grp[1]}`
|
||||
})
|
||||
.toArray()
|
||||
|
||||
console.log(this.cli.getASCIILogo())
|
||||
console.log([
|
||||
this.nativeOutput(this.cli.getASCIILogo())
|
||||
this.nativeOutput([
|
||||
'',
|
||||
'Welcome to Extollo! Specify a command to get started.',
|
||||
'',
|
||||
@@ -48,7 +48,7 @@ export class UsageDirective extends Directive {
|
||||
'',
|
||||
'For usage information about a particular command, pass the --help flag.',
|
||||
'-------------------------------------------',
|
||||
`powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills`,
|
||||
`powered by Extollo, © ${(new Date()).getFullYear()} Garrett Mills`,
|
||||
].join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,200 +9,207 @@ export abstract class CLIOption<T> {
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useWhitelist: boolean = false
|
||||
protected useWhitelist = false
|
||||
|
||||
/**
|
||||
* Do we use the blacklist?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useBlacklist: boolean = false
|
||||
protected useBlacklist = false
|
||||
|
||||
/**
|
||||
* Do we use the less-than comparison?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useLessThan: boolean = false
|
||||
protected useLessThan = false
|
||||
|
||||
/**
|
||||
* Do we use the greater-than comparison?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useGreaterThan: boolean = false
|
||||
protected useGreaterThan = false
|
||||
|
||||
/**
|
||||
* Do we use the equality operator?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useEquality: boolean = false
|
||||
protected useEquality = false
|
||||
|
||||
/**
|
||||
* Is this option optional?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _optional: boolean = false
|
||||
protected isOptional = false
|
||||
|
||||
/**
|
||||
* Whitelisted values.
|
||||
* @type {Array<*>}
|
||||
* @private
|
||||
*/
|
||||
protected _whitelist: T[] = []
|
||||
protected whitelistItems: T[] = []
|
||||
|
||||
/**
|
||||
* Blacklisted values.
|
||||
* @type {Array<*>}
|
||||
* @private
|
||||
*/
|
||||
protected _blacklist: T[] = []
|
||||
protected blacklistItems: T[] = []
|
||||
|
||||
/**
|
||||
* Value to be compared in less than.
|
||||
* @type {*}
|
||||
* @private
|
||||
*/
|
||||
protected _lessThanValue?: T
|
||||
protected lessThanValue?: T
|
||||
|
||||
/**
|
||||
* If true, the less than will be less than or equal to.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _lessThanBit: boolean = false
|
||||
protected lessThanBit = false
|
||||
|
||||
/**
|
||||
* Value to be compared in greater than.
|
||||
* @type {*}
|
||||
* @private
|
||||
*/
|
||||
protected _greaterThanValue?: T
|
||||
protected greaterThanValue?: T
|
||||
|
||||
/**
|
||||
* If true, the greater than will be greater than or equal to.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _greateerThanBit: boolean = false
|
||||
protected greaterThanBit = false
|
||||
|
||||
/**
|
||||
* The value to be used to check equality.
|
||||
* @type {*}
|
||||
* @private
|
||||
*/
|
||||
protected _equalityValue?: T
|
||||
protected equalityValue?: T
|
||||
|
||||
/**
|
||||
* Whitelist the specified item or items and enable the whitelist.
|
||||
* @param {...*} items - the items to whitelist
|
||||
*/
|
||||
whitelist(...items: T[]) {
|
||||
this._useWhitelist = true
|
||||
items.forEach(item => this._whitelist.push(item))
|
||||
whitelist(...items: T[]): this {
|
||||
this.useWhitelist = true
|
||||
items.forEach(item => this.whitelistItems.push(item))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Blacklist the specified item or items and enable the blacklist.
|
||||
* @param {...*} items - the items to blacklist
|
||||
*/
|
||||
blacklist(...items: T[]) {
|
||||
this._useBlacklist = true
|
||||
items.forEach(item => this._blacklist.push(item))
|
||||
blacklist(...items: T[]): this {
|
||||
this.useBlacklist = true
|
||||
items.forEach(item => this.blacklistItems.push(item))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in less-than comparison and enables less-than comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
lessThan(value: T) {
|
||||
this._useLessThan = true
|
||||
this._lessThanValue = value
|
||||
lessThan(value: T): this {
|
||||
this.useLessThan = true
|
||||
this.lessThanValue = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in less-than or equal-to comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
lessThanOrEqualTo(value: T) {
|
||||
this._lessThanBit = true
|
||||
lessThanOrEqualTo(value: T): this {
|
||||
this.lessThanBit = true
|
||||
this.lessThan(value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in greater-than comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
greaterThan(value: T) {
|
||||
this._useGreaterThan = true
|
||||
this._greaterThanValue = value
|
||||
greaterThan(value: T): this {
|
||||
this.useGreaterThan = true
|
||||
this.greaterThanValue = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in greater-than or equal-to comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
greaterThanOrEqualTo(value: T) {
|
||||
this._greateerThanBit = true
|
||||
greaterThanOrEqualTo(value: T): this {
|
||||
this.greaterThanBit = true
|
||||
this.greaterThan(value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in equality comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
equals(value: T) {
|
||||
this._useEquality = true
|
||||
this._equalityValue = value
|
||||
equals(value: T): this {
|
||||
this.useEquality = true
|
||||
this.equalityValue = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified value passes the configured comparisons.
|
||||
* @param {*} value
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validate(value: any) {
|
||||
let is_valid = true
|
||||
if ( this._useEquality ) {
|
||||
is_valid = is_valid && (this._equalityValue === value)
|
||||
validate(value: T): boolean {
|
||||
let isValid = true
|
||||
if ( this.useEquality ) {
|
||||
isValid = isValid && (this.equalityValue === value)
|
||||
}
|
||||
|
||||
if ( this._useLessThan && typeof this._lessThanValue !== 'undefined' ) {
|
||||
if ( this._lessThanBit ) {
|
||||
is_valid = is_valid && (value <= this._lessThanValue)
|
||||
if ( this.useLessThan && typeof this.lessThanValue !== 'undefined' ) {
|
||||
if ( this.lessThanBit ) {
|
||||
isValid = isValid && (value <= this.lessThanValue)
|
||||
} else {
|
||||
is_valid = is_valid && (value < this._lessThanValue)
|
||||
isValid = isValid && (value < this.lessThanValue)
|
||||
}
|
||||
}
|
||||
|
||||
if ( this._useGreaterThan && typeof this._greaterThanValue !== 'undefined' ) {
|
||||
if ( this._greateerThanBit ) {
|
||||
is_valid = is_valid && (value >= this._greaterThanValue)
|
||||
if ( this.useGreaterThan && typeof this.greaterThanValue !== 'undefined' ) {
|
||||
if ( this.greaterThanBit ) {
|
||||
isValid = isValid && (value >= this.greaterThanValue)
|
||||
} else {
|
||||
is_valid = is_valid && (value > this._greaterThanValue)
|
||||
isValid = isValid && (value > this.greaterThanValue)
|
||||
}
|
||||
}
|
||||
|
||||
if ( this._useWhitelist ) {
|
||||
is_valid = is_valid && this._whitelist.some(x => {
|
||||
if ( this.useWhitelist ) {
|
||||
isValid = isValid && this.whitelistItems.some(x => {
|
||||
return x === value
|
||||
})
|
||||
}
|
||||
|
||||
if ( this._useBlacklist ) {
|
||||
is_valid = is_valid && !(this._blacklist.some(x => x === value))
|
||||
if ( this.useBlacklist ) {
|
||||
isValid = isValid && !(this.blacklistItems.some(x => x === value))
|
||||
}
|
||||
|
||||
return is_valid
|
||||
return isValid
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Option as optional.
|
||||
*/
|
||||
optional() {
|
||||
this._optional = true
|
||||
optional(): this {
|
||||
this.isOptional = true
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -216,27 +223,27 @@ export abstract class CLIOption<T> {
|
||||
* Get an array of strings denoting the human-readable requirements for this option to be valid.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
getRequirementDisplays() {
|
||||
getRequirementDisplays(): string[] {
|
||||
const clauses = []
|
||||
|
||||
if ( this._useBlacklist ) {
|
||||
clauses.push(`must not be one of: ${this._blacklist.map(x => String(x)).join(', ')}`)
|
||||
if ( this.useBlacklist ) {
|
||||
clauses.push(`must not be one of: ${this.blacklistItems.map(x => String(x)).join(', ')}`)
|
||||
}
|
||||
|
||||
if ( this._useWhitelist ) {
|
||||
clauses.push(`must be one of: ${this._whitelist.map(x => String(x)).join(', ')}`)
|
||||
if ( this.useWhitelist ) {
|
||||
clauses.push(`must be one of: ${this.whitelistItems.map(x => String(x)).join(', ')}`)
|
||||
}
|
||||
|
||||
if ( this._useGreaterThan ) {
|
||||
clauses.push(`must be greater than${this._greateerThanBit ? ' or equal to' : ''}: ${String(this._greaterThanValue)}`)
|
||||
if ( this.useGreaterThan ) {
|
||||
clauses.push(`must be greater than${this.greaterThanBit ? ' or equal to' : ''}: ${String(this.greaterThanValue)}`)
|
||||
}
|
||||
|
||||
if ( this._useLessThan ) {
|
||||
clauses.push(`must be less than${this._lessThanBit ? ' or equal to' : ''}: ${String(this._lessThanValue)}`)
|
||||
if ( this.useLessThan ) {
|
||||
clauses.push(`must be less than${this.lessThanBit ? ' or equal to' : ''}: ${String(this.lessThanValue)}`)
|
||||
}
|
||||
|
||||
if ( this._useEquality ) {
|
||||
clauses.push(`must be equal to: ${String(this._equalityValue)}`)
|
||||
if ( this.useEquality ) {
|
||||
clauses.push(`must be equal to: ${String(this.equalityValue)}`)
|
||||
}
|
||||
|
||||
return clauses
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {CLIOption} from "./CLIOption"
|
||||
import {CLIOption} from './CLIOption'
|
||||
|
||||
/**
|
||||
* Non-positional, flag-based CLI option.
|
||||
@@ -24,8 +24,10 @@ export class FlagOption<T> extends CLIOption<T> {
|
||||
* Description of the argument required by this flag.
|
||||
* If this is set, the flag will expect a positional argument to follow as a param.
|
||||
*/
|
||||
public readonly argumentDescription?: string
|
||||
) { super() }
|
||||
public readonly argumentDescription?: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the referential name for this option.
|
||||
@@ -33,7 +35,7 @@ export class FlagOption<T> extends CLIOption<T> {
|
||||
* be found, the short flag (without the '-') is used.
|
||||
* @returns {string}
|
||||
*/
|
||||
getArgumentName() {
|
||||
getArgumentName(): string {
|
||||
if ( this.longFlag ) {
|
||||
return this.longFlag.replace('--', '')
|
||||
} else if ( this.shortFlag ) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {CLIOption} from "./CLIOption"
|
||||
import {CLIOption} from './CLIOption'
|
||||
|
||||
/**
|
||||
* A positional CLI option. Defined without a flag.
|
||||
@@ -19,14 +19,16 @@ export class PositionalOption<T> extends CLIOption<T> {
|
||||
/**
|
||||
* A usage message describing this parameter.
|
||||
*/
|
||||
public readonly message: string = ''
|
||||
) { super() }
|
||||
public readonly message: string = '',
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the option.
|
||||
* @returns {string}
|
||||
*/
|
||||
getArgumentName () {
|
||||
getArgumentName(): string {
|
||||
return this.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {Singleton, Instantiable, Inject} from "../../di"
|
||||
import {Collection} from "../../util"
|
||||
import {CommandLineApplication} from "./CommandLineApplication"
|
||||
import {Directive} from "../Directive"
|
||||
import {Template} from "../Template"
|
||||
import {directive_template} from "../templates/directive"
|
||||
import {unit_template} from "../templates/unit";
|
||||
import {controller_template} from "../templates/controller";
|
||||
import {middleware_template} from "../templates/middleware";
|
||||
import {routes_template} from "../templates/routes";
|
||||
import {config_template} from "../templates/config";
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Singleton, Instantiable, Inject} from '../../di'
|
||||
import {Collection} from '../../util'
|
||||
import {CommandLineApplication} from './CommandLineApplication'
|
||||
import {Directive} from '../Directive'
|
||||
import {Template} from '../Template'
|
||||
import {templateDirective} from '../templates/directive'
|
||||
import {templateUnit} from '../templates/unit'
|
||||
import {templateController} from '../templates/controller'
|
||||
import {templateMiddleware} from '../templates/middleware'
|
||||
import {templateRoutes} from '../templates/routes'
|
||||
import {templateConfig} from '../templates/config'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
/**
|
||||
* Service for managing directives, templates, and other resources related
|
||||
@@ -27,28 +27,30 @@ export class CommandLine extends Unit {
|
||||
/** Templates registered with the CLI command. These can be created with the TemplateDirective. */
|
||||
protected templates: Collection<Template> = new Collection<Template>()
|
||||
|
||||
constructor() { super() }
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
async up() {
|
||||
this.registerTemplate(directive_template)
|
||||
this.registerTemplate(unit_template)
|
||||
this.registerTemplate(controller_template)
|
||||
this.registerTemplate(middleware_template)
|
||||
this.registerTemplate(routes_template)
|
||||
this.registerTemplate(config_template)
|
||||
async up(): Promise<void> {
|
||||
this.registerTemplate(templateDirective)
|
||||
this.registerTemplate(templateUnit)
|
||||
this.registerTemplate(templateController)
|
||||
this.registerTemplate(templateMiddleware)
|
||||
this.registerTemplate(templateRoutes)
|
||||
this.registerTemplate(templateConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the application was started from the command line.
|
||||
*/
|
||||
public isCLI() {
|
||||
public isCLI(): boolean {
|
||||
return this.app().hasUnit(CommandLineApplication)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the Extollo ASCII logo.
|
||||
*/
|
||||
public getASCIILogo() {
|
||||
public getASCIILogo(): string {
|
||||
return ` _
|
||||
/ /\\ ______ _ _ _
|
||||
/ / \\ | ____| | | | | |
|
||||
@@ -64,24 +66,26 @@ export class CommandLine extends Unit {
|
||||
* the directive available for use on the CLI.
|
||||
* @param directiveClass
|
||||
*/
|
||||
public registerDirective(directiveClass: Instantiable<Directive>) {
|
||||
public registerDirective(directiveClass: Instantiable<Directive>): this {
|
||||
if ( !this.directives.includes(directiveClass) ) {
|
||||
this.directives.push(directiveClass)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given directive is registered with this service.
|
||||
* @param directiveClass
|
||||
*/
|
||||
public hasDirective(directiveClass: Instantiable<Directive>) {
|
||||
public hasDirective(directiveClass: Instantiable<Directive>): boolean {
|
||||
return this.directives.includes(directiveClass)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of all registered directives.
|
||||
*/
|
||||
public getDirectives() {
|
||||
public getDirectives(): Collection<Instantiable<Directive>> {
|
||||
return this.directives.clone()
|
||||
}
|
||||
|
||||
@@ -90,21 +94,23 @@ export class CommandLine extends Unit {
|
||||
* available for use with the TemplateDirective service.
|
||||
* @param template
|
||||
*/
|
||||
public registerTemplate(template: Template) {
|
||||
public registerTemplate(template: Template): this {
|
||||
if ( !this.templates.firstWhere('name', '=', template.name) ) {
|
||||
this.templates.push(template)
|
||||
} else {
|
||||
this.logging.warn(`Duplicate template will not be registered: ${template.name}`)
|
||||
this.logging.debug(`Duplicate template registered at: ${(new Error()).stack}`)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a template with the given name exists.
|
||||
* @param name
|
||||
*/
|
||||
public hasTemplate(name: string) {
|
||||
return !!this.templates.firstWhere('name', '=', name)
|
||||
public hasTemplate(name: string): boolean {
|
||||
return Boolean(this.templates.firstWhere('name', '=', name))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +124,7 @@ export class CommandLine extends Unit {
|
||||
/**
|
||||
* Get a collection of all registered templates.
|
||||
*/
|
||||
public getTemplates() {
|
||||
public getTemplates(): Collection<Template> {
|
||||
return this.templates.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {Unit} from "../../lifecycle/Unit"
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Singleton, Inject} from "../../di/decorator/injection"
|
||||
import {CommandLine} from "./CommandLine"
|
||||
import {UsageDirective} from "../directive/UsageDirective";
|
||||
import {Directive} from "../Directive";
|
||||
import {ShellDirective} from "../directive/ShellDirective";
|
||||
import {TemplateDirective} from "../directive/TemplateDirective";
|
||||
import {RunDirective} from "../directive/RunDirective";
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {Singleton, Inject} from '../../di/decorator/injection'
|
||||
import {CommandLine} from './CommandLine'
|
||||
import {UsageDirective} from '../directive/UsageDirective'
|
||||
import {Directive} from '../Directive'
|
||||
import {ShellDirective} from '../directive/ShellDirective'
|
||||
import {TemplateDirective} from '../directive/TemplateDirective'
|
||||
import {RunDirective} from '../directive/RunDirective'
|
||||
|
||||
/**
|
||||
* Unit that takes the place of the final unit in the application that handles
|
||||
@@ -18,12 +18,12 @@ export class CommandLineApplication extends Unit {
|
||||
private static replacement?: typeof Unit
|
||||
|
||||
/** Set the replaced unit. */
|
||||
public static setReplacement(unitClass?: typeof Unit) {
|
||||
public static setReplacement(unitClass?: typeof Unit): void {
|
||||
this.replacement = unitClass
|
||||
}
|
||||
|
||||
/** Get the replaced unit. */
|
||||
public static getReplacement() {
|
||||
public static getReplacement(): typeof Unit | undefined {
|
||||
return this.replacement
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ export class CommandLineApplication extends Unit {
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
constructor() { super() }
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public async up() {
|
||||
public async up(): Promise<void> {
|
||||
this.cli.registerDirective(UsageDirective)
|
||||
this.cli.registerDirective(ShellDirective)
|
||||
this.cli.registerDirective(TemplateDirective)
|
||||
@@ -50,7 +52,7 @@ export class CommandLineApplication extends Unit {
|
||||
await match.invoke(argv.slice(1))
|
||||
} else {
|
||||
const usage = this.make<UsageDirective>(UsageDirective)
|
||||
await usage.handle(process.argv)
|
||||
await usage.handle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* A template that generates a new configuration file in the app/configs directory.
|
||||
*/
|
||||
const config_template: Template = {
|
||||
const templateConfig: Template = {
|
||||
name: 'config',
|
||||
fileSuffix: '.config.ts',
|
||||
description: 'Create a new config file.',
|
||||
baseAppPath: ['configs'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
render() {
|
||||
return `import { env } from '@extollo/lib'
|
||||
|
||||
export default {
|
||||
key: env('VALUE_ENV_VAR', 'default value'),
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { config_template }
|
||||
export { templateConfig }
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new controller in the app/http/controllers directory.
|
||||
*/
|
||||
const controller_template: Template = {
|
||||
const templateController: Template = {
|
||||
name: 'controller',
|
||||
fileSuffix: '.controller.ts',
|
||||
description: 'Create a controller class that can be used to handle requests.',
|
||||
baseAppPath: ['http', 'controllers'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Controller, view} from "@extollo/lib"
|
||||
import {Inject, Injectable} from "@extollo/di"
|
||||
render(name: string) {
|
||||
return `import {Controller, view, Inject, Injectable} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Controller
|
||||
@@ -25,7 +23,7 @@ export class ${name} extends Controller {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { controller_template }
|
||||
export { templateController }
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new Directive class in the app/directives directory.
|
||||
*/
|
||||
const directive_template: Template = {
|
||||
const templateDirective: Template = {
|
||||
name: 'directive',
|
||||
fileSuffix: '.directive.ts',
|
||||
description: 'Create a new Directive class which adds functionality to the ./ex command.',
|
||||
baseAppPath: ['directives'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Directive, OptionDefinition} from "@extollo/cli"
|
||||
import {Injectable} from "@extollo/di"
|
||||
render(name: string) {
|
||||
return `import {Directive, OptionDefinition, Injectable} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Directive
|
||||
@@ -37,7 +35,7 @@ export class ${name}Directive extends Directive {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { directive_template }
|
||||
export { templateDirective }
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new middleware class in app/http/middlewares.
|
||||
*/
|
||||
const middleware_template: Template = {
|
||||
const templateMiddleware: Template = {
|
||||
name: 'middleware',
|
||||
fileSuffix: '.middleware.ts',
|
||||
description: 'Create a middleware class that can be applied to routes.',
|
||||
baseAppPath: ['http', 'middlewares'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Middleware} from "@extollo/lib"
|
||||
import {Injectable} from "@extollo/di"
|
||||
render(name: string) {
|
||||
return `import {Middleware, Injectable} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Middleware
|
||||
@@ -25,7 +23,7 @@ export class ${name} extends Middleware {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { middleware_template }
|
||||
export { templateMiddleware }
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new route definition file in app/http/routes.
|
||||
*/
|
||||
const routes_template: Template = {
|
||||
const templateRoutes: Template = {
|
||||
name: 'routes',
|
||||
fileSuffix: '.routes.ts',
|
||||
description: 'Create a file for route definitions.',
|
||||
baseAppPath: ['http', 'routes'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Route} from "@extollo/lib"
|
||||
render(name: string) {
|
||||
return `import {Route} from '@extollo/lib'
|
||||
|
||||
/*
|
||||
* ${name} Routes
|
||||
@@ -20,7 +19,7 @@ const routes_template: Template = {
|
||||
|
||||
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { routes_template }
|
||||
export { templateRoutes }
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new application unit class in app/units.
|
||||
*/
|
||||
const unit_template: Template = {
|
||||
const templateUnit: Template = {
|
||||
name: 'unit',
|
||||
fileSuffix: '.ts',
|
||||
description: 'Create a service unit that will start and stop with your application.',
|
||||
baseAppPath: ['units'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Singleton, Inject} from "@extollo/di"
|
||||
import {Unit, Logging} from "@extollo/lib"
|
||||
render(name: string) {
|
||||
return `import {Singleton, Inject, Unit, Logging} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Unit
|
||||
@@ -32,7 +30,7 @@ export class ${name} extends Unit {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { unit_template }
|
||||
export { templateUnit }
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {DependencyKey, InstanceRef, Instantiable, isInstantiable, StaticClass} from "./types";
|
||||
import {AbstractFactory} from "./factory/AbstractFactory";
|
||||
import {collect, Collection, globalRegistry, logIfDebugging} from "../util";
|
||||
import {Factory} from "./factory/Factory";
|
||||
import {DuplicateFactoryKeyError} from "./error/DuplicateFactoryKeyError";
|
||||
import {ClosureFactory} from "./factory/ClosureFactory";
|
||||
import NamedFactory from "./factory/NamedFactory";
|
||||
import SingletonFactory from "./factory/SingletonFactory";
|
||||
import {InvalidDependencyKeyError} from "./error/InvalidDependencyKeyError";
|
||||
import {DependencyKey, InstanceRef, Instantiable, isInstantiable} from './types'
|
||||
import {AbstractFactory} from './factory/AbstractFactory'
|
||||
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
|
||||
import {Factory} from './factory/Factory'
|
||||
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
|
||||
import {ClosureFactory} from './factory/ClosureFactory'
|
||||
import NamedFactory from './factory/NamedFactory'
|
||||
import SingletonFactory from './factory/SingletonFactory'
|
||||
import {InvalidDependencyKeyError} from './error/InvalidDependencyKeyError'
|
||||
|
||||
export type MaybeFactory = AbstractFactory | undefined
|
||||
export type MaybeFactory<T> = AbstractFactory<T> | undefined
|
||||
export type MaybeDependency = any | undefined
|
||||
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any }
|
||||
|
||||
@@ -34,7 +34,7 @@ export class Container {
|
||||
* Collection of factories registered with this container.
|
||||
* @type Collection<AbstractFactory>
|
||||
*/
|
||||
protected factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
|
||||
protected factories: Collection<AbstractFactory<unknown>> = new Collection<AbstractFactory<unknown>>()
|
||||
|
||||
/**
|
||||
* Collection of singleton instances produced by this container.
|
||||
@@ -51,12 +51,14 @@ export class Container {
|
||||
* Register a basic instantiable class as a standard Factory with this container.
|
||||
* @param {Instantiable} dependency
|
||||
*/
|
||||
register(dependency: Instantiable<any>) {
|
||||
if ( this.resolve(dependency) )
|
||||
register(dependency: Instantiable<any>): this {
|
||||
if ( this.resolve(dependency) ) {
|
||||
throw new DuplicateFactoryKeyError(dependency)
|
||||
}
|
||||
|
||||
const factory = new Factory(dependency)
|
||||
this.factories.push(factory)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,12 +66,14 @@ export class Container {
|
||||
* @param {string} name - unique name to identify the factory in the container
|
||||
* @param {function} producer - factory to produce a value
|
||||
*/
|
||||
registerProducer(name: string | StaticClass<any, any>, producer: () => any) {
|
||||
if ( this.resolve(name) )
|
||||
registerProducer(name: DependencyKey, producer: () => any): this {
|
||||
if ( this.resolve(name) ) {
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
}
|
||||
|
||||
const factory = new ClosureFactory(name, producer)
|
||||
this.factories.push(factory)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,12 +82,14 @@ export class Container {
|
||||
* @param {string} name - unique name to identify the factory in the container
|
||||
* @param {Instantiable} dependency
|
||||
*/
|
||||
registerNamed(name: string, dependency: Instantiable<any>) {
|
||||
if ( this.resolve(name) )
|
||||
registerNamed(name: string, dependency: Instantiable<any>): this {
|
||||
if ( this.resolve(name) ) {
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
}
|
||||
|
||||
const factory = new NamedFactory(name, dependency)
|
||||
this.factories.push(factory)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,11 +98,13 @@ export class Container {
|
||||
* @param {string} key - unique name to identify the singleton in the container
|
||||
* @param value
|
||||
*/
|
||||
registerSingleton(key: string, value: any) {
|
||||
if ( this.resolve(key) )
|
||||
registerSingleton<T>(key: DependencyKey, value: T): this {
|
||||
if ( this.resolve(key) ) {
|
||||
throw new DuplicateFactoryKeyError(key)
|
||||
}
|
||||
|
||||
this.factories.push(new SingletonFactory(value, key))
|
||||
this.factories.push(new SingletonFactory(key, value))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,24 +113,30 @@ export class Container {
|
||||
* @param staticClass
|
||||
* @param instance
|
||||
*/
|
||||
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T) {
|
||||
if ( this.resolve(staticClass) )
|
||||
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
|
||||
if ( this.resolve(staticClass) ) {
|
||||
throw new DuplicateFactoryKeyError(staticClass)
|
||||
}
|
||||
|
||||
this.register(staticClass)
|
||||
this.instances.push({
|
||||
key: staticClass,
|
||||
value: instance,
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a given factory with the container.
|
||||
* @param {AbstractFactory} factory
|
||||
*/
|
||||
registerFactory(factory: AbstractFactory) {
|
||||
if ( !this.factories.includes(factory) )
|
||||
registerFactory(factory: AbstractFactory<unknown>): this {
|
||||
if ( !this.factories.includes(factory) ) {
|
||||
this.factories.push(factory)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +152,7 @@ export class Container {
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
hasKey(key: DependencyKey): boolean {
|
||||
return !!this.resolve(key)
|
||||
return Boolean(this.resolve(key))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,17 +161,22 @@ export class Container {
|
||||
*/
|
||||
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||
const instances = this.instances.where('key', '=', key)
|
||||
if ( instances.isNotEmpty() ) return instances.first()
|
||||
if ( instances.isNotEmpty() ) {
|
||||
return instances.first()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the factory for the given key, if one is registered with this container.
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
resolve(key: DependencyKey): MaybeFactory {
|
||||
resolve(key: DependencyKey): MaybeFactory<unknown> {
|
||||
const factory = this.factories.firstWhere(item => item.match(key))
|
||||
if ( factory ) return factory
|
||||
else logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
|
||||
if ( factory ) {
|
||||
return factory
|
||||
} else {
|
||||
logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,22 +191,25 @@ export class Container {
|
||||
// If we've already instantiated this, just return that
|
||||
const instance = this.getExistingInstance(key)
|
||||
logIfDebugging('extollo.di.injector', 'resolveAndCreate existing instance?', instance)
|
||||
if ( typeof instance !== 'undefined' ) return instance.value
|
||||
if ( typeof instance !== 'undefined' ) {
|
||||
return instance.value
|
||||
}
|
||||
|
||||
// Otherwise, attempt to create it
|
||||
const factory = this.resolve(key)
|
||||
logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory)
|
||||
if ( !factory )
|
||||
if ( !factory ) {
|
||||
throw new InvalidDependencyKeyError(key)
|
||||
}
|
||||
|
||||
// Produce and store a new instance
|
||||
const new_instance = this.produceFactory(factory, parameters)
|
||||
const newInstance = this.produceFactory(factory, parameters)
|
||||
this.instances.push({
|
||||
key,
|
||||
value: new_instance,
|
||||
value: newInstance,
|
||||
})
|
||||
|
||||
return new_instance
|
||||
return newInstance
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +218,7 @@ export class Container {
|
||||
* @param {AbstractFactory} factory
|
||||
* @param {array} parameters
|
||||
*/
|
||||
protected produceFactory(factory: AbstractFactory, parameters: any[]) {
|
||||
protected produceFactory<T>(factory: AbstractFactory<T>, parameters: any[]): T {
|
||||
// Create the dependencies for the factory
|
||||
const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key))
|
||||
const dependencies = keys.map<ResolvedDependency>(req => {
|
||||
@@ -210,20 +232,23 @@ export class Container {
|
||||
// Build the arguments for the factory, using dependencies in the
|
||||
// correct paramIndex positions, or parameters of we don't have
|
||||
// the dependency.
|
||||
const construction_args = []
|
||||
let params = collect(parameters).reverse()
|
||||
const constructorArguments = []
|
||||
const params = collect(parameters).reverse()
|
||||
for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) {
|
||||
const dep = dependencies.firstWhere('paramIndex', '=', i)
|
||||
if ( dep ) construction_args.push(dep.resolved)
|
||||
else construction_args.push(params.pop())
|
||||
if ( dep ) {
|
||||
constructorArguments.push(dep.resolved)
|
||||
} else {
|
||||
constructorArguments.push(params.pop())
|
||||
}
|
||||
}
|
||||
|
||||
// Produce a new instance
|
||||
const inst = factory.produce(construction_args, params.reverse().all())
|
||||
const inst = factory.produce(constructorArguments, params.reverse().all())
|
||||
|
||||
factory.getInjectedProperties().each(dependency => {
|
||||
if ( dependency.key && inst ) {
|
||||
inst[dependency.property] = this.resolveAndCreate(dependency.key)
|
||||
(inst as any)[dependency.property] = this.resolveAndCreate(dependency.key)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -240,12 +265,13 @@ export class Container {
|
||||
* @param {...any} parameters
|
||||
*/
|
||||
make<T>(target: DependencyKey, ...parameters: any[]): T {
|
||||
if ( this.hasKey(target) )
|
||||
if ( this.hasKey(target) ) {
|
||||
return this.resolveAndCreate(target, ...parameters)
|
||||
else if ( typeof target !== 'string' && isInstantiable(target) )
|
||||
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
|
||||
return this.produceFactory(new Factory(target), parameters)
|
||||
else
|
||||
} else {
|
||||
throw new TypeError(`Invalid or unknown make target: ${target}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,8 +281,9 @@ export class Container {
|
||||
getDependencies(target: DependencyKey): Collection<DependencyKey> {
|
||||
const factory = this.resolve(target)
|
||||
|
||||
if ( !factory )
|
||||
if ( !factory ) {
|
||||
throw new InvalidDependencyKeyError(target)
|
||||
}
|
||||
|
||||
return factory.getDependencyKeys().pluck('key')
|
||||
}
|
||||
@@ -265,8 +292,9 @@ export class Container {
|
||||
* Given a different container, copy the factories and instances from this container over to it.
|
||||
* @param container
|
||||
*/
|
||||
cloneTo(container: Container) {
|
||||
cloneTo(container: Container): this {
|
||||
container.factories = this.factories.clone()
|
||||
container.instances = this.instances.clone()
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Container, MaybeDependency, MaybeFactory} from "./Container"
|
||||
import {DependencyKey} from "./types"
|
||||
import {Container, MaybeDependency, MaybeFactory} from './Container'
|
||||
import {DependencyKey} from './types'
|
||||
|
||||
/**
|
||||
* A container that uses some parent container as a base, but
|
||||
@@ -26,8 +26,8 @@ export class ScopedContainer extends Container {
|
||||
* Create a new scoped container based on a parent container instance.
|
||||
* @param container
|
||||
*/
|
||||
public static fromParent(container: Container) {
|
||||
return new ScopedContainer(container);
|
||||
public static fromParent(container: Container): ScopedContainer {
|
||||
return new ScopedContainer(container)
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -47,15 +47,19 @@ export class ScopedContainer extends Container {
|
||||
|
||||
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||
const inst = super.getExistingInstance(key)
|
||||
if ( inst ) return inst;
|
||||
if ( inst ) {
|
||||
return inst
|
||||
}
|
||||
|
||||
return this.parentContainer.getExistingInstance(key);
|
||||
return this.parentContainer.getExistingInstance(key)
|
||||
}
|
||||
|
||||
resolve(key: DependencyKey): MaybeFactory {
|
||||
const factory = super.resolve(key);
|
||||
if ( factory ) return factory;
|
||||
resolve(key: DependencyKey): MaybeFactory<any> {
|
||||
const factory = super.resolve(key)
|
||||
if ( factory ) {
|
||||
return factory
|
||||
}
|
||||
|
||||
return this.parentContainer?.resolve(key);
|
||||
return this.parentContainer?.resolve(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
import {collect, Collection} from "../../util";
|
||||
import {collect, Collection} from '../../util'
|
||||
import {
|
||||
DependencyKey,
|
||||
DependencyRequirement,
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
InjectionType,
|
||||
DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
|
||||
PropertyDependency,
|
||||
} from "../types";
|
||||
import {Container} from "../Container";
|
||||
} from '../types'
|
||||
import {Container} from '../Container'
|
||||
|
||||
/**
|
||||
* Get a collection of dependency requirements for the given target object.
|
||||
* @param {Object} target
|
||||
* @return Collection<DependencyRequirement>
|
||||
*/
|
||||
function initDependencyMetadata(target: Object): Collection<DependencyRequirement> {
|
||||
const paramTypes = Reflect.getMetadata('design:paramtypes', target)
|
||||
function initDependencyMetadata(target: unknown): Collection<DependencyRequirement> {
|
||||
const paramTypes = Reflect.getMetadata('design:paramtypes', target as any)
|
||||
return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((type, idx) => {
|
||||
return {
|
||||
paramIndex: idx,
|
||||
@@ -37,32 +37,32 @@ export const Injectable = (): ClassDecorator => {
|
||||
return (target) => {
|
||||
const meta = initDependencyMetadata(target)
|
||||
const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
|
||||
const new_meta = new Collection<DependencyRequirement>()
|
||||
const newMetadata = new Collection<DependencyRequirement>()
|
||||
|
||||
if ( existing ) {
|
||||
const max_new = meta.max('paramIndex')
|
||||
const max_existing = existing.max('paramIndex')
|
||||
for ( let i = 0; i <= Math.max(max_new, max_existing); i++ ) {
|
||||
const existing_dr = existing.firstWhere('paramIndex', '=', i)
|
||||
const new_dr = meta.firstWhere('paramIndex', '=', i)
|
||||
const maxNew = meta.max('paramIndex')
|
||||
const maxExisting = existing.max('paramIndex')
|
||||
for ( let i = 0; i <= Math.max(maxNew, maxExisting); i++ ) {
|
||||
const existingDR = existing.firstWhere('paramIndex', '=', i)
|
||||
const newDR = meta.firstWhere('paramIndex', '=', i)
|
||||
|
||||
if ( existing_dr && !new_dr ) {
|
||||
new_meta.push(existing_dr)
|
||||
} else if ( new_dr && !existing_dr ) {
|
||||
new_meta.push(new_dr)
|
||||
} else if ( new_dr && existing_dr ) {
|
||||
if ( existing_dr.overridden ) {
|
||||
new_meta.push(existing_dr)
|
||||
if ( existingDR && !newDR ) {
|
||||
newMetadata.push(existingDR)
|
||||
} else if ( newDR && !existingDR ) {
|
||||
newMetadata.push(newDR)
|
||||
} else if ( newDR && existingDR ) {
|
||||
if ( existingDR.overridden ) {
|
||||
newMetadata.push(existingDR)
|
||||
} else {
|
||||
new_meta.push(new_dr)
|
||||
newMetadata.push(newDR)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
new_meta.concat(meta)
|
||||
newMetadata.concat(meta)
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, new_meta, target)
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, newMetadata, target)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,14 +82,17 @@ export const Inject = (key?: DependencyKey): PropertyDecorator => {
|
||||
}
|
||||
|
||||
const type = Reflect.getMetadata('design:type', target, property)
|
||||
if ( !key && type ) key = type
|
||||
if ( !key && type ) {
|
||||
key = type
|
||||
}
|
||||
|
||||
if ( key ) {
|
||||
const existing = propertyMetadata.firstWhere('property', '=', property)
|
||||
if ( existing ) {
|
||||
existing.key = key
|
||||
} else {
|
||||
propertyMetadata.push({ property, key })
|
||||
propertyMetadata.push({ property,
|
||||
key })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +121,7 @@ export const InjectParam = (key: DependencyKey): ParameterDecorator => {
|
||||
meta.push({
|
||||
paramIndex,
|
||||
key,
|
||||
overridden: true
|
||||
overridden: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,7 +138,7 @@ export const Singleton = (name?: string): ClassDecorator => {
|
||||
if ( isInstantiable(target) ) {
|
||||
const injectionType: InjectionType = {
|
||||
type: name ? 'named' : 'singleton',
|
||||
...(name ? { name } : {})
|
||||
...(name ? { name } : {}),
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {DependencyKey} from "../types";
|
||||
import {DependencyKey} from '../types'
|
||||
|
||||
/**
|
||||
* Error thrown when a factory is registered with a duplicate dependency key.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {DependencyKey} from "../types";
|
||||
import {DependencyKey} from '../types'
|
||||
|
||||
/**
|
||||
* Error thrown when a dependency key that has not been registered is passed to a resolver.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
import { Collection } from "../../util";
|
||||
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||
import { Collection } from '../../util'
|
||||
|
||||
/**
|
||||
* Abstract base class for dependency container factories.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class AbstractFactory {
|
||||
export abstract class AbstractFactory<T> {
|
||||
protected constructor(
|
||||
/**
|
||||
* Token that was registered for this factory. In most cases, this is the static
|
||||
@@ -13,7 +13,7 @@ export abstract class AbstractFactory {
|
||||
* @var
|
||||
* @protected
|
||||
*/
|
||||
protected token: any
|
||||
protected token: DependencyKey,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -21,14 +21,14 @@ export abstract class AbstractFactory {
|
||||
* @param {Array} dependencies - the resolved dependencies, in order
|
||||
* @param {Array} parameters - the bound constructor parameters, in order
|
||||
*/
|
||||
abstract produce(dependencies: any[], parameters: any[]): any
|
||||
abstract produce(dependencies: any[], parameters: any[]): T
|
||||
|
||||
/**
|
||||
* Should return true if the given identifier matches the token for this factory.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
abstract match(something: any): boolean
|
||||
abstract match(something: unknown): boolean
|
||||
|
||||
/**
|
||||
* Get the dependency requirements required by this factory's token.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {AbstractFactory} from "./AbstractFactory";
|
||||
import {DependencyRequirement, PropertyDependency, StaticClass} from "../types";
|
||||
import {Collection} from "../../util";
|
||||
import {AbstractFactory} from './AbstractFactory'
|
||||
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||
import {Collection} from '../../util'
|
||||
|
||||
/**
|
||||
* A factory whose token is produced by calling a function.
|
||||
@@ -17,19 +17,19 @@ import {Collection} from "../../util";
|
||||
* fact.produce([], []) // => 4
|
||||
* ```
|
||||
*/
|
||||
export class ClosureFactory extends AbstractFactory {
|
||||
export class ClosureFactory<T> extends AbstractFactory<T> {
|
||||
constructor(
|
||||
protected readonly name: string | StaticClass<any, any>,
|
||||
protected readonly token: () => any,
|
||||
protected readonly name: DependencyKey,
|
||||
protected readonly token: () => T,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]): any {
|
||||
produce(): any {
|
||||
return this.token()
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
match(something: unknown): boolean {
|
||||
return something === this.name
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {AbstractFactory} from "./AbstractFactory";
|
||||
import {AbstractFactory} from './AbstractFactory'
|
||||
import {
|
||||
DEPENDENCY_KEYS_METADATA_KEY,
|
||||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
|
||||
DependencyRequirement,
|
||||
Instantiable,
|
||||
PropertyDependency
|
||||
} from "../types";
|
||||
import {Collection} from "../../util";
|
||||
PropertyDependency,
|
||||
} from '../types'
|
||||
import {Collection} from '../../util'
|
||||
import 'reflect-metadata'
|
||||
|
||||
/**
|
||||
@@ -29,9 +29,9 @@ import 'reflect-metadata'
|
||||
* fact.produce([myServiceInstance], []) // => A { myService: myServiceInstance }
|
||||
* ```
|
||||
*/
|
||||
export class Factory extends AbstractFactory {
|
||||
export class Factory<T> extends AbstractFactory<T> {
|
||||
constructor(
|
||||
protected readonly token: Instantiable<any>
|
||||
protected readonly token: Instantiable<T>,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
@@ -40,13 +40,15 @@ export class Factory extends AbstractFactory {
|
||||
return new this.token(...dependencies, ...parameters)
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
match(something: unknown): boolean {
|
||||
return something === this.token // || (something?.name && something.name === this.token.name)
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token)
|
||||
if ( meta ) return meta
|
||||
if ( meta ) {
|
||||
return meta
|
||||
}
|
||||
return new Collection<DependencyRequirement>()
|
||||
}
|
||||
|
||||
@@ -56,7 +58,9 @@ export class Factory extends AbstractFactory {
|
||||
|
||||
do {
|
||||
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
|
||||
if ( loadedMeta ) meta.concat(loadedMeta)
|
||||
if ( loadedMeta ) {
|
||||
meta.concat(loadedMeta)
|
||||
}
|
||||
currentToken = Object.getPrototypeOf(currentToken)
|
||||
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {Factory} from "./Factory";
|
||||
import {Instantiable} from "../types";
|
||||
import {Factory} from './Factory'
|
||||
import {Instantiable} from '../types'
|
||||
|
||||
/**
|
||||
* Container factory that produces an instance of the token, however the token
|
||||
* is identified by a string name rather than a class reference.
|
||||
* @extends Factory
|
||||
*/
|
||||
export default class NamedFactory extends Factory {
|
||||
export default class NamedFactory<T> extends Factory<T> {
|
||||
constructor(
|
||||
/**
|
||||
* The name identifying this factory in the container.
|
||||
@@ -18,12 +18,12 @@ export default class NamedFactory extends Factory {
|
||||
* The token to be instantiated.
|
||||
* @type {Instantiable}
|
||||
*/
|
||||
protected token: Instantiable<any>,
|
||||
protected token: Instantiable<T>,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
match(something: unknown): boolean {
|
||||
return something === this.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Factory } from './Factory'
|
||||
import { Collection } from '../../util'
|
||||
import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||
|
||||
/**
|
||||
* Container factory which returns its token as its value, without attempting
|
||||
@@ -19,29 +19,23 @@ import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
*
|
||||
* @extends Factory
|
||||
*/
|
||||
export default class SingletonFactory extends Factory {
|
||||
export default class SingletonFactory<T> extends Factory<T> {
|
||||
constructor(
|
||||
/**
|
||||
* Instantiated value of this factory.
|
||||
* @type FunctionConstructor
|
||||
* Token identifying this singleton.
|
||||
*/
|
||||
protected token: FunctionConstructor,
|
||||
protected token: DependencyKey,
|
||||
|
||||
/**
|
||||
* String name of this singleton identifying it in the container.
|
||||
* @type string
|
||||
* The value of this singleton.
|
||||
*/
|
||||
protected key: string,
|
||||
protected value: T,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]) {
|
||||
return this.token
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
return something === this.key
|
||||
produce(): T {
|
||||
return this.value
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor';
|
||||
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties';
|
||||
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type';
|
||||
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor'
|
||||
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties'
|
||||
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type'
|
||||
|
||||
/**
|
||||
* Interface that designates a particular value as able to be constructed.
|
||||
@@ -13,20 +13,26 @@ export interface Instantiable<T> {
|
||||
* Returns true if the given value is instantiable.
|
||||
* @param what
|
||||
*/
|
||||
export function isInstantiable<T>(what: any): what is Instantiable<T> {
|
||||
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
|
||||
export function isInstantiable<T>(what: unknown): what is Instantiable<T> {
|
||||
return (
|
||||
Boolean(what)
|
||||
&& (typeof what === 'object' || typeof what === 'function')
|
||||
&& (what !== null)
|
||||
&& 'constructor' in what
|
||||
&& typeof what.constructor === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type that identifies a value as a static class, even if it is not instantiable.
|
||||
*/
|
||||
export type StaticClass<T, T2> = Function & {prototype: T} & T2
|
||||
export type StaticClass<T, T2> = Function & {prototype: T} & T2 // eslint-disable-line @typescript-eslint/ban-types
|
||||
|
||||
/**
|
||||
* Returns true if the parameter is a static class.
|
||||
* @param something
|
||||
*/
|
||||
export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> {
|
||||
export function isStaticClass<T, T2>(something: unknown): something is StaticClass<T, T2> {
|
||||
return typeof something === 'function' && typeof something.prototype !== 'undefined'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Container, Injectable, InjectParam} from '../di'
|
||||
import {Request} from "../http/lifecycle/Request";
|
||||
import {Request} from '../http/lifecycle/Request'
|
||||
import {Valid, ValidationRules} from './rules/types'
|
||||
import {Validator} from './Validator'
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
import {DataContainer} from "../http/lifecycle/Request";
|
||||
import {AppClass} from '../lifecycle/AppClass'
|
||||
import {DataContainer} from '../http/lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Base class for defining reusable validators for request routes.
|
||||
@@ -30,10 +30,12 @@ export abstract class FormRequest<T> extends AppClass {
|
||||
|
||||
constructor(
|
||||
@InjectParam(Request)
|
||||
protected readonly data: DataContainer
|
||||
) { super() }
|
||||
protected readonly data: DataContainer,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected container() {
|
||||
protected container(): Container {
|
||||
return (this.data as unknown) as Container
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from "./rules/types";
|
||||
import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from "../util";
|
||||
import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from './rules/types'
|
||||
import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from '../util'
|
||||
|
||||
/**
|
||||
* An error thrown thrown when an object fails its validation.
|
||||
@@ -7,15 +7,16 @@ import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from "../util
|
||||
export class ValidationError<T> extends ErrorWithContext {
|
||||
constructor(
|
||||
/** The original input data. */
|
||||
public readonly data: any,
|
||||
public readonly data: unknown,
|
||||
|
||||
/** The validator instance used. */
|
||||
public readonly validator: Validator<T>,
|
||||
|
||||
/** Validation error messages, by field. */
|
||||
public readonly errors: Messages
|
||||
public readonly errors: Messages,
|
||||
) {
|
||||
super('One or more fields were invalid.', { data, messages: errors.all() });
|
||||
super('One or more fields were invalid.', { data,
|
||||
messages: errors.all() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +26,7 @@ export class ValidationError<T> extends ErrorWithContext {
|
||||
export class Validator<T> {
|
||||
constructor(
|
||||
/** The rules used to validate input objects. */
|
||||
protected readonly rules: ValidationRules
|
||||
protected readonly rules: ValidationRules,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -47,7 +48,7 @@ export class Validator<T> {
|
||||
* Returns true if the given data is valid and type aliases it as Valid<T>.
|
||||
* @param data
|
||||
*/
|
||||
public async isValid(data: any): Promise<boolean> {
|
||||
public async isValid(data: unknown): Promise<boolean> {
|
||||
return !(await this.validateAndGetErrors(data)).any()
|
||||
}
|
||||
|
||||
@@ -56,18 +57,20 @@ export class Validator<T> {
|
||||
* @param data
|
||||
* @protected
|
||||
*/
|
||||
protected async validateAndGetErrors(data: any): Promise<Messages> {
|
||||
protected async validateAndGetErrors(data: unknown): Promise<Messages> {
|
||||
const messages = new Messages()
|
||||
const params: ValidatorFunctionParams = { data }
|
||||
|
||||
for ( const key in this.rules ) {
|
||||
if ( !this.rules.hasOwnProperty(key) ) continue;
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.rules, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
// This walks over all of the values in the data structure using the nested
|
||||
// key notation. It's not type-safe, but neither is the original input object
|
||||
// yet, so it's useful here.
|
||||
for ( const walkEntry of dataWalkUnsafe<any>(data, key) ) {
|
||||
let [entry, dataKey] = walkEntry
|
||||
for ( const walkEntry of dataWalkUnsafe<any>(data as any, key) ) {
|
||||
let [entry, dataKey] = walkEntry // eslint-disable-line prefer-const
|
||||
const rules = (Array.isArray(this.rules[key]) ? this.rules[key] : [this.rules[key]]) as ValidatorFunction[]
|
||||
|
||||
for ( const rule of rules ) {
|
||||
@@ -83,13 +86,15 @@ export class Validator<T> {
|
||||
}
|
||||
|
||||
for ( const error of errors ) {
|
||||
if ( !messages.has(dataKey, error) ) messages.put(dataKey, error)
|
||||
if ( !messages.has(dataKey, error) ) {
|
||||
messages.put(dataKey, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( result.valid && result.castValue ) {
|
||||
entry = result.castValue
|
||||
data = dataSetUnsafe(dataKey, entry, data)
|
||||
data = dataSetUnsafe(dataKey, entry, data as any)
|
||||
}
|
||||
|
||||
if ( result.stopValidation ) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Instantiable} from '../di'
|
||||
import {FormRequest} from './FormRequest'
|
||||
import {ValidationError} from './Validator'
|
||||
import {ResponseObject, RouteHandler} from "../http/routing/Route";
|
||||
import {Request} from "../http/lifecycle/Request";
|
||||
import {ResponseObject, RouteHandler} from '../http/routing/Route'
|
||||
import {Request} from '../http/lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Builds a middleware function that validates a request's input against
|
||||
|
||||
@@ -1,130 +1,150 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
|
||||
export namespace Arr {
|
||||
/** Requires the input value to be an array. */
|
||||
export function is(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( Array.isArray(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an array'
|
||||
}
|
||||
/** Requires the input value to be an array. */
|
||||
function is(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( Array.isArray(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
/** Requires the values in the input value array to be distinct. */
|
||||
export function distinct(fieldName: string, inputValue: any): ValidationResult {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an array',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the values in the input value array to be distinct. */
|
||||
function distinct(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && (new Set(inputValue)).size === inputValue.length ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must not contain duplicate values',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
function includes(value: unknown): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( (new Set(inputValue)).size === inputValue.length ) {
|
||||
if ( Array.isArray(inputValue) && inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must not contain duplicate values'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
export function includes(value: any): ValidatorFunction {
|
||||
return function includes(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must include ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array NOT to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
export function excludes(value: any): ValidatorFunction {
|
||||
return function excludes(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( !inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must not include ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have exactly `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
export function length(len: number): ValidatorFunction {
|
||||
return function length(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at least `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMin(len: number): ValidatorFunction {
|
||||
return function lengthMin(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at most `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMax(len: number): ValidatorFunction {
|
||||
return function lengthMax(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`
|
||||
}
|
||||
message: `must include ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array NOT to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
function excludes(value: unknown): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && !inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must not include ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have exactly `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
function length(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && inputValue.length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at least `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMin(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && inputValue.length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at most `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMax(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && inputValue.length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Arr = {
|
||||
is,
|
||||
distinct,
|
||||
includes,
|
||||
excludes,
|
||||
length,
|
||||
lengthMin,
|
||||
lengthMax,
|
||||
}
|
||||
|
||||
@@ -1,70 +1,80 @@
|
||||
import {infer as inferUtil} from '../../util'
|
||||
import {ValidationResult} from "./types";
|
||||
import {ValidationResult} from './types'
|
||||
|
||||
export namespace Cast {
|
||||
/** Attempt to infer the native type of a string value. */
|
||||
export function infer(fieldName: string, inputValue: any): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the input value to a boolean.
|
||||
* Note that this assumes the value may be boolish. The strings "true", "True",
|
||||
* "TRUE", and "1" evaluate to `true`, while "false", "False", "FALSE", and "0"
|
||||
* evaluate to `false`.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
export function boolean(fieldName: string, inputValue: any): ValidationResult {
|
||||
let castValue = !!inputValue
|
||||
|
||||
if ( ['true', 'True', 'TRUE', '1'].includes(inputValue) ) castValue = true
|
||||
if ( ['false', 'False', 'FALSE', '0'].includes(inputValue) ) castValue = false
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
castValue,
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a string. */
|
||||
export function string(fieldName: string, inputValue: any): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: String(inputValue),
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a number, if it is numerical. Fails otherwise. */
|
||||
export function numeric(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseFloat(inputValue)) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseFloat(inputValue)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to an integer. Fails otherwise. */
|
||||
export function integer(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseInt(inputValue)) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseInt(inputValue)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
/** Attempt to infer the native type of a string value. */
|
||||
function infer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the input value to a boolean.
|
||||
* Note that this assumes the value may be boolish. The strings "true", "True",
|
||||
* "TRUE", and "1" evaluate to `true`, while "false", "False", "FALSE", and "0"
|
||||
* evaluate to `false`.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
function boolean(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
let castValue = Boolean(inputValue)
|
||||
|
||||
if ( ['true', 'True', 'TRUE', '1'].includes(String(inputValue)) ) {
|
||||
castValue = true
|
||||
}
|
||||
if ( ['false', 'False', 'FALSE', '0'].includes(String(inputValue)) ) {
|
||||
castValue = false
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
castValue,
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a string. */
|
||||
function string(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: String(inputValue),
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a number, if it is numerical. Fails otherwise. */
|
||||
function numeric(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseFloat(String(inputValue))) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseFloat(String(inputValue)),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to an integer. Fails otherwise. */
|
||||
function integer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseInt(String(inputValue), 10)) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseInt(String(inputValue), 10),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
}
|
||||
|
||||
export const Cast = {
|
||||
infer,
|
||||
boolean,
|
||||
string,
|
||||
numeric,
|
||||
integer,
|
||||
}
|
||||
|
||||
@@ -1,197 +1,210 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
|
||||
export namespace Num {
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be greater than some value.
|
||||
* @param value
|
||||
*/
|
||||
export function greaterThan(value: number): ValidatorFunction {
|
||||
return function greaterThan(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue > value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be greater than ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at least some value.
|
||||
* @param value
|
||||
*/
|
||||
export function atLeast(value: number): ValidatorFunction {
|
||||
return function atLeast(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue >= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be less than some value.
|
||||
* @param value
|
||||
*/
|
||||
export function lessThan(value: number): ValidatorFunction {
|
||||
return function lessThan(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue < value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be less than ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at most some value.
|
||||
* @param value
|
||||
*/
|
||||
export function atMost(value: number): ValidatorFunction {
|
||||
return function atMost(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue <= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
export function digits(num: number): ValidatorFunction {
|
||||
return function digits(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).replace('.', '').length === num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have exactly ${num} digits`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
export function digitsMin(num: number): ValidatorFunction {
|
||||
return function digitsMin(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).replace('.', '').length >= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at least ${num} digits`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
export function digitsMax(num: number): ValidatorFunction {
|
||||
return function digitsMax(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).replace('.', '').length <= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at most ${num} digits`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to end with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
export function ends(num: number): ValidatorFunction {
|
||||
return function ends(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).endsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${num}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to begin with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
export function begins(num: number): ValidatorFunction {
|
||||
return function begins(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).startsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${num}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be a multiple of the given number.
|
||||
* @param num
|
||||
*/
|
||||
export function multipleOf(num: number): ValidatorFunction {
|
||||
return function multipleOf(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue % num === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be a multiple of ${num}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be even. */
|
||||
export function even(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue % 2 === 0 ) {
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be greater than some value.
|
||||
* @param value
|
||||
*/
|
||||
function greaterThan(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) > value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be even',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be odd. */
|
||||
export function odd(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue % 2 === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be odd',
|
||||
message: `must be greater than ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at least some value.
|
||||
* @param value
|
||||
*/
|
||||
function atLeast(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) >= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be less than some value.
|
||||
* @param value
|
||||
*/
|
||||
function lessThan(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) < value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be less than ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at most some value.
|
||||
* @param value
|
||||
*/
|
||||
function atMost(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) <= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
function digits(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).replace('.', '').length === num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have exactly ${num} digits`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
function digitsMin(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).replace('.', '').length >= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at least ${num} digits`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
function digitsMax(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).replace('.', '').length <= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at most ${num} digits`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to end with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
function ends(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).endsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${num}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to begin with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
function begins(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).startsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${num}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be a multiple of the given number.
|
||||
* @param num
|
||||
*/
|
||||
function multipleOf(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( parseFloat(String(inputValue)) % num === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be a multiple of ${num}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be even. */
|
||||
function even(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( parseFloat(String(inputValue)) % 2 === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be even',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be odd. */
|
||||
function odd(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( parseFloat(String(inputValue)) % 2 === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be odd',
|
||||
}
|
||||
}
|
||||
|
||||
export const Num = {
|
||||
greaterThan,
|
||||
atLeast,
|
||||
lessThan,
|
||||
atMost,
|
||||
digits,
|
||||
digitsMin,
|
||||
digitsMax,
|
||||
ends,
|
||||
begins,
|
||||
multipleOf,
|
||||
even,
|
||||
odd,
|
||||
}
|
||||
|
||||
@@ -1,175 +1,191 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
import {UniversalPath} from '../../util'
|
||||
|
||||
export namespace Is {
|
||||
/** Requires the given input value to be some form of affirmative boolean. */
|
||||
export function accepted(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be accepted'
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be some form of boolean. */
|
||||
export function boolean(fieldName: string, inputValue: any): ValidationResult {
|
||||
const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0]
|
||||
if ( boolish.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be true or false'
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be of type string. */
|
||||
export function string(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( typeof inputValue === 'string' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a string'
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be present and non-nullish. */
|
||||
export function required(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is required',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
export function present(fieldName: string, inputValue: any): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
export function filled(fieldName: string, inputValue: any): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Requires the given input value to be absent or nullish. */
|
||||
export function prohibited(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not allowed',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
export function absent(fieldName: string, inputValue: any): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
export function empty(fieldName: string, inputValue: any): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
export function foundIn(values: any[]): ValidatorFunction {
|
||||
return function foundIn(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input NOT to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
export function notFoundIn(values: any[]): ValidatorFunction {
|
||||
return function foundIn(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be number-like. */
|
||||
export function numeric(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseFloat(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be integer-like. */
|
||||
export function integer(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseInt(inputValue)) && parseInt(inputValue) === parseFloat(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be a UniversalPath. */
|
||||
export function file(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue instanceof UniversalPath ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a file'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A special validator function that marks a field as optional.
|
||||
* If the value of the field is nullish, no further validation rules will be applied.
|
||||
* If it is non-nullish, validation will continue.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
export function optional(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue ?? true ) {
|
||||
return {
|
||||
valid: true,
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be some form of affirmative boolean. */
|
||||
function accepted(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be accepted',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be some form of boolean. */
|
||||
function boolean(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0]
|
||||
if ( boolish.includes(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be true or false',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be of type string. */
|
||||
function string(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( typeof inputValue === 'string' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a string',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be present and non-nullish. */
|
||||
function required(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is required',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
function present(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
function filled(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Requires the given input value to be absent or nullish. */
|
||||
function prohibited(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not allowed',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
function absent(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
function empty(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
function foundIn(values: any[]): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input NOT to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
function notFoundIn(values: any[]): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be number-like. */
|
||||
function numeric(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseFloat(String(inputValue))) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be integer-like. */
|
||||
function integer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseInt(String(inputValue), 10)) && parseInt(String(inputValue), 10) === parseFloat(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be a UniversalPath. */
|
||||
function file(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( inputValue instanceof UniversalPath ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a file',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A special validator function that marks a field as optional.
|
||||
* If the value of the field is nullish, no further validation rules will be applied.
|
||||
* If it is non-nullish, validation will continue.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
function optional(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( inputValue ?? true ) {
|
||||
return {
|
||||
valid: true,
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
export const Is = {
|
||||
accepted,
|
||||
boolean,
|
||||
string,
|
||||
required,
|
||||
present,
|
||||
filled,
|
||||
prohibited,
|
||||
absent,
|
||||
empty,
|
||||
foundIn,
|
||||
notFoundIn,
|
||||
numeric,
|
||||
integer,
|
||||
file,
|
||||
optional,
|
||||
}
|
||||
|
||||
@@ -1,224 +1,245 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
import {isJSON} from '../../util'
|
||||
|
||||
/**
|
||||
* String-related validation rules.
|
||||
*/
|
||||
export namespace Str {
|
||||
const regexes: {[key: string]: RegExp} = {
|
||||
'string.is.alpha': /[a-zA-Z]*/,
|
||||
'string.is.alpha_num': /[a-zA-Z0-9]*/,
|
||||
'string.is.alpha_dash': /[a-zA-Z\-]*/,
|
||||
'string.is.alpha_score': /[a-zA-Z_]*/,
|
||||
'string.is.alpha_num_dash_score': /[a-zA-Z\-_0-9]*/,
|
||||
'string.is.email': /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])/,
|
||||
'string.is.ip': /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/,
|
||||
'string.is.ip.v4': /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
|
||||
'string.is.ip.v6': /(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))/,
|
||||
'string.is.mime': /^(?=[-a-z]{1,127}\/[-.a-z0-9]{1,127}$)[a-z]+(-[a-z]+)*\/[a-z0-9]+([-.][a-z0-9]+)*$/,
|
||||
'string.is.url': /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=+$,\w]+@)?[A-Za-z0-9.\-]+|(?:www\.|[\-;:&=+$,\w]+@)[A-Za-z0-9.\-]+)((?:\/[+~%\/.\w\-_]*)?\??(?:[\-+=&;%@.\w_]*)#?(?:[.!\/\\\w]*))?)/,
|
||||
'string.is.uuid': /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
|
||||
const regexes: {[key: string]: RegExp} = {
|
||||
'string.is.alpha': /[a-zA-Z]*/,
|
||||
'string.is.alpha_num': /[a-zA-Z0-9]*/,
|
||||
'string.is.alpha_dash': /[a-zA-Z-]*/,
|
||||
'string.is.alpha_score': /[a-zA-Z_]*/,
|
||||
'string.is.alpha_num_dash_score': /[a-zA-Z\-_0-9]*/,
|
||||
'string.is.email': /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])/, // eslint-disable-line no-control-regex
|
||||
'string.is.ip': /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/,
|
||||
'string.is.ip.v4': /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
|
||||
'string.is.ip.v6': /(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))/,
|
||||
'string.is.mime': /^(?=[-a-z]{1,127}\/[-.a-z0-9]{1,127}$)[a-z]+(-[a-z]+)*\/[a-z0-9]+([-.][a-z0-9]+)*$/,
|
||||
'string.is.url': /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w\-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\\w]*))?)/,
|
||||
'string.is.uuid': /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
|
||||
}
|
||||
|
||||
function validateRex(key: string, inputValue: unknown, message: string): ValidationResult {
|
||||
if ( regexes[key].test(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
function validateRex(key: string, inputValue: any, message: string): ValidationResult {
|
||||
if ( regexes[key].test(inputValue) ) {
|
||||
return {
|
||||
valid: false,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters only. */
|
||||
function alpha(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha', inputValue, 'must be alphabetical only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphanumeric characters only. */
|
||||
function alphaNum(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "-" character only. */
|
||||
function alphaDash(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_dash', inputValue, 'must be alphabetical and dashes only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "_" character only. */
|
||||
function alphaScore(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_score', inputValue, 'must be alphabetical and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters, numeric characters, "-", or "_" only. */
|
||||
function alphaNumDashScore(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_num_dash_score', inputValue, 'must be alphanumeric, dashes, and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC email address format. */
|
||||
function email(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.email', inputValue, 'must be an email address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 or IPv6 address. */
|
||||
function ip(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.ip', inputValue, 'must be a valid IP address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 address. */
|
||||
function ipv4(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.ip.v4', inputValue, 'must be a valid IP version 4 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv6 address. */
|
||||
function ipv6(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.ip.v6', inputValue, 'must be a valid IP version 6 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid file MIME type. */
|
||||
function mime(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC URL format. */
|
||||
function url(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.url', inputValue, 'must be a valid URL')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC UUID format. */
|
||||
function uuid(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.uuid', inputValue, 'must be a valid UUID')
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input value to match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
function regex(rex: RegExp): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( rex.test(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters only. */
|
||||
export function alpha(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha', inputValue, 'must be alphabetical only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphanumeric characters only. */
|
||||
export function alphaNum(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "-" character only. */
|
||||
export function alphaDash(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_dash', inputValue, 'must be alphabetical and dashes only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "_" character only. */
|
||||
export function alphaScore(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_score', inputValue, 'must be alphabetical and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters, numeric characters, "-", or "_" only. */
|
||||
export function alphaNumDashScore(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_num_dash_score', inputValue, 'must be alphanumeric, dashes, and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC email address format. */
|
||||
export function email(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.email', inputValue, 'must be an email address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 or IPv6 address. */
|
||||
export function ip(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.ip', inputValue, 'must be a valid IP address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 address. */
|
||||
export function ipv4(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.ip.v4', inputValue, 'must be a valid IP version 4 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv6 address. */
|
||||
export function ipv6(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.ip.v6', inputValue, 'must be a valid IP version 6 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid file MIME type. */
|
||||
export function mime(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC URL format. */
|
||||
export function url(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.url', inputValue, 'must be a valid URL')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC UUID format. */
|
||||
export function uuid(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.uuid', inputValue, 'must be a valid UUID')
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input value to match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
export function regex(rex: RegExp): ValidatorFunction {
|
||||
return function regex(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( rex.test(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not valid'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input to NOT match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
export function notRegex(rex: RegExp): ValidatorFunction {
|
||||
return function notRegex(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !rex.test(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not valid'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to end with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
export function ends(substr: string): ValidatorFunction {
|
||||
return function ends(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).endsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${substr}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to begin with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
export function begins(substr: string): ValidatorFunction {
|
||||
return function begins(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).startsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${substr}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid JSON string. */
|
||||
export function json(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( isJSON(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be valid JSON'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly len many characters.
|
||||
* @param len
|
||||
*/
|
||||
export function length(len: number): ValidatorFunction {
|
||||
return function length(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue.length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least len many characters.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMin(len: number): ValidatorFunction {
|
||||
return function lengthMin(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue.length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most len many characters.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMax(len: number): ValidatorFunction {
|
||||
return function lengthMax(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue.length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`
|
||||
}
|
||||
message: 'is not valid',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input to NOT match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
function notRegex(rex: RegExp): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( !rex.test(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not valid',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to end with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
function ends(substr: string): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).endsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${substr}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to begin with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
function begins(substr: string): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).startsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${substr}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid JSON string. */
|
||||
function json(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( isJSON(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be valid JSON',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly len many characters.
|
||||
* @param len
|
||||
*/
|
||||
function length(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least len many characters.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMin(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most len many characters.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMax(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Str = {
|
||||
alpha,
|
||||
alphaNum,
|
||||
alphaDash,
|
||||
alphaScore,
|
||||
alphaNumDashScore,
|
||||
email,
|
||||
ip,
|
||||
ipv4,
|
||||
ipv6,
|
||||
mime,
|
||||
url,
|
||||
uuid,
|
||||
regex,
|
||||
notRegex,
|
||||
ends,
|
||||
begins,
|
||||
json,
|
||||
length,
|
||||
lengthMin,
|
||||
lengthMax,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import {UniversalPath} from '../../util'
|
||||
import {Template} from '../../cli'
|
||||
|
||||
const form_template: Template = {
|
||||
const templateForm: Template = {
|
||||
name: 'form',
|
||||
fileSuffix: '.form.ts',
|
||||
description: 'Create a new form request validator',
|
||||
baseAppPath: ['http', 'forms'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {FormRequest, ValidationRules, Rule} from '@extollo/forms'
|
||||
import {Injectable} from '@extollo/di'
|
||||
render(name: string) {
|
||||
return `import {Injectable, FormRequest, ValidationRules, Rule} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} object
|
||||
@@ -40,7 +38,7 @@ export class ${name}FormRequest extends FormRequest<${name}Form> {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { form_template }
|
||||
export { templateForm }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Singleton, Inject} from '../../di'
|
||||
import {CommandLine} from '../../cli'
|
||||
import {form_template} from '../templates/form'
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {templateForm} from '../templates/form'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
@Singleton()
|
||||
export class Forms extends Unit {
|
||||
@@ -13,6 +13,6 @@ export class Forms extends Unit {
|
||||
protected readonly logging!: Logging
|
||||
|
||||
public async up(): Promise<void> {
|
||||
this.cli.registerTemplate(form_template)
|
||||
this.cli.registerTemplate(templateForm)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
import {Request} from "./lifecycle/Request";
|
||||
import {AppClass} from '../lifecycle/AppClass'
|
||||
import {Request} from './lifecycle/Request'
|
||||
import {Container} from '../di'
|
||||
|
||||
/**
|
||||
* Base class for controllers that define methods that
|
||||
@@ -7,10 +8,12 @@ import {Request} from "./lifecycle/Request";
|
||||
*/
|
||||
export class Controller extends AppClass {
|
||||
constructor(
|
||||
protected readonly request: Request
|
||||
) { super() }
|
||||
protected readonly request: Request,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected container() {
|
||||
protected container(): Container {
|
||||
return this.request
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ErrorWithContext, HTTPStatus, HTTPMessage} from "../util"
|
||||
import {ErrorWithContext, HTTPStatus, HTTPMessage} from '../util'
|
||||
|
||||
/**
|
||||
* An error class that has an associated HTTP status.
|
||||
@@ -9,7 +9,7 @@ import {ErrorWithContext, HTTPStatus, HTTPMessage} from "../util"
|
||||
export class HTTPError extends ErrorWithContext {
|
||||
constructor(
|
||||
public readonly status: HTTPStatus = 500,
|
||||
public readonly message: string = ''
|
||||
public readonly message: string = '',
|
||||
) {
|
||||
super('HTTP ERROR')
|
||||
this.message = message || HTTPMessage[status]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {uninfer, infer, uuid_v4} from "../../util";
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {uninfer, infer, uuid4} from '../../util'
|
||||
|
||||
/**
|
||||
* Base type representing a parsed cookie.
|
||||
@@ -61,7 +61,7 @@ export class HTTPCookieJar {
|
||||
* @param value
|
||||
* @param options
|
||||
*/
|
||||
set(name: string, value: any, options?: HTTPCookieOptions) {
|
||||
set(name: string, value: unknown, options?: HTTPCookieOptions): this {
|
||||
this.parsed[name] = {
|
||||
key: name,
|
||||
value,
|
||||
@@ -69,14 +69,16 @@ export class HTTPCookieJar {
|
||||
exists: false,
|
||||
options,
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a cookie exists with the given name.
|
||||
* @param name
|
||||
*/
|
||||
has(name: string) {
|
||||
return !!this.parsed[name]
|
||||
has(name: string): boolean {
|
||||
return Boolean(this.parsed[name])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,17 +90,21 @@ export class HTTPCookieJar {
|
||||
* @param name
|
||||
* @param options
|
||||
*/
|
||||
clear(name: string, options?: HTTPCookieOptions) {
|
||||
if ( !options ) options = {}
|
||||
clear(name: string, options?: HTTPCookieOptions): this {
|
||||
if ( !options ) {
|
||||
options = {}
|
||||
}
|
||||
options.expires = new Date(0)
|
||||
|
||||
this.parsed[name] = {
|
||||
key: name,
|
||||
value: undefined,
|
||||
originalValue: uuid_v4(),
|
||||
originalValue: uuid4(),
|
||||
exists: false,
|
||||
options,
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,10 +114,14 @@ export class HTTPCookieJar {
|
||||
const headers: string[] = []
|
||||
|
||||
for ( const key in this.parsed ) {
|
||||
if ( !this.parsed.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.parsed, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const cookie = this.parsed[key]
|
||||
if ( cookie.exists ) continue
|
||||
if ( cookie.exists ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parts = []
|
||||
parts.push(`${key}=${encodeURIComponent(cookie.originalValue)}`)
|
||||
@@ -144,7 +154,7 @@ export class HTTPCookieJar {
|
||||
const map = {
|
||||
strict: 'Strict',
|
||||
lax: 'Lax',
|
||||
'none-secure': 'None; Secure'
|
||||
'none-secure': 'None; Secure',
|
||||
}
|
||||
|
||||
parts.push(map[cookie.options.sameSite])
|
||||
@@ -163,7 +173,9 @@ export class HTTPCookieJar {
|
||||
const parts = cookie.split('=')
|
||||
|
||||
const key = parts.shift()?.trim()
|
||||
if ( !key ) return;
|
||||
if ( !key ) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = decodeURI(parts.join('='))
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {Inject, Instantiable, Singleton} from "../../di"
|
||||
import {Collection, HTTPStatus} from "../../util"
|
||||
import {HTTPKernelModule} from "./HTTPKernelModule";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {error} from "../response/ErrorResponseFactory";
|
||||
import {Inject, Instantiable, Singleton} from '../../di'
|
||||
import {Collection, HTTPStatus} from '../../util'
|
||||
import {HTTPKernelModule} from './HTTPKernelModule'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {error} from '../response/ErrorResponseFactory'
|
||||
|
||||
/**
|
||||
* Interface for fluently registering kernel modules into the kernel.
|
||||
@@ -105,7 +105,8 @@ export class HTTPKernel extends AppClass {
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logging.error(e)
|
||||
await error(e).status(HTTPStatus.INTERNAL_SERVER_ERROR).write(request)
|
||||
await error(e).status(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
.write(request)
|
||||
}
|
||||
|
||||
this.logging.verbose('Finished kernel lifecycle')
|
||||
@@ -127,16 +128,16 @@ export class HTTPKernel extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(found_index, this.app().make(module))
|
||||
let foundIdx = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(foundIdx, this.app().make(module))
|
||||
return this
|
||||
} else {
|
||||
found_index = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
foundIdx = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
}
|
||||
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(found_index, this.app().make(module))
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(foundIdx, this.app().make(module))
|
||||
} else {
|
||||
throw new KernelModuleNotFoundError(other.name)
|
||||
}
|
||||
@@ -149,16 +150,16 @@ export class HTTPKernel extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(found_index + 1, this.app().make(module))
|
||||
let foundIdx = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(foundIdx + 1, this.app().make(module))
|
||||
return this
|
||||
} else {
|
||||
found_index = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
foundIdx = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
}
|
||||
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(found_index + 1, this.app().make(module))
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(foundIdx + 1, this.app().make(module))
|
||||
} else {
|
||||
throw new KernelModuleNotFoundError(other.name)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Injectable} from "../../di";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {HTTPKernel} from "./HTTPKernel";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {Injectable} from '../../di'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {HTTPKernel} from './HTTPKernel'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Base class for modules that define logic that is applied to requests
|
||||
@@ -23,7 +23,7 @@ export class HTTPKernelModule extends AppClass {
|
||||
* @param {Request} request
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
public async match(request: Request): Promise<boolean> {
|
||||
public async match(request: Request): Promise<boolean> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class HTTPKernelModule extends AppClass {
|
||||
* Register this module with the given HTTP kernel.
|
||||
* @param {HTTPKernel} kernel
|
||||
*/
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).before()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {plaintext} from "../../response/StringResponseFactory";
|
||||
import {ResponseFactory} from "../../response/ResponseFactory";
|
||||
import {json} from "../../response/JSONResponseFactory";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {plaintext} from '../../response/StringResponseFactory'
|
||||
import {ResponseFactory} from '../../response/ResponseFactory'
|
||||
import {json} from '../../response/JSONResponseFactory'
|
||||
|
||||
/**
|
||||
* Base class for HTTP kernel modules that apply some response from a route handler to the request.
|
||||
@@ -15,7 +15,7 @@ export abstract class AbstractResolvedRouteHandlerHTTPModule extends HTTPKernelM
|
||||
* @param request
|
||||
* @protected
|
||||
*/
|
||||
protected async applyResponseObject(object: ResponseObject, request: Request) {
|
||||
protected async applyResponseObject(object: ResponseObject, request: Request): Promise<void> {
|
||||
if ( (typeof object === 'string') || (typeof object === 'number') ) {
|
||||
object = plaintext(String(object))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {http} from "../../response/HTTPErrorResponseFactory";
|
||||
import {HTTPStatus} from "../../../util";
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {http} from '../../response/HTTPErrorResponseFactory'
|
||||
import {HTTPStatus} from '../../../util'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that runs the handler for the request's route.
|
||||
@@ -12,14 +12,14 @@ import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHan
|
||||
* In most cases, this is the controller method defined by the route.
|
||||
*/
|
||||
export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).core()
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
let object: ResponseObject = await route.handler(request)
|
||||
const object: ResponseObject = await route.handler(request)
|
||||
|
||||
await this.applyResponseObject(object, request)
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||
import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
import {PersistSessionHTTPModule} from './PersistSessionHTTPModule'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that executes the postflight handlers for the route.
|
||||
@@ -11,18 +11,18 @@ import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
|
||||
* Usually, this is post middleware.
|
||||
*/
|
||||
export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).before(PersistSessionHTTPModule)
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const postflight = route.postflight
|
||||
|
||||
for ( const handler of postflight ) {
|
||||
const result: ResponseObject = await handler(request)
|
||||
if ( typeof result !== "undefined" ) {
|
||||
if ( typeof result !== 'undefined' ) {
|
||||
await this.applyResponseObject(result, request)
|
||||
request.response.blockingWriteback(true)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {MountActivatedRouteHTTPModule} from "./MountActivatedRouteHTTPModule";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {MountActivatedRouteHTTPModule} from './MountActivatedRouteHTTPModule'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
|
||||
/**
|
||||
* HTTP Kernel module that executes the preflight handlers for the route.
|
||||
@@ -11,18 +11,18 @@ import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHan
|
||||
* Usually, this is the pre middleware.
|
||||
*/
|
||||
export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).after(MountActivatedRouteHTTPModule)
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const preflight = route.preflight
|
||||
|
||||
for ( const handler of preflight ) {
|
||||
const result: ResponseObject = await handler(request)
|
||||
if ( typeof result !== "undefined" ) {
|
||||
if ( typeof result !== 'undefined' ) {
|
||||
await this.applyResponseObject(result, request)
|
||||
request.response.blockingWriteback(true)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Injectable} from "../../../di"
|
||||
import {ErrorWithContext} from "../../../util"
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {SetSessionCookieHTTPModule} from "./SetSessionCookieHTTPModule";
|
||||
import {SessionFactory} from "../../session/SessionFactory";
|
||||
import {Session} from "../../session/Session";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Injectable} from '../../../di'
|
||||
import {ErrorWithContext} from '../../../util'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {SetSessionCookieHTTPModule} from './SetSessionCookieHTTPModule'
|
||||
import {SessionFactory} from '../../session/SessionFactory'
|
||||
import {Session} from '../../session/Session'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that creates the session using the configured driver
|
||||
@@ -15,11 +15,11 @@ import {Session} from "../../session/Session";
|
||||
export class InjectSessionHTTPModule extends HTTPKernelModule {
|
||||
public readonly executeWithBlockingWriteback = true
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).after(SetSessionCookieHTTPModule)
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
request.registerFactory(new SessionFactory())
|
||||
|
||||
const session = <Session> request.make(Session)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {Injectable, Inject} from "../../../di"
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Routing} from "../../../service/Routing";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {Logging} from "../../../service/Logging";
|
||||
import {Injectable, Inject} from '../../../di'
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Routing} from '../../../service/Routing'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that tries to find a registered route matching the request's
|
||||
@@ -20,7 +20,7 @@ export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).before()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule"
|
||||
import {HTTPKernel} from "../HTTPKernel"
|
||||
import * as Busboy from "busboy"
|
||||
import {Request} from "../../lifecycle/Request"
|
||||
import {infer, uuid_v4} from "../../../util"
|
||||
import {Files} from "../../../service/Files"
|
||||
import {Config} from "../../../service/Config"
|
||||
import {Logging} from "../../../service/Logging"
|
||||
import {Injectable, Inject, Container} from "../../../di"
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import * as Busboy from 'busboy'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {infer, uuid4} from '../../../util'
|
||||
import {Files} from '../../../service/Files'
|
||||
import {Config} from '../../../service/Config'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
import {Injectable, Inject, Container} from '../../../di'
|
||||
|
||||
@Injectable()
|
||||
export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
static register(kernel: HTTPKernel) {
|
||||
static register(kernel: HTTPKernel): void {
|
||||
const files = <Files> Container.getContainer().make(Files)
|
||||
const logging = <Logging> Container.getContainer().make(Logging)
|
||||
if ( !files.hasFilesystem() ) {
|
||||
@@ -32,8 +32,11 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
const contentType = request.getHeader('content-type')
|
||||
const contentTypes = (Array.isArray(contentType) ? contentType : [contentType])
|
||||
.filter(Boolean).map(x => x!.toLowerCase().split(';')[0])
|
||||
if ( !contentType ) return request
|
||||
.filter(Boolean).map(x => String(x).toLowerCase()
|
||||
.split(';')[0])
|
||||
if ( !contentType ) {
|
||||
return request
|
||||
}
|
||||
|
||||
if (
|
||||
contentTypes.includes('multipart/form-data')
|
||||
@@ -65,7 +68,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
try {
|
||||
const body = JSON.parse(data)
|
||||
for ( const key in body ) {
|
||||
if ( !body.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(body, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
request.parsedInput[key] = body[key]
|
||||
}
|
||||
res()
|
||||
@@ -94,8 +100,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
request.parsedInput[field] = infer(val)
|
||||
})
|
||||
|
||||
busboy.on('file', async (field, file, filename, encoding, mimetype) => {
|
||||
if ( !this.files.hasFilesystem() ) return
|
||||
busboy.on('file', async (field, file, filename, encoding, mimetype) => { // eslint-disable-line max-params
|
||||
if ( !this.files.hasFilesystem() ) {
|
||||
return
|
||||
}
|
||||
|
||||
if ( !config?.enable ) {
|
||||
this.logging.warn(`Skipping uploaded file '${filename}' because uploading is disabled. Set the server.uploads.enable config to allow uploads.`)
|
||||
@@ -122,7 +130,7 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
}
|
||||
|
||||
const fs = this.files.getFilesystem()
|
||||
const storePath = `${config.filesystemPrefix ? config.filesystemPrefix : ''}${(config.filesystemPrefix && !config.filesystemPrefix.endsWith('/')) ? '/' : ''}${field}-${uuid_v4()}`
|
||||
const storePath = `${config.filesystemPrefix ? config.filesystemPrefix : ''}${(config.filesystemPrefix && !config.filesystemPrefix.endsWith('/')) ? '/' : ''}${field}-${uuid4()}`
|
||||
this.logging.verbose(`Uploading file in field ${field} to ${fs.getPrefix()}${storePath}`)
|
||||
file.pipe(await fs.putStoreFileAsStream({ storePath })) // FIXME might need to revisit this to ensure we don't res() before pipe finishes
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Injectable} from "../../../di"
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Session} from "../../session/Session";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Injectable} from '../../../di'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Session} from '../../session/Session'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that runs after the main logic in the request to persist
|
||||
@@ -12,7 +12,7 @@ import {Session} from "../../session/Session";
|
||||
export class PersistSessionHTTPModule extends HTTPKernelModule {
|
||||
public readonly executeWithBlockingWriteback = true
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).last()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Injectable, Inject} from "../../../di"
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Config} from "../../../service/Config";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Injectable, Inject} from '../../../di'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Config} from '../../../service/Config'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that sets the `X-Powered-By` header.
|
||||
@@ -14,11 +14,11 @@ export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected readonly config!: Config;
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).after()
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( !this.config.get('server.poweredBy.hide', false) ) {
|
||||
request.response.setHeader('X-Powered-By', this.config.get('server.poweredBy.header', 'Extollo'))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Injectable, Inject} from "../../../di";
|
||||
import {uuid_v4} from "../../../util";
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Logging} from "../../../service/Logging";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Injectable, Inject} from '../../../di'
|
||||
import {uuid4} from '../../../util'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that tries to look up the session ID from the request.
|
||||
@@ -16,13 +16,13 @@ export class SetSessionCookieHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).first()
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( !request.cookies.has('extollo.session') ) {
|
||||
const session = `${uuid_v4()}-${uuid_v4()}`
|
||||
const session = `${uuid4()}-${uuid4()}`
|
||||
|
||||
this.logging.verbose(`Starting session: ${session}`)
|
||||
request.cookies.set('extollo.session', session)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Injectable, ScopedContainer, Container} from "../../di"
|
||||
import {infer, UniversalPath} from "../../util"
|
||||
import {IncomingMessage, ServerResponse} from "http"
|
||||
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
|
||||
import {TLSSocket} from "tls";
|
||||
import * as url from "url";
|
||||
import {Response} from "./Response";
|
||||
import * as Negotiator from "negotiator";
|
||||
import {Injectable, ScopedContainer, Container} from '../../di'
|
||||
import {infer, UniversalPath} from '../../util'
|
||||
import {IncomingMessage, ServerResponse} from 'http'
|
||||
import {HTTPCookieJar} from '../kernel/HTTPCookieJar'
|
||||
import {TLSSocket} from 'tls'
|
||||
import * as url from 'url'
|
||||
import {Response} from './Response'
|
||||
import * as Negotiator from 'negotiator'
|
||||
|
||||
/**
|
||||
* Enumeration of different HTTP verbs.
|
||||
@@ -17,8 +17,8 @@ export type HTTPMethod = 'post' | 'get' | 'patch' | 'put' | 'delete' | 'unknown'
|
||||
* Returns true if the given item is a valid HTTP verb.
|
||||
* @param what
|
||||
*/
|
||||
export function isHTTPMethod(what: any): what is HTTPMethod {
|
||||
return ['post', 'get', 'patch', 'put', 'delete'].includes(what)
|
||||
export function isHTTPMethod(what: unknown): what is HTTPMethod {
|
||||
return ['post', 'get', 'patch', 'put', 'delete'].includes(String(what))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +98,7 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
public readonly uploadedFiles: {[key: string]: UniversalPath} = {}
|
||||
|
||||
/** If true, the response lifecycle will not time out and send errors. */
|
||||
public bypassTimeout: boolean = false
|
||||
public bypassTimeout = false
|
||||
|
||||
constructor(
|
||||
/** The native Node.js request. */
|
||||
@@ -109,7 +109,7 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
) {
|
||||
super(Container.getContainer())
|
||||
|
||||
this.secure = !!(clientRequest.connection as TLSSocket).encrypted
|
||||
this.secure = Boolean((clientRequest.connection as TLSSocket).encrypted)
|
||||
|
||||
this.cookies = new HTTPCookieJar(this)
|
||||
this.url = String(clientRequest.url)
|
||||
@@ -137,6 +137,10 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
|
||||
const query: {[key: string]: any} = {}
|
||||
for ( const key in this.rawQueryData ) {
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.rawQueryData, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const value = this.rawQueryData[key]
|
||||
|
||||
if ( Array.isArray(value) ) {
|
||||
@@ -151,12 +155,11 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
this.query = query
|
||||
this.isXHR = String(this.clientRequest.headers['x-requested-with']).toLowerCase() === 'xmlhttprequest'
|
||||
|
||||
// @ts-ignore
|
||||
const {address = '0.0.0.0', family = 'IPv4', port = 0} = this.clientRequest.connection.address()
|
||||
const {address = '0.0.0.0', family = 'IPv4', port = 0} = this.clientRequest.connection.address() as any
|
||||
this.address = {
|
||||
address,
|
||||
family,
|
||||
port
|
||||
port,
|
||||
}
|
||||
|
||||
this.mediaTypes = (new Negotiator(clientRequest)).mediaTypes()
|
||||
@@ -164,12 +167,12 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
}
|
||||
|
||||
/** Get the value of a header, if it exists. */
|
||||
public getHeader(name: string) {
|
||||
public getHeader(name: string): string | string[] | undefined {
|
||||
return this.clientRequest.headers[name.toLowerCase()]
|
||||
}
|
||||
|
||||
/** Get the native Node.js IncomingMessage object. */
|
||||
public toNative() {
|
||||
public toNative(): IncomingMessage {
|
||||
return this.clientRequest
|
||||
}
|
||||
|
||||
@@ -177,7 +180,7 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
* Get the value of an input field on the request. Spans multiple input sources.
|
||||
* @param key
|
||||
*/
|
||||
public input(key?: string) {
|
||||
public input(key?: string): unknown {
|
||||
if ( !key ) {
|
||||
return {
|
||||
...this.parsedInput,
|
||||
@@ -206,17 +209,21 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
* Returns true if the request accepts the given media type.
|
||||
* @param type - a mimetype, or the short forms json, xml, or html
|
||||
*/
|
||||
accepts(type: string) {
|
||||
if ( type === 'json' ) type = 'application/json'
|
||||
else if ( type === 'xml' ) type = 'application/xml'
|
||||
else if ( type === 'html' ) type = 'text/html'
|
||||
accepts(type: string): boolean {
|
||||
if ( type === 'json' ) {
|
||||
type = 'application/json'
|
||||
} else if ( type === 'xml' ) {
|
||||
type = 'application/xml'
|
||||
} else if ( type === 'html' ) {
|
||||
type = 'text/html'
|
||||
}
|
||||
|
||||
type = type.toLowerCase()
|
||||
|
||||
const possible = [
|
||||
type,
|
||||
type.split('/')[0] + '/*',
|
||||
'*/*'
|
||||
'*/*',
|
||||
]
|
||||
|
||||
return this.mediaTypes.some(media => possible.includes(media.toLowerCase()))
|
||||
@@ -230,9 +237,15 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
const xmlIdx = this.mediaTypes.indexOf('application/xml') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*')
|
||||
const htmlIdx = this.mediaTypes.indexOf('text/html') ?? this.mediaTypes.indexOf('text/*') ?? this.mediaTypes.indexOf('*/*')
|
||||
|
||||
if ( htmlIdx >= 0 && htmlIdx <= jsonIdx && htmlIdx <= xmlIdx ) return 'html'
|
||||
if ( jsonIdx >= 0 && jsonIdx <= htmlIdx && jsonIdx <= xmlIdx ) return 'json'
|
||||
if ( xmlIdx >= 0 && xmlIdx <= jsonIdx && xmlIdx <= htmlIdx ) return 'xml'
|
||||
if ( htmlIdx >= 0 && htmlIdx <= jsonIdx && htmlIdx <= xmlIdx ) {
|
||||
return 'html'
|
||||
}
|
||||
if ( jsonIdx >= 0 && jsonIdx <= htmlIdx && jsonIdx <= xmlIdx ) {
|
||||
return 'json'
|
||||
}
|
||||
if ( xmlIdx >= 0 && xmlIdx <= jsonIdx && xmlIdx <= htmlIdx ) {
|
||||
return 'xml'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {Request} from "./Request";
|
||||
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "../../util"
|
||||
import {ServerResponse} from "http"
|
||||
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
|
||||
import {Request} from './Request'
|
||||
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from '../../util'
|
||||
import {ServerResponse} from 'http'
|
||||
import {HTTPCookieJar} from '../kernel/HTTPCookieJar'
|
||||
|
||||
/**
|
||||
* Error thrown when the server tries to re-send headers after they have been sent once.
|
||||
*/
|
||||
export class HeadersAlreadySentError extends ErrorWithContext {
|
||||
constructor(response: Response, headerName?: string) {
|
||||
super(`Cannot modify or re-send headers for this request as they have already been sent.`);
|
||||
super(`Cannot modify or re-send headers for this request as they have already been sent.`)
|
||||
this.context = { headerName }
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ export class HeadersAlreadySentError extends ErrorWithContext {
|
||||
* Error thrown when the server tries to re-send a response that has already been sent.
|
||||
*/
|
||||
export class ResponseAlreadySentError extends ErrorWithContext {
|
||||
constructor(response: Response) {
|
||||
super(`Cannot modify or re-send response as it has already ended.`);
|
||||
constructor(public readonly response: Response) {
|
||||
super(`Cannot modify or re-send response as it has already ended.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ export class Response {
|
||||
private headers: {[key: string]: string | string[]} = {}
|
||||
|
||||
/** True if the headers have been sent. */
|
||||
private _sentHeaders: boolean = false
|
||||
private sentHeaders = false
|
||||
|
||||
/** True if the response has been sent and closed. */
|
||||
private _responseEnded: boolean = false
|
||||
private responseEnded = false
|
||||
|
||||
/** The HTTP status code that should be sent to the client. */
|
||||
private _status: HTTPStatus = HTTPStatus.OK
|
||||
private status: HTTPStatus = HTTPStatus.OK
|
||||
|
||||
/**
|
||||
* If this is true, then some module in the kernel has flagged the response
|
||||
@@ -44,10 +44,10 @@ export class Response {
|
||||
* the response.
|
||||
* @private
|
||||
*/
|
||||
private _blockingWriteback: boolean = false
|
||||
private isBlockingWriteback = false
|
||||
|
||||
/** The body contents that should be written to the response. */
|
||||
public body: string = ''
|
||||
public body = ''
|
||||
|
||||
/**
|
||||
* Behavior subject fired right before the response content is written.
|
||||
@@ -68,14 +68,18 @@ export class Response {
|
||||
) { }
|
||||
|
||||
/** Get the currently set response status. */
|
||||
public getStatus() {
|
||||
return this._status
|
||||
public getStatus(): HTTPStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
/** Set a new response status. */
|
||||
public setStatus(status: HTTPStatus) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status')
|
||||
this._status = status
|
||||
public setStatus(status: HTTPStatus): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this, 'status')
|
||||
}
|
||||
|
||||
this.status = status
|
||||
return this
|
||||
}
|
||||
|
||||
/** Get the HTTPCookieJar for the client. */
|
||||
@@ -89,8 +93,11 @@ export class Response {
|
||||
}
|
||||
|
||||
/** Set the value of the response header. */
|
||||
public setHeader(name: string, value: string | string[]) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||
public setHeader(name: string, value: string | string[]): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this, name)
|
||||
}
|
||||
|
||||
this.headers[name] = value
|
||||
return this
|
||||
}
|
||||
@@ -99,9 +106,13 @@ export class Response {
|
||||
* Bulk set the specified headers in the response.
|
||||
* @param data
|
||||
*/
|
||||
public setHeaders(data: {[name: string]: string | string[]}) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this)
|
||||
this.headers = {...this.headers, ...data}
|
||||
public setHeaders(data: {[name: string]: string | string[]}): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this)
|
||||
}
|
||||
|
||||
this.headers = {...this.headers,
|
||||
...data}
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -110,67 +121,88 @@ export class Response {
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
public appendHeader(name: string, value: string | string[]) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||
if ( !Array.isArray(value) ) value = [value]
|
||||
public appendHeader(name: string, value: string | string[]): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this, name)
|
||||
}
|
||||
if ( !Array.isArray(value) ) {
|
||||
value = [value]
|
||||
}
|
||||
let existing = this.headers[name] ?? []
|
||||
if ( !Array.isArray(existing) ) existing = [existing]
|
||||
if ( !Array.isArray(existing) ) {
|
||||
existing = [existing]
|
||||
}
|
||||
|
||||
existing = [...existing, ...value]
|
||||
if ( existing.length === 1 ) existing = existing[0]
|
||||
if ( existing.length === 1 ) {
|
||||
existing = existing[0]
|
||||
}
|
||||
|
||||
this.headers[name] = existing
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the headers to the client.
|
||||
*/
|
||||
public sendHeaders() {
|
||||
public sendHeaders(): this {
|
||||
const headers = {} as any
|
||||
|
||||
const setCookieHeaders = this.cookies.getSetCookieHeaders()
|
||||
if ( setCookieHeaders.length ) headers['Set-Cookie'] = setCookieHeaders
|
||||
if ( setCookieHeaders.length ) {
|
||||
headers['Set-Cookie'] = setCookieHeaders
|
||||
}
|
||||
|
||||
for ( const key in this.headers ) {
|
||||
if ( !this.headers.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.headers, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
headers[key] = this.headers[key]
|
||||
}
|
||||
|
||||
this.serverResponse.writeHead(this._status, headers)
|
||||
this._sentHeaders = true
|
||||
this.serverResponse.writeHead(this.status, headers)
|
||||
this.sentHeaders = true
|
||||
return this
|
||||
}
|
||||
|
||||
/** Returns true if the headers have been sent. */
|
||||
public hasSentHeaders() {
|
||||
return this._sentHeaders
|
||||
public hasSentHeaders(): boolean {
|
||||
return this.sentHeaders
|
||||
}
|
||||
|
||||
/** Returns true if a body has been set in the response. */
|
||||
public hasBody() {
|
||||
return !!this.body
|
||||
public hasBody(): boolean {
|
||||
return Boolean(this.body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the flag for whether the writeback should be blocked.
|
||||
* @param set - if this is specified, the value will be set.
|
||||
*/
|
||||
public blockingWriteback(set?: boolean) {
|
||||
public blockingWriteback(set?: boolean): boolean {
|
||||
if ( typeof set !== 'undefined' ) {
|
||||
this._blockingWriteback = set
|
||||
this.isBlockingWriteback = set
|
||||
}
|
||||
|
||||
return this._blockingWriteback
|
||||
return this.isBlockingWriteback
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the headers and specified data to the client.
|
||||
* @param data
|
||||
*/
|
||||
public async write(data: any) {
|
||||
public async write(data: unknown): Promise<void> {
|
||||
return new Promise<void>((res, rej) => {
|
||||
if ( !this._sentHeaders ) this.sendHeaders()
|
||||
if ( !this.sentHeaders ) {
|
||||
this.sendHeaders()
|
||||
}
|
||||
this.serverResponse.write(data, error => {
|
||||
if ( error ) rej(error)
|
||||
else res()
|
||||
if ( error ) {
|
||||
rej(error)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -178,7 +210,7 @@ export class Response {
|
||||
/**
|
||||
* Send the response to the client, writing the headers and configured body.
|
||||
*/
|
||||
public async send() {
|
||||
public async send(): Promise<void> {
|
||||
await this.sending$.next(this)
|
||||
this.setHeader('Content-Length', String(this.body?.length ?? 0))
|
||||
await this.write(this.body ?? '')
|
||||
@@ -189,10 +221,14 @@ export class Response {
|
||||
/**
|
||||
* Mark the response as ended and close the socket.
|
||||
*/
|
||||
public end() {
|
||||
if ( this._responseEnded ) throw new ResponseAlreadySentError(this)
|
||||
this._sentHeaders = true
|
||||
public end(): this {
|
||||
if ( this.responseEnded ) {
|
||||
throw new ResponseAlreadySentError(this)
|
||||
}
|
||||
|
||||
this.sentHeaders = true
|
||||
this.serverResponse.end()
|
||||
return this
|
||||
}
|
||||
|
||||
// location?
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ResponseFactory} from "./ResponseFactory"
|
||||
import {Rehydratable} from "../../util"
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Rehydratable} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function that creates a DehydratedStateResponseFactory.
|
||||
@@ -15,10 +15,12 @@ export function dehydrate(value: Rehydratable): DehydratedStateResponseFactory {
|
||||
*/
|
||||
export class DehydratedStateResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly rehydratable: Rehydratable
|
||||
) { super() }
|
||||
public readonly rehydratable: Rehydratable,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.body = JSON.stringify(this.rehydratable.dehydrate())
|
||||
request.response.setHeader('Content-Type', 'application/json')
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import {ResponseFactory} from "./ResponseFactory"
|
||||
import {ErrorWithContext, HTTPStatus} from "../../util"
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import * as api from "./api"
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {ErrorWithContext, HTTPStatus} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import * as api from './api'
|
||||
|
||||
/**
|
||||
* Helper to create a new ErrorResponseFactory, with the given HTTP status and output format.
|
||||
* @param error
|
||||
* @param thrownError
|
||||
* @param status
|
||||
* @param output
|
||||
*/
|
||||
export function error(
|
||||
error: Error | string,
|
||||
thrownError: Error | string,
|
||||
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
output: 'json' | 'html' | 'auto' = 'auto'
|
||||
output: 'json' | 'html' | 'auto' = 'auto',
|
||||
): ErrorResponseFactory {
|
||||
if ( typeof error === 'string' ) error = new Error(error)
|
||||
return new ErrorResponseFactory(error, status, output)
|
||||
if ( typeof thrownError === 'string' ) {
|
||||
thrownError = new Error(thrownError)
|
||||
}
|
||||
return new ErrorResponseFactory(thrownError, status, output)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,9 +27,9 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
protected targetMode: 'json' | 'html' | 'auto' = 'auto'
|
||||
|
||||
constructor(
|
||||
public readonly error: Error,
|
||||
public readonly thrownError: Error,
|
||||
status: HTTPStatus,
|
||||
output: 'json' | 'html' | 'auto' = 'auto'
|
||||
output: 'json' | 'html' | 'auto' = 'auto',
|
||||
) {
|
||||
super()
|
||||
this.status(status)
|
||||
@@ -39,16 +41,16 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
return this
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
const wants = request.wants()
|
||||
|
||||
if ( this.targetMode === 'json' || (this.targetMode === 'auto' && wants === 'json') ) {
|
||||
request.response.setHeader('Content-Type', 'application/json')
|
||||
request.response.body = this.buildJSON(this.error)
|
||||
request.response.body = this.buildJSON(this.thrownError)
|
||||
} else if ( this.targetMode === 'html' || (this.targetMode === 'auto' && (wants === 'html' || wants === 'unknown')) ) {
|
||||
request.response.setHeader('Content-Type', 'text/html')
|
||||
request.response.body = this.buildHTML(this.error)
|
||||
request.response.body = this.buildHTML(this.thrownError)
|
||||
}
|
||||
|
||||
// FIXME XML support
|
||||
@@ -61,12 +63,12 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
* @param {Error} error
|
||||
* @return string
|
||||
*/
|
||||
protected buildHTML(error: Error) {
|
||||
protected buildHTML(thrownError: Error): string {
|
||||
let context: any
|
||||
if ( error instanceof ErrorWithContext ) {
|
||||
context = error.context
|
||||
if ( error.originalError ) {
|
||||
error = error.originalError
|
||||
if ( thrownError instanceof ErrorWithContext ) {
|
||||
context = thrownError.context
|
||||
if ( thrownError.originalError ) {
|
||||
thrownError = thrownError.originalError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +76,11 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
<b>Sorry, an unexpected error occurred while processing your request.</b>
|
||||
<br>
|
||||
<pre><code>
|
||||
Name: ${error.name}
|
||||
Message: ${error.message}
|
||||
Name: ${thrownError.name}
|
||||
Message: ${thrownError.message}
|
||||
Stack trace:
|
||||
- ${error.stack ? error.stack.split(/\s+at\s+/).slice(1).join('<br> - ') : 'none'}
|
||||
- ${thrownError.stack ? thrownError.stack.split(/\s+at\s+/).slice(1)
|
||||
.join('<br> - ') : 'none'}
|
||||
</code></pre>
|
||||
`
|
||||
|
||||
@@ -85,7 +88,8 @@ Stack trace:
|
||||
str += `
|
||||
<pre><code>
|
||||
Context:
|
||||
${Object.keys(context).map(key => ` - ${key} : ${context[key]}`).join('\n')}
|
||||
${Object.keys(context).map(key => ` - ${key} : ${context[key]}`)
|
||||
.join('\n')}
|
||||
</code></pre>
|
||||
`
|
||||
}
|
||||
@@ -93,7 +97,7 @@ ${Object.keys(context).map(key => ` - ${key} : ${context[key]}`).join('\n')}
|
||||
return str
|
||||
}
|
||||
|
||||
protected buildJSON(error: Error) {
|
||||
return JSON.stringify(api.error(error))
|
||||
protected buildJSON(thrownError: Error): string {
|
||||
return JSON.stringify(api.error(thrownError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function that creates a new HTMLResponseFactory.
|
||||
@@ -15,9 +15,11 @@ export function html(value: string): HTMLResponseFactory {
|
||||
export class HTMLResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly value: string,
|
||||
) { super() }
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
request.response.body = this.value
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ErrorResponseFactory} from "./ErrorResponseFactory";
|
||||
import {HTTPError} from "../HTTPError";
|
||||
import {HTTPStatus} from "../../util"
|
||||
import {ErrorResponseFactory} from './ErrorResponseFactory'
|
||||
import {HTTPError} from '../HTTPError'
|
||||
import {HTTPStatus} from '../../util'
|
||||
|
||||
/**
|
||||
* Helper that generates a new HTTPErrorResponseFactory given the HTTP status and message.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function to create a new JSONResponseFactory of the given value.
|
||||
* @param value
|
||||
*/
|
||||
export function json(value: any): JSONResponseFactory {
|
||||
export function json(value: unknown): JSONResponseFactory {
|
||||
return new JSONResponseFactory(value)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@ export function json(value: any): JSONResponseFactory {
|
||||
*/
|
||||
export class JSONResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly value: any
|
||||
) { super() }
|
||||
public readonly value: unknown,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Content-Type', 'application/json')
|
||||
request.response.body = JSON.stringify(this.value)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {HTTPStatus} from "../../util"
|
||||
import {Request} from "../lifecycle/Request"
|
||||
import {HTTPStatus} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Abstract class that defines "factory" that knows how to write a particular
|
||||
@@ -19,7 +19,7 @@ export abstract class ResponseFactory {
|
||||
}
|
||||
|
||||
/** Set the target status of this factory. */
|
||||
public status(status: HTTPStatus) {
|
||||
public status(status: HTTPStatus): this {
|
||||
this.targetStatus = status
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function that creates a new StringResponseFactory for the given string value.
|
||||
@@ -16,9 +16,11 @@ export class StringResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
/** The string to write as the body. */
|
||||
public readonly value: string,
|
||||
) { super() }
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Content-Type', 'text/plain')
|
||||
request.response.body = this.value
|
||||
|
||||