4 Commits
0.2.1 ... 0.3.0

Author SHA1 Message Date
0b86d796e8 version 0.3.0
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2021-06-02 22:41:26 -05:00
1d5056b753 Setup eslint and enforce rules
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-02 22:36:25 -05:00
82e7a1f299 Add docs build pipeline to drone config
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2021-06-01 22:21:29 -05:00
4849016784 Move docs in-repo
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-01 21:32:24 -05:00
184 changed files with 9453 additions and 3120 deletions

View File

@@ -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 kind: pipeline
name: default name: default
type: docker type: docker

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
lib
dist

113
.eslintrc.json Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
www*

31
docs/HOME.md Normal file
View 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.

View File

@@ -0,0 +1 @@
# About the Extollo Project

View 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
View File

@@ -0,0 +1,6 @@
[
{
"pattern": "^",
"replace": "https://code.garrettmills.dev/extollo/lib/src/branch/master/"
}
]

BIN
docs/static/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

19
docs/static/humans.txt vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

64
docs/theme/assets/css/pages.css vendored Normal file
View 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

Binary file not shown.

Binary file not shown.

BIN
docs/theme/assets/images/icons.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
docs/theme/assets/images/icons@2x.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

BIN
docs/theme/assets/images/widgets@2x.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

1
docs/theme/assets/js/main.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View 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

View 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

View 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

View 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

View 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
View 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
View 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
View 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}}&nbsp;
{{/ifCond}}
{{model.name}}
{{#if model.typeParameters}}
&lt;
{{#each model.typeParameters}}
{{#if @index}},&nbsp;{{/if}}
{{name}}
{{/each}}
&gt;
{{/if}}
{{/compact}}</h1>
</div>
</div>
</header>

View File

@@ -0,0 +1,3 @@
<div class="tsd-panel tsd-typography">
{{#markdown}}{{{model.pagesPlugin.item.contents}}}{{/markdown}}
</div>

1338
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@extollo/lib", "name": "@extollo/lib",
"version": "0.2.1", "version": "0.3.0",
"description": "The framework library that lifts up your code.", "description": "The framework library that lifts up your code.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
@@ -30,14 +30,21 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ssh2": "^1.1.0", "ssh2": "^1.1.0",
"ts-node": "^9.1.1", "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", "typescript": "^4.2.3",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"prebuild": "pnpm run lint",
"build": "tsc", "build": "tsc",
"app": "tsc && node lib/index.js", "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": [ "files": [
"lib/**/*" "lib/**/*"
@@ -49,5 +56,10 @@
"url": "https://code.garrettmills.dev/extollo/lib" "url": "https://code.garrettmills.dev/extollo/lib"
}, },
"author": "garrettmills <shout@garrettmills.dev>", "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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
import {Injectable, Inject} from "../di" import {Injectable, Inject} from '../di'
import {infer, ErrorWithContext} from "../util" import {infer, ErrorWithContext} from '../util'
import {CLIOption} from "./directive/options/CLIOption" import {CLIOption} from './directive/options/CLIOption'
import {PositionalOption} from "./directive/options/PositionalOption"; import {PositionalOption} from './directive/options/PositionalOption'
import {FlagOption} from "./directive/options/FlagOption"; import {FlagOption} from './directive/options/FlagOption'
import {AppClass} from "../lifecycle/AppClass"; import {AppClass} from '../lifecycle/AppClass'
import {Logging} from "../service/Logging"; import {Logging} from '../service/Logging'
/** /**
* Type alias for a definition of a command-line option. * Type alias for a definition of a command-line option.
@@ -35,7 +35,7 @@ export abstract class Directive extends AppClass {
protected readonly logging!: Logging protected readonly logging!: Logging
/** Parsed option values. */ /** Parsed option values. */
private _optionValues: any private optionValues: any
/** /**
* Get the keyword or array of keywords that will specify this directive. * Get the keyword or array of keywords that will specify this directive.
@@ -84,8 +84,8 @@ export abstract class Directive extends AppClass {
* @param optionValues * @param optionValues
* @private * @private
*/ */
private _setOptionValues(optionValues: any) { private setOptionValues(optionValues: any) {
this._optionValues = optionValues; this.optionValues = optionValues
} }
/** /**
@@ -93,9 +93,9 @@ export abstract class Directive extends AppClass {
* @param name * @param name
* @param defaultValue * @param defaultValue
*/ */
public option(name: string, defaultValue?: any) { public option(name: string, defaultValue?: unknown): any {
if ( name in this._optionValues ) { if ( name in this.optionValues ) {
return this._optionValues[name] return this.optionValues[name]
} }
return defaultValue return defaultValue
@@ -110,20 +110,28 @@ export abstract class Directive extends AppClass {
* *
* @param argv * @param argv
*/ */
async invoke(argv: string[]) { async invoke(argv: string[]): Promise<void> {
const options = this.getResolvedOptions() const options = this.getResolvedOptions()
if ( this.didRequestUsage(argv) ) { if ( this.didRequestUsage(argv) ) {
// @ts-ignore const positionalArguments: PositionalOption<any>[] = []
const positionalArguments: PositionalOption<any>[] = options.filter(opt => opt instanceof PositionalOption) options.forEach(opt => {
if ( opt instanceof PositionalOption ) {
positionalArguments.push(opt)
}
})
// @ts-ignore const flagArguments: FlagOption<any>[] = []
const flagArguments: FlagOption<any>[] = options.filter(opt => opt instanceof FlagOption) options.forEach(opt => {
if ( opt instanceof FlagOption ) {
flagArguments.push(opt)
}
})
const positionalDisplay: string = positionalArguments.map(x => `<${x.getArgumentName()}>`).join(' ') const positionalDisplay: string = positionalArguments.map(x => `<${x.getArgumentName()}>`).join(' ')
const flagDisplay: string = flagArguments.length ? ' [...flags]' : '' const flagDisplay: string = flagArguments.length ? ' [...flags]' : ''
console.log([ this.nativeOutput([
'', '',
`DIRECTIVE: ${this.getMainKeyword()} - ${this.getDescription()}`, `DIRECTIVE: ${this.getMainKeyword()} - ${this.getDescription()}`,
'', '',
@@ -131,7 +139,7 @@ export abstract class Directive extends AppClass {
].join('\n')) ].join('\n'))
if ( positionalArguments.length ) { if ( positionalArguments.length ) {
console.log([ this.nativeOutput([
'', '',
`POSITIONAL ARGUMENTS:`, `POSITIONAL ARGUMENTS:`,
...(positionalArguments.map(arg => { ...(positionalArguments.map(arg => {
@@ -141,7 +149,7 @@ export abstract class Directive extends AppClass {
} }
if ( flagArguments.length ) { if ( flagArguments.length ) {
console.log([ this.nativeOutput([
'', '',
`FLAGS:`, `FLAGS:`,
...(flagArguments.map(arg => { ...(flagArguments.map(arg => {
@@ -152,34 +160,34 @@ export abstract class Directive extends AppClass {
const help = this.getHelpText() const help = this.getHelpText()
if ( help ) { if ( help ) {
console.log('\n' + help) this.nativeOutput('\n' + help)
} }
console.log('\n') this.nativeOutput('\n')
} else { } else {
try { try {
const optionValues = this.parseOptions(options, argv) const optionValues = this.parseOptions(options, argv)
this._setOptionValues(optionValues) this.setOptionValues(optionValues)
await this.handle(argv) await this.handle(argv)
} catch (e) { } catch (e) {
console.error(e.message) this.nativeOutput(e.message)
if ( e instanceof OptionValidationError ) { if ( e instanceof OptionValidationError ) {
// expecting, value, requirements // expecting, value, requirements
if ( e.context.expecting ) { 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) ) { if ( e.context.requirements && Array.isArray(e.context.requirements) ) {
for ( const req of e.context.requirements ) { for ( const req of e.context.requirements ) {
console.error(` - ${req}`) this.nativeOutput(` - ${req}`)
} }
} }
if ( e.context.value ) { 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. * Returns true if the given keyword should invoke this directive.
* @param name * @param name
*/ */
public matchesKeyword(name: string) { public matchesKeyword(name: string): boolean {
let kws = this.getKeywords() let kws = this.getKeywords()
if ( !Array.isArray(kws) ) kws = [kws] if ( !Array.isArray(kws) ) {
kws = [kws]
}
return kws.includes(name) return kws.includes(name)
} }
@@ -227,7 +237,7 @@ export abstract class Directive extends AppClass {
* Print the given output to the log as success text. * Print the given output to the log as success text.
* @param output * @param output
*/ */
success(output: any) { success(output: unknown): void {
this.logging.success(output, true) 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. * Print the given output to the log as error text.
* @param output * @param output
*/ */
error(output: any) { error(output: unknown): void {
this.logging.error(output, true) 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. * Print the given output to the log as warning text.
* @param output * @param output
*/ */
warn(output: any) { warn(output: unknown): void {
this.logging.warn(output, true) 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. * Print the given output to the log as info text.
* @param output * @param output
*/ */
info(output: any) { info(output: unknown): void {
this.logging.info(output, true) 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. * Print the given output to the log as debugging text.
* @param output * @param output
*/ */
debug(output: any) { debug(output: unknown): void {
this.logging.debug(output, true) 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. * Print the given output to the log as verbose text.
* @param output * @param output
*/ */
verbose(output: any) { verbose(output: unknown): void {
this.logging.verbose(output, true) 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' * Get the flag option that signals help. Usually, this is named 'help'
* and supports the flags '--help' and '-?'. * and supports the flags '--help' and '-?'.
*/ */
getHelpOption() { getHelpOption(): FlagOption<any> {
return new FlagOption('--help', '-?', 'usage information about this directive') 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 * Process the raw CLI arguments using an array of option class instances to build
* a mapping of option names to provided values. * a mapping of option names to provided values.
*/ */
parseOptions(options: CLIOption<any>[], args: string[]) { parseOptions(options: CLIOption<any>[], args: string[]): {[key: string]: any} {
// @ts-ignore let positionalArguments: PositionalOption<any>[] = []
let positionalArguments: PositionalOption<any>[] = options.filter(cls => cls instanceof PositionalOption) 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 = {} const optionValue: any = {}
flagArguments.push(this.getHelpOption()) flagArguments.push(this.getHelpOption())
@@ -325,7 +344,7 @@ export abstract class Directive extends AppClass {
const flagArgument = flagArguments.filter(x => x.shortFlag === value) const flagArgument = flagArguments.filter(x => x.shortFlag === value)
if ( flagArgument.length < 1 ) { if ( flagArgument.length < 1 ) {
throw new OptionValidationError(`Unknown flag argument: ${value}`, { throw new OptionValidationError(`Unknown flag argument: ${value}`, {
value value,
}) })
} else { } else {
if ( flagArgument[0].argumentDescription ) { if ( flagArgument[0].argumentDescription ) {
@@ -350,7 +369,7 @@ export abstract class Directive extends AppClass {
} else { } else {
if ( positionalArguments.length < 1 ) { if ( positionalArguments.length < 1 ) {
throw new OptionValidationError(`Unknown positional argument: ${value}`, { throw new OptionValidationError(`Unknown positional argument: ${value}`, {
value value,
}) })
} else { } else {
const inferredValue = infer(value) const inferredValue = infer(value)
@@ -368,13 +387,13 @@ export abstract class Directive extends AppClass {
if ( expectingFlagArgument ) { if ( expectingFlagArgument ) {
throw new OptionValidationError(`Missing argument for flag: ${positionalFlagName}`, { throw new OptionValidationError(`Missing argument for flag: ${positionalFlagName}`, {
expecting: positionalFlagName expecting: positionalFlagName,
}) })
} }
if ( positionalArguments.length > 0 ) { if ( positionalArguments.length > 0 ) {
throw new OptionValidationError(`Missing required argument: ${positionalArguments[0].getArgumentName()}`, { 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. * 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 * @returns {boolean} - true if the help flag appeared
*/ */
didRequestUsage(argv: string[]) { didRequestUsage(argv: string[]): boolean {
const help_option = this.getHelpOption() const helpOption = this.getHelpOption()
for ( const arg of argv ) { 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 true
} }
} }
return false return false
} }
protected nativeOutput(...outputs: any[]): void {
console.log(...outputs) // eslint-disable-line no-console
}
} }

View File

@@ -1,8 +1,8 @@
import {Directive} from "../Directive" import {Directive} from '../Directive'
import {CommandLineApplication} from "../service" import {CommandLineApplication} from '../service'
import {Injectable} from "../../di" import {Injectable} from '../../di'
import {ErrorWithContext} from "../../util" import {ErrorWithContext} from '../../util'
import {Unit} from "../../lifecycle/Unit"; import {Unit} from '../../lifecycle/Unit'
/** /**
* A directive that starts the framework's final target normally. * A directive that starts the framework's final target normally.

View File

@@ -1,7 +1,7 @@
import {Directive} from "../Directive" import {Directive} from '../Directive'
import * as colors from "colors/safe" import * as colors from 'colors/safe'
import * as repl from 'repl' import * as repl from 'repl'
import {DependencyKey} from "../../di"; import {DependencyKey} from '../../di'
/** /**
* Launch an interactive REPL shell from within the application. * Launch an interactive REPL shell from within the application.
@@ -9,7 +9,7 @@ import {DependencyKey} from "../../di";
*/ */
export class ShellDirective extends Directive { export class ShellDirective extends Directive {
protected options: any = { 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(') ➤ ')}`, prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`,
} }
@@ -38,7 +38,7 @@ export class ShellDirective extends Directive {
} }
await new Promise<void>(res => { await new Promise<void>(res => {
console.log(this.options.welcome) this.nativeOutput(this.options.welcome)
this.repl = repl.start(this.options.prompt) this.repl = repl.start(this.options.prompt)
Object.assign(this.repl.context, state) Object.assign(this.repl.context, state)
this.repl.on('exit', () => res()) this.repl.on('exit', () => res())

View File

@@ -1,8 +1,8 @@
import {Directive, OptionDefinition} from "../Directive" import {Directive, OptionDefinition} from '../Directive'
import {PositionalOption} from "./options/PositionalOption" import {PositionalOption} from './options/PositionalOption'
import {CommandLine} from "../service" import {CommandLine} from '../service'
import {Inject, Injectable} from "../../di"; import {Inject, Injectable} from '../../di'
import {ErrorWithContext} from "../../util"; import {ErrorWithContext} from '../../util'
/** /**
* Create a new file based on a template registered with the CommandLine service. * 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 => { ...(registeredTemplates.map(template => {
return ` - ${template.name}: ${template.description}` return ` - ${template.name}: ${template.description}`
}).all()) }).all()),
].join('\n') ].join('\n')
} }
async handle(argv: string[]) { async handle(): Promise<void> {
const templateName: string = this.option('template_name') const templateName: string = this.option('template_name')
const destinationName: string = this.option('file_name') const destinationName: string = this.option('file_name')

View File

@@ -1,7 +1,7 @@
import {Directive} from "../Directive" import {Directive} from '../Directive'
import {Injectable, Inject} from "../../di" import {Injectable, Inject} from '../../di'
import {padRight} from "../../util" import {padRight} from '../../util'
import {CommandLine} from "../service" import {CommandLine} from '../service'
/** /**
* Directive that prints the help message and usage information about * 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' return 'print information about available commands'
} }
public handle(argv: string[]): void | Promise<void> { public handle(): void | Promise<void> {
const directiveStrings = this.cli.getDirectives() const directiveStrings = this.cli.getDirectives()
.map(cls => this.make<Directive>(cls)) .map(cls => this.make<Directive>(cls))
.map<[string, string]>(dir => { .map<[string, string]>(dir => {
@@ -37,8 +37,8 @@ export class UsageDirective extends Directive {
}) })
.toArray() .toArray()
console.log(this.cli.getASCIILogo()) this.nativeOutput(this.cli.getASCIILogo())
console.log([ this.nativeOutput([
'', '',
'Welcome to Extollo! Specify a command to get started.', '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.', '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')) ].join('\n'))
} }
} }

View File

@@ -9,200 +9,207 @@ export abstract class CLIOption<T> {
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _useWhitelist: boolean = false protected useWhitelist = false
/** /**
* Do we use the blacklist? * Do we use the blacklist?
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _useBlacklist: boolean = false protected useBlacklist = false
/** /**
* Do we use the less-than comparison? * Do we use the less-than comparison?
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _useLessThan: boolean = false protected useLessThan = false
/** /**
* Do we use the greater-than comparison? * Do we use the greater-than comparison?
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _useGreaterThan: boolean = false protected useGreaterThan = false
/** /**
* Do we use the equality operator? * Do we use the equality operator?
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _useEquality: boolean = false protected useEquality = false
/** /**
* Is this option optional? * Is this option optional?
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _optional: boolean = false protected isOptional = false
/** /**
* Whitelisted values. * Whitelisted values.
* @type {Array<*>} * @type {Array<*>}
* @private * @private
*/ */
protected _whitelist: T[] = [] protected whitelistItems: T[] = []
/** /**
* Blacklisted values. * Blacklisted values.
* @type {Array<*>} * @type {Array<*>}
* @private * @private
*/ */
protected _blacklist: T[] = [] protected blacklistItems: T[] = []
/** /**
* Value to be compared in less than. * Value to be compared in less than.
* @type {*} * @type {*}
* @private * @private
*/ */
protected _lessThanValue?: T protected lessThanValue?: T
/** /**
* If true, the less than will be less than or equal to. * If true, the less than will be less than or equal to.
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _lessThanBit: boolean = false protected lessThanBit = false
/** /**
* Value to be compared in greater than. * Value to be compared in greater than.
* @type {*} * @type {*}
* @private * @private
*/ */
protected _greaterThanValue?: T protected greaterThanValue?: T
/** /**
* If true, the greater than will be greater than or equal to. * If true, the greater than will be greater than or equal to.
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
protected _greateerThanBit: boolean = false protected greaterThanBit = false
/** /**
* The value to be used to check equality. * The value to be used to check equality.
* @type {*} * @type {*}
* @private * @private
*/ */
protected _equalityValue?: T protected equalityValue?: T
/** /**
* Whitelist the specified item or items and enable the whitelist. * Whitelist the specified item or items and enable the whitelist.
* @param {...*} items - the items to whitelist * @param {...*} items - the items to whitelist
*/ */
whitelist(...items: T[]) { whitelist(...items: T[]): this {
this._useWhitelist = true this.useWhitelist = true
items.forEach(item => this._whitelist.push(item)) items.forEach(item => this.whitelistItems.push(item))
return this
} }
/** /**
* Blacklist the specified item or items and enable the blacklist. * Blacklist the specified item or items and enable the blacklist.
* @param {...*} items - the items to blacklist * @param {...*} items - the items to blacklist
*/ */
blacklist(...items: T[]) { blacklist(...items: T[]): this {
this._useBlacklist = true this.useBlacklist = true
items.forEach(item => this._blacklist.push(item)) items.forEach(item => this.blacklistItems.push(item))
return this
} }
/** /**
* Specifies the value to be used in less-than comparison and enables less-than comparison. * Specifies the value to be used in less-than comparison and enables less-than comparison.
* @param {*} value * @param {*} value
*/ */
lessThan(value: T) { lessThan(value: T): this {
this._useLessThan = true this.useLessThan = true
this._lessThanValue = value this.lessThanValue = value
return this
} }
/** /**
* Specifies the value to be used in less-than or equal-to comparison and enables that comparison. * Specifies the value to be used in less-than or equal-to comparison and enables that comparison.
* @param {*} value * @param {*} value
*/ */
lessThanOrEqualTo(value: T) { lessThanOrEqualTo(value: T): this {
this._lessThanBit = true this.lessThanBit = true
this.lessThan(value) this.lessThan(value)
return this
} }
/** /**
* Specifies the value to be used in greater-than comparison and enables that comparison. * Specifies the value to be used in greater-than comparison and enables that comparison.
* @param {*} value * @param {*} value
*/ */
greaterThan(value: T) { greaterThan(value: T): this {
this._useGreaterThan = true this.useGreaterThan = true
this._greaterThanValue = value this.greaterThanValue = value
return this
} }
/** /**
* Specifies the value to be used in greater-than or equal-to comparison and enables that comparison. * Specifies the value to be used in greater-than or equal-to comparison and enables that comparison.
* @param {*} value * @param {*} value
*/ */
greaterThanOrEqualTo(value: T) { greaterThanOrEqualTo(value: T): this {
this._greateerThanBit = true this.greaterThanBit = true
this.greaterThan(value) this.greaterThan(value)
return this
} }
/** /**
* Specifies the value to be used in equality comparison and enables that comparison. * Specifies the value to be used in equality comparison and enables that comparison.
* @param {*} value * @param {*} value
*/ */
equals(value: T) { equals(value: T): this {
this._useEquality = true this.useEquality = true
this._equalityValue = value this.equalityValue = value
return this
} }
/** /**
* Checks if the specified value passes the configured comparisons. * Checks if the specified value passes the configured comparisons.
* @param {*} value * @param value
* @returns {boolean} * @returns {boolean}
*/ */
validate(value: any) { validate(value: T): boolean {
let is_valid = true let isValid = true
if ( this._useEquality ) { if ( this.useEquality ) {
is_valid = is_valid && (this._equalityValue === value) isValid = isValid && (this.equalityValue === value)
} }
if ( this._useLessThan && typeof this._lessThanValue !== 'undefined' ) { if ( this.useLessThan && typeof this.lessThanValue !== 'undefined' ) {
if ( this._lessThanBit ) { if ( this.lessThanBit ) {
is_valid = is_valid && (value <= this._lessThanValue) isValid = isValid && (value <= this.lessThanValue)
} else { } else {
is_valid = is_valid && (value < this._lessThanValue) isValid = isValid && (value < this.lessThanValue)
} }
} }
if ( this._useGreaterThan && typeof this._greaterThanValue !== 'undefined' ) { if ( this.useGreaterThan && typeof this.greaterThanValue !== 'undefined' ) {
if ( this._greateerThanBit ) { if ( this.greaterThanBit ) {
is_valid = is_valid && (value >= this._greaterThanValue) isValid = isValid && (value >= this.greaterThanValue)
} else { } else {
is_valid = is_valid && (value > this._greaterThanValue) isValid = isValid && (value > this.greaterThanValue)
} }
} }
if ( this._useWhitelist ) { if ( this.useWhitelist ) {
is_valid = is_valid && this._whitelist.some(x => { isValid = isValid && this.whitelistItems.some(x => {
return x === value return x === value
}) })
} }
if ( this._useBlacklist ) { if ( this.useBlacklist ) {
is_valid = is_valid && !(this._blacklist.some(x => x === value)) isValid = isValid && !(this.blacklistItems.some(x => x === value))
} }
return is_valid return isValid
} }
/** /**
* Sets the Option as optional. * Sets the Option as optional.
*/ */
optional() { optional(): this {
this._optional = true this.isOptional = true
return this 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. * Get an array of strings denoting the human-readable requirements for this option to be valid.
* @returns {Array<string>} * @returns {Array<string>}
*/ */
getRequirementDisplays() { getRequirementDisplays(): string[] {
const clauses = [] const clauses = []
if ( this._useBlacklist ) { if ( this.useBlacklist ) {
clauses.push(`must not be one of: ${this._blacklist.map(x => String(x)).join(', ')}`) clauses.push(`must not be one of: ${this.blacklistItems.map(x => String(x)).join(', ')}`)
} }
if ( this._useWhitelist ) { if ( this.useWhitelist ) {
clauses.push(`must be one of: ${this._whitelist.map(x => String(x)).join(', ')}`) clauses.push(`must be one of: ${this.whitelistItems.map(x => String(x)).join(', ')}`)
} }
if ( this._useGreaterThan ) { if ( this.useGreaterThan ) {
clauses.push(`must be greater than${this._greateerThanBit ? ' or equal to' : ''}: ${String(this._greaterThanValue)}`) clauses.push(`must be greater than${this.greaterThanBit ? ' or equal to' : ''}: ${String(this.greaterThanValue)}`)
} }
if ( this._useLessThan ) { if ( this.useLessThan ) {
clauses.push(`must be less than${this._lessThanBit ? ' or equal to' : ''}: ${String(this._lessThanValue)}`) clauses.push(`must be less than${this.lessThanBit ? ' or equal to' : ''}: ${String(this.lessThanValue)}`)
} }
if ( this._useEquality ) { if ( this.useEquality ) {
clauses.push(`must be equal to: ${String(this._equalityValue)}`) clauses.push(`must be equal to: ${String(this.equalityValue)}`)
} }
return clauses return clauses

View File

@@ -1,4 +1,4 @@
import {CLIOption} from "./CLIOption" import {CLIOption} from './CLIOption'
/** /**
* Non-positional, flag-based CLI option. * 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. * Description of the argument required by this flag.
* If this is set, the flag will expect a positional argument to follow as a param. * If this is set, the flag will expect a positional argument to follow as a param.
*/ */
public readonly argumentDescription?: string public readonly argumentDescription?: string,
) { super() } ) {
super()
}
/** /**
* Get the referential name for this option. * 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. * be found, the short flag (without the '-') is used.
* @returns {string} * @returns {string}
*/ */
getArgumentName() { getArgumentName(): string {
if ( this.longFlag ) { if ( this.longFlag ) {
return this.longFlag.replace('--', '') return this.longFlag.replace('--', '')
} else if ( this.shortFlag ) { } else if ( this.shortFlag ) {

View File

@@ -1,4 +1,4 @@
import {CLIOption} from "./CLIOption" import {CLIOption} from './CLIOption'
/** /**
* A positional CLI option. Defined without a flag. * 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. * A usage message describing this parameter.
*/ */
public readonly message: string = '' public readonly message: string = '',
) { super() } ) {
super()
}
/** /**
* Gets the name of the option. * Gets the name of the option.
* @returns {string} * @returns {string}
*/ */
getArgumentName () { getArgumentName(): string {
return this.name return this.name
} }
} }

View File

@@ -1,16 +1,16 @@
import {Singleton, Instantiable, Inject} from "../../di" import {Singleton, Instantiable, Inject} from '../../di'
import {Collection} from "../../util" import {Collection} from '../../util'
import {CommandLineApplication} from "./CommandLineApplication" import {CommandLineApplication} from './CommandLineApplication'
import {Directive} from "../Directive" import {Directive} from '../Directive'
import {Template} from "../Template" import {Template} from '../Template'
import {directive_template} from "../templates/directive" import {templateDirective} from '../templates/directive'
import {unit_template} from "../templates/unit"; import {templateUnit} from '../templates/unit'
import {controller_template} from "../templates/controller"; import {templateController} from '../templates/controller'
import {middleware_template} from "../templates/middleware"; import {templateMiddleware} from '../templates/middleware'
import {routes_template} from "../templates/routes"; import {templateRoutes} from '../templates/routes'
import {config_template} from "../templates/config"; import {templateConfig} from '../templates/config'
import {Unit} from "../../lifecycle/Unit"; import {Unit} from '../../lifecycle/Unit'
import {Logging} from "../../service/Logging"; import {Logging} from '../../service/Logging'
/** /**
* Service for managing directives, templates, and other resources related * 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. */ /** Templates registered with the CLI command. These can be created with the TemplateDirective. */
protected templates: Collection<Template> = new Collection<Template>() protected templates: Collection<Template> = new Collection<Template>()
constructor() { super() } constructor() {
super()
}
async up() { async up(): Promise<void> {
this.registerTemplate(directive_template) this.registerTemplate(templateDirective)
this.registerTemplate(unit_template) this.registerTemplate(templateUnit)
this.registerTemplate(controller_template) this.registerTemplate(templateController)
this.registerTemplate(middleware_template) this.registerTemplate(templateMiddleware)
this.registerTemplate(routes_template) this.registerTemplate(templateRoutes)
this.registerTemplate(config_template) this.registerTemplate(templateConfig)
} }
/** /**
* Returns true if the application was started from the command line. * Returns true if the application was started from the command line.
*/ */
public isCLI() { public isCLI(): boolean {
return this.app().hasUnit(CommandLineApplication) return this.app().hasUnit(CommandLineApplication)
} }
/** /**
* Returns a string containing the Extollo ASCII logo. * Returns a string containing the Extollo ASCII logo.
*/ */
public getASCIILogo() { public getASCIILogo(): string {
return ` _ return ` _
/ /\\ ______ _ _ _ / /\\ ______ _ _ _
/ / \\ | ____| | | | | | / / \\ | ____| | | | | |
@@ -64,24 +66,26 @@ export class CommandLine extends Unit {
* the directive available for use on the CLI. * the directive available for use on the CLI.
* @param directiveClass * @param directiveClass
*/ */
public registerDirective(directiveClass: Instantiable<Directive>) { public registerDirective(directiveClass: Instantiable<Directive>): this {
if ( !this.directives.includes(directiveClass) ) { if ( !this.directives.includes(directiveClass) ) {
this.directives.push(directiveClass) this.directives.push(directiveClass)
} }
return this
} }
/** /**
* Returns true if the given directive is registered with this service. * Returns true if the given directive is registered with this service.
* @param directiveClass * @param directiveClass
*/ */
public hasDirective(directiveClass: Instantiable<Directive>) { public hasDirective(directiveClass: Instantiable<Directive>): boolean {
return this.directives.includes(directiveClass) return this.directives.includes(directiveClass)
} }
/** /**
* Get a collection of all registered directives. * Get a collection of all registered directives.
*/ */
public getDirectives() { public getDirectives(): Collection<Instantiable<Directive>> {
return this.directives.clone() return this.directives.clone()
} }
@@ -90,21 +94,23 @@ export class CommandLine extends Unit {
* available for use with the TemplateDirective service. * available for use with the TemplateDirective service.
* @param template * @param template
*/ */
public registerTemplate(template: Template) { public registerTemplate(template: Template): this {
if ( !this.templates.firstWhere('name', '=', template.name) ) { if ( !this.templates.firstWhere('name', '=', template.name) ) {
this.templates.push(template) this.templates.push(template)
} else { } else {
this.logging.warn(`Duplicate template will not be registered: ${template.name}`) this.logging.warn(`Duplicate template will not be registered: ${template.name}`)
this.logging.debug(`Duplicate template registered at: ${(new Error()).stack}`) this.logging.debug(`Duplicate template registered at: ${(new Error()).stack}`)
} }
return this
} }
/** /**
* Returns true if a template with the given name exists. * Returns true if a template with the given name exists.
* @param name * @param name
*/ */
public hasTemplate(name: string) { public hasTemplate(name: string): boolean {
return !!this.templates.firstWhere('name', '=', name) return Boolean(this.templates.firstWhere('name', '=', name))
} }
/** /**
@@ -118,7 +124,7 @@ export class CommandLine extends Unit {
/** /**
* Get a collection of all registered templates. * Get a collection of all registered templates.
*/ */
public getTemplates() { public getTemplates(): Collection<Template> {
return this.templates.clone() return this.templates.clone()
} }
} }

View File

@@ -1,12 +1,12 @@
import {Unit} from "../../lifecycle/Unit" import {Unit} from '../../lifecycle/Unit'
import {Logging} from "../../service/Logging"; import {Logging} from '../../service/Logging'
import {Singleton, Inject} from "../../di/decorator/injection" import {Singleton, Inject} from '../../di/decorator/injection'
import {CommandLine} from "./CommandLine" import {CommandLine} from './CommandLine'
import {UsageDirective} from "../directive/UsageDirective"; import {UsageDirective} from '../directive/UsageDirective'
import {Directive} from "../Directive"; import {Directive} from '../Directive'
import {ShellDirective} from "../directive/ShellDirective"; import {ShellDirective} from '../directive/ShellDirective'
import {TemplateDirective} from "../directive/TemplateDirective"; import {TemplateDirective} from '../directive/TemplateDirective'
import {RunDirective} from "../directive/RunDirective"; import {RunDirective} from '../directive/RunDirective'
/** /**
* Unit that takes the place of the final unit in the application that handles * 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 private static replacement?: typeof Unit
/** Set the replaced unit. */ /** Set the replaced unit. */
public static setReplacement(unitClass?: typeof Unit) { public static setReplacement(unitClass?: typeof Unit): void {
this.replacement = unitClass this.replacement = unitClass
} }
/** Get the replaced unit. */ /** Get the replaced unit. */
public static getReplacement() { public static getReplacement(): typeof Unit | undefined {
return this.replacement return this.replacement
} }
@@ -33,9 +33,11 @@ export class CommandLineApplication extends Unit {
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
constructor() { super() } constructor() {
super()
}
public async up() { public async up(): Promise<void> {
this.cli.registerDirective(UsageDirective) this.cli.registerDirective(UsageDirective)
this.cli.registerDirective(ShellDirective) this.cli.registerDirective(ShellDirective)
this.cli.registerDirective(TemplateDirective) this.cli.registerDirective(TemplateDirective)
@@ -50,7 +52,7 @@ export class CommandLineApplication extends Unit {
await match.invoke(argv.slice(1)) await match.invoke(argv.slice(1))
} else { } else {
const usage = this.make<UsageDirective>(UsageDirective) const usage = this.make<UsageDirective>(UsageDirective)
await usage.handle(process.argv) await usage.handle()
} }
} }
} }

View File

@@ -1,22 +1,21 @@
import {Template} from "../Template" import {Template} from '../Template'
import {UniversalPath} from "../../util"
/** /**
* A template that generates a new configuration file in the app/configs directory. * A template that generates a new configuration file in the app/configs directory.
*/ */
const config_template: Template = { const templateConfig: Template = {
name: 'config', name: 'config',
fileSuffix: '.config.ts', fileSuffix: '.config.ts',
description: 'Create a new config file.', description: 'Create a new config file.',
baseAppPath: ['configs'], baseAppPath: ['configs'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) { render() {
return `import { env } from '@extollo/lib' return `import { env } from '@extollo/lib'
export default { export default {
key: env('VALUE_ENV_VAR', 'default value'), key: env('VALUE_ENV_VAR', 'default value'),
} }
` `
} },
} }
export { config_template } export { templateConfig }

View File

@@ -1,17 +1,15 @@
import {Template} from "../Template" import {Template} from '../Template'
import {UniversalPath} from "../../util"
/** /**
* Template that generates a new controller in the app/http/controllers directory. * Template that generates a new controller in the app/http/controllers directory.
*/ */
const controller_template: Template = { const templateController: Template = {
name: 'controller', name: 'controller',
fileSuffix: '.controller.ts', fileSuffix: '.controller.ts',
description: 'Create a controller class that can be used to handle requests.', description: 'Create a controller class that can be used to handle requests.',
baseAppPath: ['http', 'controllers'], baseAppPath: ['http', 'controllers'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) { render(name: string) {
return `import {Controller, view} from "@extollo/lib" return `import {Controller, view, Inject, Injectable} from '@extollo/lib'
import {Inject, Injectable} from "@extollo/di"
/** /**
* ${name} Controller * ${name} Controller
@@ -25,7 +23,7 @@ export class ${name} extends Controller {
} }
} }
` `
} },
} }
export { controller_template } export { templateController }

View File

@@ -1,17 +1,15 @@
import {Template} from "../Template" import {Template} from '../Template'
import {UniversalPath} from "../../util"
/** /**
* Template that generates a new Directive class in the app/directives directory. * Template that generates a new Directive class in the app/directives directory.
*/ */
const directive_template: Template = { const templateDirective: Template = {
name: 'directive', name: 'directive',
fileSuffix: '.directive.ts', fileSuffix: '.directive.ts',
description: 'Create a new Directive class which adds functionality to the ./ex command.', description: 'Create a new Directive class which adds functionality to the ./ex command.',
baseAppPath: ['directives'], baseAppPath: ['directives'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) { render(name: string) {
return `import {Directive, OptionDefinition} from "@extollo/cli" return `import {Directive, OptionDefinition, Injectable} from '@extollo/lib'
import {Injectable} from "@extollo/di"
/** /**
* ${name} Directive * ${name} Directive
@@ -37,7 +35,7 @@ export class ${name}Directive extends Directive {
} }
} }
` `
} },
} }
export { directive_template } export { templateDirective }

View File

@@ -1,17 +1,15 @@
import {Template} from "../Template" import {Template} from '../Template'
import {UniversalPath} from "../../util"
/** /**
* Template that generates a new middleware class in app/http/middlewares. * Template that generates a new middleware class in app/http/middlewares.
*/ */
const middleware_template: Template = { const templateMiddleware: Template = {
name: 'middleware', name: 'middleware',
fileSuffix: '.middleware.ts', fileSuffix: '.middleware.ts',
description: 'Create a middleware class that can be applied to routes.', description: 'Create a middleware class that can be applied to routes.',
baseAppPath: ['http', 'middlewares'], baseAppPath: ['http', 'middlewares'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) { render(name: string) {
return `import {Middleware} from "@extollo/lib" return `import {Middleware, Injectable} from '@extollo/lib'
import {Injectable} from "@extollo/di"
/** /**
* ${name} Middleware * ${name} Middleware
@@ -25,7 +23,7 @@ export class ${name} extends Middleware {
} }
} }
` `
} },
} }
export { middleware_template } export { templateMiddleware }

View File

@@ -1,16 +1,15 @@
import {Template} from "../Template" import {Template} from '../Template'
import {UniversalPath} from "../../util"
/** /**
* Template that generates a new route definition file in app/http/routes. * Template that generates a new route definition file in app/http/routes.
*/ */
const routes_template: Template = { const templateRoutes: Template = {
name: 'routes', name: 'routes',
fileSuffix: '.routes.ts', fileSuffix: '.routes.ts',
description: 'Create a file for route definitions.', description: 'Create a file for route definitions.',
baseAppPath: ['http', 'routes'], baseAppPath: ['http', 'routes'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) { render(name: string) {
return `import {Route} from "@extollo/lib" return `import {Route} from '@extollo/lib'
/* /*
* ${name} Routes * ${name} Routes
@@ -20,7 +19,7 @@ const routes_template: Template = {
` `
} },
} }
export { routes_template } export { templateRoutes }

View File

@@ -1,17 +1,15 @@
import {Template} from "../Template" import {Template} from '../Template'
import {UniversalPath} from "../../util"
/** /**
* Template that generates a new application unit class in app/units. * Template that generates a new application unit class in app/units.
*/ */
const unit_template: Template = { const templateUnit: Template = {
name: 'unit', name: 'unit',
fileSuffix: '.ts', fileSuffix: '.ts',
description: 'Create a service unit that will start and stop with your application.', description: 'Create a service unit that will start and stop with your application.',
baseAppPath: ['units'], baseAppPath: ['units'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) { render(name: string) {
return `import {Singleton, Inject} from "@extollo/di" return `import {Singleton, Inject, Unit, Logging} from '@extollo/lib'
import {Unit, Logging} from "@extollo/lib"
/** /**
* ${name} Unit * ${name} Unit
@@ -32,7 +30,7 @@ export class ${name} extends Unit {
} }
} }
` `
} },
} }
export { unit_template } export { templateUnit }

View File

@@ -1,14 +1,14 @@
import {DependencyKey, InstanceRef, Instantiable, isInstantiable, StaticClass} from "./types"; import {DependencyKey, InstanceRef, Instantiable, isInstantiable} from './types'
import {AbstractFactory} from "./factory/AbstractFactory"; import {AbstractFactory} from './factory/AbstractFactory'
import {collect, Collection, globalRegistry, logIfDebugging} from "../util"; import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
import {Factory} from "./factory/Factory"; import {Factory} from './factory/Factory'
import {DuplicateFactoryKeyError} from "./error/DuplicateFactoryKeyError"; import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
import {ClosureFactory} from "./factory/ClosureFactory"; import {ClosureFactory} from './factory/ClosureFactory'
import NamedFactory from "./factory/NamedFactory"; import NamedFactory from './factory/NamedFactory'
import SingletonFactory from "./factory/SingletonFactory"; import SingletonFactory from './factory/SingletonFactory'
import {InvalidDependencyKeyError} from "./error/InvalidDependencyKeyError"; import {InvalidDependencyKeyError} from './error/InvalidDependencyKeyError'
export type MaybeFactory = AbstractFactory | undefined export type MaybeFactory<T> = AbstractFactory<T> | undefined
export type MaybeDependency = any | undefined export type MaybeDependency = any | undefined
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any } export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any }
@@ -34,7 +34,7 @@ export class Container {
* Collection of factories registered with this container. * Collection of factories registered with this container.
* @type Collection<AbstractFactory> * @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. * 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. * Register a basic instantiable class as a standard Factory with this container.
* @param {Instantiable} dependency * @param {Instantiable} dependency
*/ */
register(dependency: Instantiable<any>) { register(dependency: Instantiable<any>): this {
if ( this.resolve(dependency) ) if ( this.resolve(dependency) ) {
throw new DuplicateFactoryKeyError(dependency) throw new DuplicateFactoryKeyError(dependency)
}
const factory = new Factory(dependency) const factory = new Factory(dependency)
this.factories.push(factory) 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 {string} name - unique name to identify the factory in the container
* @param {function} producer - factory to produce a value * @param {function} producer - factory to produce a value
*/ */
registerProducer(name: string | StaticClass<any, any>, producer: () => any) { registerProducer(name: DependencyKey, producer: () => any): this {
if ( this.resolve(name) ) if ( this.resolve(name) ) {
throw new DuplicateFactoryKeyError(name) throw new DuplicateFactoryKeyError(name)
}
const factory = new ClosureFactory(name, producer) const factory = new ClosureFactory(name, producer)
this.factories.push(factory) 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 {string} name - unique name to identify the factory in the container
* @param {Instantiable} dependency * @param {Instantiable} dependency
*/ */
registerNamed(name: string, dependency: Instantiable<any>) { registerNamed(name: string, dependency: Instantiable<any>): this {
if ( this.resolve(name) ) if ( this.resolve(name) ) {
throw new DuplicateFactoryKeyError(name) throw new DuplicateFactoryKeyError(name)
}
const factory = new NamedFactory(name, dependency) const factory = new NamedFactory(name, dependency)
this.factories.push(factory) 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 {string} key - unique name to identify the singleton in the container
* @param value * @param value
*/ */
registerSingleton(key: string, value: any) { registerSingleton<T>(key: DependencyKey, value: T): this {
if ( this.resolve(key) ) if ( this.resolve(key) ) {
throw new DuplicateFactoryKeyError(key) throw new DuplicateFactoryKeyError(key)
}
this.factories.push(new SingletonFactory(value, key)) this.factories.push(new SingletonFactory(key, value))
return this
} }
/** /**
@@ -105,26 +113,32 @@ export class Container {
* @param staticClass * @param staticClass
* @param instance * @param instance
*/ */
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T) { registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
if ( this.resolve(staticClass) ) if ( this.resolve(staticClass) ) {
throw new DuplicateFactoryKeyError(staticClass) throw new DuplicateFactoryKeyError(staticClass)
}
this.register(staticClass) this.register(staticClass)
this.instances.push({ this.instances.push({
key: staticClass, key: staticClass,
value: instance, value: instance,
}) })
return this
} }
/** /**
* Register a given factory with the container. * Register a given factory with the container.
* @param {AbstractFactory} factory * @param {AbstractFactory} factory
*/ */
registerFactory(factory: AbstractFactory) { registerFactory(factory: AbstractFactory<unknown>): this {
if ( !this.factories.includes(factory) ) if ( !this.factories.includes(factory) ) {
this.factories.push(factory) this.factories.push(factory)
} }
return this
}
/** /**
* Returns true if the container has an already-produced value for the given key. * Returns true if the container has an already-produced value for the given key.
* @param {DependencyKey} key * @param {DependencyKey} key
@@ -138,7 +152,7 @@ export class Container {
* @param {DependencyKey} key * @param {DependencyKey} key
*/ */
hasKey(key: DependencyKey): boolean { hasKey(key: DependencyKey): boolean {
return !!this.resolve(key) return Boolean(this.resolve(key))
} }
/** /**
@@ -147,17 +161,22 @@ export class Container {
*/ */
getExistingInstance(key: DependencyKey): MaybeDependency { getExistingInstance(key: DependencyKey): MaybeDependency {
const instances = this.instances.where('key', '=', key) 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. * Find the factory for the given key, if one is registered with this container.
* @param {DependencyKey} key * @param {DependencyKey} key
*/ */
resolve(key: DependencyKey): MaybeFactory { resolve(key: DependencyKey): MaybeFactory<unknown> {
const factory = this.factories.firstWhere(item => item.match(key)) const factory = this.factories.firstWhere(item => item.match(key))
if ( factory ) return factory if ( factory ) {
else logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories) 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 // If we've already instantiated this, just return that
const instance = this.getExistingInstance(key) const instance = this.getExistingInstance(key)
logIfDebugging('extollo.di.injector', 'resolveAndCreate existing instance?', instance) 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 // Otherwise, attempt to create it
const factory = this.resolve(key) const factory = this.resolve(key)
logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory) logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory)
if ( !factory ) if ( !factory ) {
throw new InvalidDependencyKeyError(key) throw new InvalidDependencyKeyError(key)
}
// Produce and store a new instance // Produce and store a new instance
const new_instance = this.produceFactory(factory, parameters) const newInstance = this.produceFactory(factory, parameters)
this.instances.push({ this.instances.push({
key, key,
value: new_instance, value: newInstance,
}) })
return new_instance return newInstance
} }
/** /**
@@ -196,7 +218,7 @@ export class Container {
* @param {AbstractFactory} factory * @param {AbstractFactory} factory
* @param {array} parameters * @param {array} parameters
*/ */
protected produceFactory(factory: AbstractFactory, parameters: any[]) { protected produceFactory<T>(factory: AbstractFactory<T>, parameters: any[]): T {
// Create the dependencies for the factory // Create the dependencies for the factory
const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key)) const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key))
const dependencies = keys.map<ResolvedDependency>(req => { const dependencies = keys.map<ResolvedDependency>(req => {
@@ -210,20 +232,23 @@ export class Container {
// Build the arguments for the factory, using dependencies in the // Build the arguments for the factory, using dependencies in the
// correct paramIndex positions, or parameters of we don't have // correct paramIndex positions, or parameters of we don't have
// the dependency. // the dependency.
const construction_args = [] const constructorArguments = []
let params = collect(parameters).reverse() const params = collect(parameters).reverse()
for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) { for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) {
const dep = dependencies.firstWhere('paramIndex', '=', i) const dep = dependencies.firstWhere('paramIndex', '=', i)
if ( dep ) construction_args.push(dep.resolved) if ( dep ) {
else construction_args.push(params.pop()) constructorArguments.push(dep.resolved)
} else {
constructorArguments.push(params.pop())
}
} }
// Produce a new instance // 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 => { factory.getInjectedProperties().each(dependency => {
if ( dependency.key && inst ) { if ( dependency.key && inst ) {
inst[dependency.property] = this.resolveAndCreate(dependency.key) (inst as any)[dependency.property] = this.resolveAndCreate(dependency.key)
} }
}) })
@@ -240,13 +265,14 @@ export class Container {
* @param {...any} parameters * @param {...any} parameters
*/ */
make<T>(target: DependencyKey, ...parameters: any[]): T { make<T>(target: DependencyKey, ...parameters: any[]): T {
if ( this.hasKey(target) ) if ( this.hasKey(target) ) {
return this.resolveAndCreate(target, ...parameters) 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) return this.produceFactory(new Factory(target), parameters)
else } else {
throw new TypeError(`Invalid or unknown make target: ${target}`) throw new TypeError(`Invalid or unknown make target: ${target}`)
} }
}
/** /**
* Get a collection of dependency keys required by the given target, if it is registered with this container. * Get a collection of dependency keys required by the given target, if it is registered with this container.
@@ -255,8 +281,9 @@ export class Container {
getDependencies(target: DependencyKey): Collection<DependencyKey> { getDependencies(target: DependencyKey): Collection<DependencyKey> {
const factory = this.resolve(target) const factory = this.resolve(target)
if ( !factory ) if ( !factory ) {
throw new InvalidDependencyKeyError(target) throw new InvalidDependencyKeyError(target)
}
return factory.getDependencyKeys().pluck('key') 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. * Given a different container, copy the factories and instances from this container over to it.
* @param container * @param container
*/ */
cloneTo(container: Container) { cloneTo(container: Container): this {
container.factories = this.factories.clone() container.factories = this.factories.clone()
container.instances = this.instances.clone() container.instances = this.instances.clone()
return this
} }
} }

View File

@@ -1,5 +1,5 @@
import {Container, MaybeDependency, MaybeFactory} from "./Container" import {Container, MaybeDependency, MaybeFactory} from './Container'
import {DependencyKey} from "./types" import {DependencyKey} from './types'
/** /**
* A container that uses some parent container as a base, but * 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. * Create a new scoped container based on a parent container instance.
* @param container * @param container
*/ */
public static fromParent(container: Container) { public static fromParent(container: Container): ScopedContainer {
return new ScopedContainer(container); return new ScopedContainer(container)
} }
constructor( constructor(
@@ -47,15 +47,19 @@ export class ScopedContainer extends Container {
getExistingInstance(key: DependencyKey): MaybeDependency { getExistingInstance(key: DependencyKey): MaybeDependency {
const inst = super.getExistingInstance(key) const inst = super.getExistingInstance(key)
if ( inst ) return inst; if ( inst ) {
return inst
return this.parentContainer.getExistingInstance(key);
} }
resolve(key: DependencyKey): MaybeFactory { return this.parentContainer.getExistingInstance(key)
const factory = super.resolve(key); }
if ( factory ) return factory;
return this.parentContainer?.resolve(key); resolve(key: DependencyKey): MaybeFactory<any> {
const factory = super.resolve(key)
if ( factory ) {
return factory
}
return this.parentContainer?.resolve(key)
} }
} }

View File

@@ -1,5 +1,5 @@
import 'reflect-metadata' import 'reflect-metadata'
import {collect, Collection} from "../../util"; import {collect, Collection} from '../../util'
import { import {
DependencyKey, DependencyKey,
DependencyRequirement, DependencyRequirement,
@@ -9,16 +9,16 @@ import {
InjectionType, InjectionType,
DEPENDENCY_KEYS_SERVICE_TYPE_KEY, DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
PropertyDependency, PropertyDependency,
} from "../types"; } from '../types'
import {Container} from "../Container"; import {Container} from '../Container'
/** /**
* Get a collection of dependency requirements for the given target object. * Get a collection of dependency requirements for the given target object.
* @param {Object} target * @param {Object} target
* @return Collection<DependencyRequirement> * @return Collection<DependencyRequirement>
*/ */
function initDependencyMetadata(target: Object): Collection<DependencyRequirement> { function initDependencyMetadata(target: unknown): Collection<DependencyRequirement> {
const paramTypes = Reflect.getMetadata('design:paramtypes', target) const paramTypes = Reflect.getMetadata('design:paramtypes', target as any)
return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((type, idx) => { return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((type, idx) => {
return { return {
paramIndex: idx, paramIndex: idx,
@@ -37,32 +37,32 @@ export const Injectable = (): ClassDecorator => {
return (target) => { return (target) => {
const meta = initDependencyMetadata(target) const meta = initDependencyMetadata(target)
const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
const new_meta = new Collection<DependencyRequirement>() const newMetadata = new Collection<DependencyRequirement>()
if ( existing ) { if ( existing ) {
const max_new = meta.max('paramIndex') const maxNew = meta.max('paramIndex')
const max_existing = existing.max('paramIndex') const maxExisting = existing.max('paramIndex')
for ( let i = 0; i <= Math.max(max_new, max_existing); i++ ) { for ( let i = 0; i <= Math.max(maxNew, maxExisting); i++ ) {
const existing_dr = existing.firstWhere('paramIndex', '=', i) const existingDR = existing.firstWhere('paramIndex', '=', i)
const new_dr = meta.firstWhere('paramIndex', '=', i) const newDR = meta.firstWhere('paramIndex', '=', i)
if ( existing_dr && !new_dr ) { if ( existingDR && !newDR ) {
new_meta.push(existing_dr) newMetadata.push(existingDR)
} else if ( new_dr && !existing_dr ) { } else if ( newDR && !existingDR ) {
new_meta.push(new_dr) newMetadata.push(newDR)
} else if ( new_dr && existing_dr ) { } else if ( newDR && existingDR ) {
if ( existing_dr.overridden ) { if ( existingDR.overridden ) {
new_meta.push(existing_dr) newMetadata.push(existingDR)
} else { } else {
new_meta.push(new_dr) newMetadata.push(newDR)
} }
} }
} }
} else { } 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) const type = Reflect.getMetadata('design:type', target, property)
if ( !key && type ) key = type if ( !key && type ) {
key = type
}
if ( key ) { if ( key ) {
const existing = propertyMetadata.firstWhere('property', '=', property) const existing = propertyMetadata.firstWhere('property', '=', property)
if ( existing ) { if ( existing ) {
existing.key = key existing.key = key
} else { } else {
propertyMetadata.push({ property, key }) propertyMetadata.push({ property,
key })
} }
} }
@@ -118,7 +121,7 @@ export const InjectParam = (key: DependencyKey): ParameterDecorator => {
meta.push({ meta.push({
paramIndex, paramIndex,
key, key,
overridden: true overridden: true,
}) })
} }
@@ -135,7 +138,7 @@ export const Singleton = (name?: string): ClassDecorator => {
if ( isInstantiable(target) ) { if ( isInstantiable(target) ) {
const injectionType: InjectionType = { const injectionType: InjectionType = {
type: name ? 'named' : 'singleton', type: name ? 'named' : 'singleton',
...(name ? { name } : {}) ...(name ? { name } : {}),
} }
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target) Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)

View File

@@ -1,4 +1,4 @@
import {DependencyKey} from "../types"; import {DependencyKey} from '../types'
/** /**
* Error thrown when a factory is registered with a duplicate dependency key. * Error thrown when a factory is registered with a duplicate dependency key.

View File

@@ -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. * Error thrown when a dependency key that has not been registered is passed to a resolver.

View File

@@ -1,11 +1,11 @@
import {DependencyRequirement, PropertyDependency} from "../types"; import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
import { Collection } from "../../util"; import { Collection } from '../../util'
/** /**
* Abstract base class for dependency container factories. * Abstract base class for dependency container factories.
* @abstract * @abstract
*/ */
export abstract class AbstractFactory { export abstract class AbstractFactory<T> {
protected constructor( protected constructor(
/** /**
* Token that was registered for this factory. In most cases, this is the static * Token that was registered for this factory. In most cases, this is the static
@@ -13,7 +13,7 @@ export abstract class AbstractFactory {
* @var * @var
* @protected * @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} dependencies - the resolved dependencies, in order
* @param {Array} parameters - the bound constructor parameters, 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. * Should return true if the given identifier matches the token for this factory.
* @param something * @param something
* @return boolean * @return boolean
*/ */
abstract match(something: any): boolean abstract match(something: unknown): boolean
/** /**
* Get the dependency requirements required by this factory's token. * Get the dependency requirements required by this factory's token.

View File

@@ -1,6 +1,6 @@
import {AbstractFactory} from "./AbstractFactory"; import {AbstractFactory} from './AbstractFactory'
import {DependencyRequirement, PropertyDependency, StaticClass} from "../types"; import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
import {Collection} from "../../util"; import {Collection} from '../../util'
/** /**
* A factory whose token is produced by calling a function. * A factory whose token is produced by calling a function.
@@ -17,19 +17,19 @@ import {Collection} from "../../util";
* fact.produce([], []) // => 4 * fact.produce([], []) // => 4
* ``` * ```
*/ */
export class ClosureFactory extends AbstractFactory { export class ClosureFactory<T> extends AbstractFactory<T> {
constructor( constructor(
protected readonly name: string | StaticClass<any, any>, protected readonly name: DependencyKey,
protected readonly token: () => any, protected readonly token: () => T,
) { ) {
super(token) super(token)
} }
produce(dependencies: any[], parameters: any[]): any { produce(): any {
return this.token() return this.token()
} }
match(something: any) { match(something: unknown): boolean {
return something === this.name return something === this.name
} }

View File

@@ -1,12 +1,12 @@
import {AbstractFactory} from "./AbstractFactory"; import {AbstractFactory} from './AbstractFactory'
import { import {
DEPENDENCY_KEYS_METADATA_KEY, DEPENDENCY_KEYS_METADATA_KEY,
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
DependencyRequirement, DependencyRequirement,
Instantiable, Instantiable,
PropertyDependency PropertyDependency,
} from "../types"; } from '../types'
import {Collection} from "../../util"; import {Collection} from '../../util'
import 'reflect-metadata' import 'reflect-metadata'
/** /**
@@ -29,9 +29,9 @@ import 'reflect-metadata'
* fact.produce([myServiceInstance], []) // => A { myService: myServiceInstance } * fact.produce([myServiceInstance], []) // => A { myService: myServiceInstance }
* ``` * ```
*/ */
export class Factory extends AbstractFactory { export class Factory<T> extends AbstractFactory<T> {
constructor( constructor(
protected readonly token: Instantiable<any> protected readonly token: Instantiable<T>,
) { ) {
super(token) super(token)
} }
@@ -40,13 +40,15 @@ export class Factory extends AbstractFactory {
return new this.token(...dependencies, ...parameters) return new this.token(...dependencies, ...parameters)
} }
match(something: any) { match(something: unknown): boolean {
return something === this.token // || (something?.name && something.name === this.token.name) return something === this.token // || (something?.name && something.name === this.token.name)
} }
getDependencyKeys(): Collection<DependencyRequirement> { getDependencyKeys(): Collection<DependencyRequirement> {
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token) const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token)
if ( meta ) return meta if ( meta ) {
return meta
}
return new Collection<DependencyRequirement>() return new Collection<DependencyRequirement>()
} }
@@ -56,7 +58,9 @@ export class Factory extends AbstractFactory {
do { do {
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken) const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
if ( loadedMeta ) meta.concat(loadedMeta) if ( loadedMeta ) {
meta.concat(loadedMeta)
}
currentToken = Object.getPrototypeOf(currentToken) currentToken = Object.getPrototypeOf(currentToken)
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype) } while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)

View File

@@ -1,12 +1,12 @@
import {Factory} from "./Factory"; import {Factory} from './Factory'
import {Instantiable} from "../types"; import {Instantiable} from '../types'
/** /**
* Container factory that produces an instance of the token, however the token * Container factory that produces an instance of the token, however the token
* is identified by a string name rather than a class reference. * is identified by a string name rather than a class reference.
* @extends Factory * @extends Factory
*/ */
export default class NamedFactory extends Factory { export default class NamedFactory<T> extends Factory<T> {
constructor( constructor(
/** /**
* The name identifying this factory in the container. * The name identifying this factory in the container.
@@ -18,12 +18,12 @@ export default class NamedFactory extends Factory {
* The token to be instantiated. * The token to be instantiated.
* @type {Instantiable} * @type {Instantiable}
*/ */
protected token: Instantiable<any>, protected token: Instantiable<T>,
) { ) {
super(token) super(token)
} }
match(something: any) { match(something: unknown): boolean {
return something === this.name return something === this.name
} }
} }

View File

@@ -1,6 +1,6 @@
import { Factory } from './Factory' import { Factory } from './Factory'
import { Collection } from '../../util' 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 * Container factory which returns its token as its value, without attempting
@@ -19,29 +19,23 @@ import {DependencyRequirement, PropertyDependency} from "../types";
* *
* @extends Factory * @extends Factory
*/ */
export default class SingletonFactory extends Factory { export default class SingletonFactory<T> extends Factory<T> {
constructor( constructor(
/** /**
* Instantiated value of this factory. * Token identifying this singleton.
* @type FunctionConstructor
*/ */
protected token: FunctionConstructor, protected token: DependencyKey,
/** /**
* String name of this singleton identifying it in the container. * The value of this singleton.
* @type string
*/ */
protected key: string, protected value: T,
) { ) {
super(token) super(token)
} }
produce(dependencies: any[], parameters: any[]) { produce(): T {
return this.token return this.value
}
match(something: any) {
return something === this.key
} }
getDependencyKeys(): Collection<DependencyRequirement> { getDependencyKeys(): Collection<DependencyRequirement> {

View File

@@ -1,6 +1,6 @@
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor'; 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_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties'
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type'; export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type'
/** /**
* Interface that designates a particular value as able to be constructed. * 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. * Returns true if the given value is instantiable.
* @param what * @param what
*/ */
export function isInstantiable<T>(what: any): what is Instantiable<T> { export function isInstantiable<T>(what: unknown): what is Instantiable<T> {
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function' 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. * 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. * Returns true if the parameter is a static class.
* @param something * @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' return typeof something === 'function' && typeof something.prototype !== 'undefined'
} }

View File

@@ -1,9 +1,9 @@
import {Container, Injectable, InjectParam} from '../di' 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 {Valid, ValidationRules} from './rules/types'
import {Validator} from './Validator' import {Validator} from './Validator'
import {AppClass} from "../lifecycle/AppClass"; import {AppClass} from '../lifecycle/AppClass'
import {DataContainer} from "../http/lifecycle/Request"; import {DataContainer} from '../http/lifecycle/Request'
/** /**
* Base class for defining reusable validators for request routes. * Base class for defining reusable validators for request routes.
@@ -30,10 +30,12 @@ export abstract class FormRequest<T> extends AppClass {
constructor( constructor(
@InjectParam(Request) @InjectParam(Request)
protected readonly data: DataContainer protected readonly data: DataContainer,
) { super() } ) {
super()
}
protected container() { protected container(): Container {
return (this.data as unknown) as Container return (this.data as unknown) as Container
} }

View File

@@ -1,5 +1,5 @@
import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from "./rules/types"; import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from './rules/types'
import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from "../util"; import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from '../util'
/** /**
* An error thrown thrown when an object fails its validation. * 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 { export class ValidationError<T> extends ErrorWithContext {
constructor( constructor(
/** The original input data. */ /** The original input data. */
public readonly data: any, public readonly data: unknown,
/** The validator instance used. */ /** The validator instance used. */
public readonly validator: Validator<T>, public readonly validator: Validator<T>,
/** Validation error messages, by field. */ /** 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> { export class Validator<T> {
constructor( constructor(
/** The rules used to validate input objects. */ /** 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>. * Returns true if the given data is valid and type aliases it as Valid<T>.
* @param data * @param data
*/ */
public async isValid(data: any): Promise<boolean> { public async isValid(data: unknown): Promise<boolean> {
return !(await this.validateAndGetErrors(data)).any() return !(await this.validateAndGetErrors(data)).any()
} }
@@ -56,18 +57,20 @@ export class Validator<T> {
* @param data * @param data
* @protected * @protected
*/ */
protected async validateAndGetErrors(data: any): Promise<Messages> { protected async validateAndGetErrors(data: unknown): Promise<Messages> {
const messages = new Messages() const messages = new Messages()
const params: ValidatorFunctionParams = { data } const params: ValidatorFunctionParams = { data }
for ( const key in this.rules ) { 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 // 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 // key notation. It's not type-safe, but neither is the original input object
// yet, so it's useful here. // yet, so it's useful here.
for ( const walkEntry of dataWalkUnsafe<any>(data, key) ) { for ( const walkEntry of dataWalkUnsafe<any>(data as any, key) ) {
let [entry, dataKey] = walkEntry let [entry, dataKey] = walkEntry // eslint-disable-line prefer-const
const rules = (Array.isArray(this.rules[key]) ? this.rules[key] : [this.rules[key]]) as ValidatorFunction[] const rules = (Array.isArray(this.rules[key]) ? this.rules[key] : [this.rules[key]]) as ValidatorFunction[]
for ( const rule of rules ) { for ( const rule of rules ) {
@@ -83,13 +86,15 @@ export class Validator<T> {
} }
for ( const error of errors ) { 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 ) { if ( result.valid && result.castValue ) {
entry = result.castValue entry = result.castValue
data = dataSetUnsafe(dataKey, entry, data) data = dataSetUnsafe(dataKey, entry, data as any)
} }
if ( result.stopValidation ) { if ( result.stopValidation ) {

View File

@@ -1,8 +1,8 @@
import {Instantiable} from '../di' import {Instantiable} from '../di'
import {FormRequest} from './FormRequest' import {FormRequest} from './FormRequest'
import {ValidationError} from './Validator' import {ValidationError} from './Validator'
import {ResponseObject, RouteHandler} from "../http/routing/Route"; import {ResponseObject, RouteHandler} from '../http/routing/Route'
import {Request} from "../http/lifecycle/Request"; import {Request} from '../http/lifecycle/Request'
/** /**
* Builds a middleware function that validates a request's input against * Builds a middleware function that validates a request's input against

View File

@@ -1,30 +1,31 @@
import {ValidationResult, ValidatorFunction} from "./types"; import {ValidationResult, ValidatorFunction} from './types'
export namespace Arr {
/** Requires the input value to be an array. */ /** Requires the input value to be an array. */
export function is(fieldName: string, inputValue: any): ValidationResult { function is(fieldName: string, inputValue: unknown): ValidationResult {
if ( Array.isArray(inputValue) ) { if ( Array.isArray(inputValue) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'must be an array' message: 'must be an array',
} }
} }
/** Requires the values in the input value array to be distinct. */ /** Requires the values in the input value array to be distinct. */
export function distinct(fieldName: string, inputValue: any): ValidationResult { function distinct(fieldName: string, inputValue: unknown): ValidationResult {
const arr = is(fieldName, inputValue) 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) && (new Set(inputValue)).size === inputValue.length ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'must not contain duplicate values' message: 'must not contain duplicate values',
} }
} }
@@ -32,18 +33,20 @@ export namespace Arr {
* Builds a validator function that requires the input array to contain the given value. * Builds a validator function that requires the input array to contain the given value.
* @param value * @param value
*/ */
export function includes(value: any): ValidatorFunction { function includes(value: unknown): ValidatorFunction {
return function includes(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
const arr = is(fieldName, inputValue) const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr if ( !arr.valid ) {
return arr
}
if ( inputValue.includes(value) ) { if ( Array.isArray(inputValue) && inputValue.includes(value) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must include ${value}` message: `must include ${value}`,
} }
} }
} }
@@ -52,18 +55,20 @@ export namespace Arr {
* Builds a validator function that requires the input array NOT to contain the given value. * Builds a validator function that requires the input array NOT to contain the given value.
* @param value * @param value
*/ */
export function excludes(value: any): ValidatorFunction { function excludes(value: unknown): ValidatorFunction {
return function excludes(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
const arr = is(fieldName, inputValue) const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr if ( !arr.valid ) {
return arr
}
if ( !inputValue.includes(value) ) { if ( Array.isArray(inputValue) && !inputValue.includes(value) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must not include ${value}` message: `must not include ${value}`,
} }
} }
} }
@@ -72,18 +77,20 @@ export namespace Arr {
* Builds a validator function that requires the input array to have exactly `len` many entries. * Builds a validator function that requires the input array to have exactly `len` many entries.
* @param len * @param len
*/ */
export function length(len: number): ValidatorFunction { function length(len: number): ValidatorFunction {
return function length(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
const arr = is(fieldName, inputValue) const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr if ( !arr.valid ) {
return arr
}
if ( inputValue.length === len ) { if ( Array.isArray(inputValue) && inputValue.length === len ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be exactly of length ${len}` message: `must be exactly of length ${len}`,
} }
} }
} }
@@ -92,18 +99,20 @@ export namespace Arr {
* Builds a validator function that requires the input array to have at least `len` many entries. * Builds a validator function that requires the input array to have at least `len` many entries.
* @param len * @param len
*/ */
export function lengthMin(len: number): ValidatorFunction { function lengthMin(len: number): ValidatorFunction {
return function lengthMin(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
const arr = is(fieldName, inputValue) const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr if ( !arr.valid ) {
return arr
}
if ( inputValue.length >= len ) { if ( Array.isArray(inputValue) && inputValue.length >= len ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be at least length ${len}` message: `must be at least length ${len}`,
} }
} }
} }
@@ -112,19 +121,30 @@ export namespace Arr {
* Builds a validator function that requires the input array to have at most `len` many entries. * Builds a validator function that requires the input array to have at most `len` many entries.
* @param len * @param len
*/ */
export function lengthMax(len: number): ValidatorFunction { function lengthMax(len: number): ValidatorFunction {
return function lengthMax(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
const arr = is(fieldName, inputValue) const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr if ( !arr.valid ) {
return arr
}
if ( inputValue.length <= len ) { if ( Array.isArray(inputValue) && inputValue.length <= len ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be at most length ${len}` message: `must be at most length ${len}`,
} }
} }
} }
export const Arr = {
is,
distinct,
includes,
excludes,
length,
lengthMin,
lengthMax,
} }

View File

@@ -1,9 +1,8 @@
import {infer as inferUtil} from '../../util' 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. */ /** Attempt to infer the native type of a string value. */
export function infer(fieldName: string, inputValue: any): ValidationResult { function infer(fieldName: string, inputValue: unknown): ValidationResult {
return { return {
valid: true, valid: true,
castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue, castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue,
@@ -18,11 +17,15 @@ export namespace Cast {
* @param fieldName * @param fieldName
* @param inputValue * @param inputValue
*/ */
export function boolean(fieldName: string, inputValue: any): ValidationResult { function boolean(fieldName: string, inputValue: unknown): ValidationResult {
let castValue = !!inputValue let castValue = Boolean(inputValue)
if ( ['true', 'True', 'TRUE', '1'].includes(inputValue) ) castValue = true if ( ['true', 'True', 'TRUE', '1'].includes(String(inputValue)) ) {
if ( ['false', 'False', 'FALSE', '0'].includes(inputValue) ) castValue = false castValue = true
}
if ( ['false', 'False', 'FALSE', '0'].includes(String(inputValue)) ) {
castValue = false
}
return { return {
valid: true, valid: true,
@@ -31,7 +34,7 @@ export namespace Cast {
} }
/** Casts the input value to a string. */ /** Casts the input value to a string. */
export function string(fieldName: string, inputValue: any): ValidationResult { function string(fieldName: string, inputValue: unknown): ValidationResult {
return { return {
valid: true, valid: true,
castValue: String(inputValue), castValue: String(inputValue),
@@ -39,11 +42,11 @@ export namespace Cast {
} }
/** Casts the input value to a number, if it is numerical. Fails otherwise. */ /** Casts the input value to a number, if it is numerical. Fails otherwise. */
export function numeric(fieldName: string, inputValue: any): ValidationResult { function numeric(fieldName: string, inputValue: unknown): ValidationResult {
if ( !isNaN(parseFloat(inputValue)) ) { if ( !isNaN(parseFloat(String(inputValue))) ) {
return { return {
valid: true, valid: true,
castValue: parseFloat(inputValue) castValue: parseFloat(String(inputValue)),
} }
} }
@@ -54,11 +57,11 @@ export namespace Cast {
} }
/** Casts the input value to an integer. Fails otherwise. */ /** Casts the input value to an integer. Fails otherwise. */
export function integer(fieldName: string, inputValue: any): ValidationResult { function integer(fieldName: string, inputValue: unknown): ValidationResult {
if ( !isNaN(parseInt(inputValue)) ) { if ( !isNaN(parseInt(String(inputValue), 10)) ) {
return { return {
valid: true, valid: true,
castValue: parseInt(inputValue) castValue: parseInt(String(inputValue), 10),
} }
} }
@@ -67,4 +70,11 @@ export namespace Cast {
message: 'must be an integer', message: 'must be an integer',
} }
} }
export const Cast = {
infer,
boolean,
string,
numeric,
integer,
} }

View File

@@ -1,19 +1,18 @@
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. * Builds a validator function that requires the input value to be greater than some value.
* @param value * @param value
*/ */
export function greaterThan(value: number): ValidatorFunction { function greaterThan(value: number): ValidatorFunction {
return function greaterThan(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue > value ) { if ( Number(inputValue) > value ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be greater than ${value}` message: `must be greater than ${value}`,
} }
} }
} }
@@ -22,15 +21,15 @@ export namespace Num {
* Builds a validator function that requires the input value to be at least some value. * Builds a validator function that requires the input value to be at least some value.
* @param value * @param value
*/ */
export function atLeast(value: number): ValidatorFunction { function atLeast(value: number): ValidatorFunction {
return function atLeast(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue >= value ) { if ( Number(inputValue) >= value ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be at least ${value}` message: `must be at least ${value}`,
} }
} }
} }
@@ -39,15 +38,15 @@ export namespace Num {
* Builds a validator function that requires the input value to be less than some value. * Builds a validator function that requires the input value to be less than some value.
* @param value * @param value
*/ */
export function lessThan(value: number): ValidatorFunction { function lessThan(value: number): ValidatorFunction {
return function lessThan(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue < value ) { if ( Number(inputValue) < value ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be less than ${value}` message: `must be less than ${value}`,
} }
} }
} }
@@ -56,15 +55,15 @@ export namespace Num {
* Builds a validator function that requires the input value to be at most some value. * Builds a validator function that requires the input value to be at most some value.
* @param value * @param value
*/ */
export function atMost(value: number): ValidatorFunction { function atMost(value: number): ValidatorFunction {
return function atMost(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue <= value ) { if ( Number(inputValue) <= value ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be at most ${value}` message: `must be at most ${value}`,
} }
} }
} }
@@ -73,15 +72,15 @@ export namespace Num {
* Builds a validator function that requires the input value to have exactly `num` many digits. * Builds a validator function that requires the input value to have exactly `num` many digits.
* @param num * @param num
*/ */
export function digits(num: number): ValidatorFunction { function digits(num: number): ValidatorFunction {
return function digits(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( String(inputValue).replace('.', '').length === num ) { if ( String(inputValue).replace('.', '').length === num ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must have exactly ${num} digits` message: `must have exactly ${num} digits`,
} }
} }
} }
@@ -90,15 +89,15 @@ export namespace Num {
* Builds a validator function that requires the input value to have at least `num` many digits. * Builds a validator function that requires the input value to have at least `num` many digits.
* @param num * @param num
*/ */
export function digitsMin(num: number): ValidatorFunction { function digitsMin(num: number): ValidatorFunction {
return function digitsMin(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( String(inputValue).replace('.', '').length >= num ) { if ( String(inputValue).replace('.', '').length >= num ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must have at least ${num} digits` message: `must have at least ${num} digits`,
} }
} }
} }
@@ -107,15 +106,15 @@ export namespace Num {
* Builds a validator function that requires the input value to have at most `num` many digits. * Builds a validator function that requires the input value to have at most `num` many digits.
* @param num * @param num
*/ */
export function digitsMax(num: number): ValidatorFunction { function digitsMax(num: number): ValidatorFunction {
return function digitsMax(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( String(inputValue).replace('.', '').length <= num ) { if ( String(inputValue).replace('.', '').length <= num ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must have at most ${num} digits` message: `must have at most ${num} digits`,
} }
} }
} }
@@ -124,15 +123,15 @@ export namespace Num {
* Builds a validator function that requires the input value to end with the given number sequence. * Builds a validator function that requires the input value to end with the given number sequence.
* @param num * @param num
*/ */
export function ends(num: number): ValidatorFunction { function ends(num: number): ValidatorFunction {
return function ends(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( String(inputValue).endsWith(String(num)) ) { if ( String(inputValue).endsWith(String(num)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must end with "${num}"` message: `must end with "${num}"`,
} }
} }
} }
@@ -141,15 +140,15 @@ export namespace Num {
* Builds a validator function that requires the input value to begin with the given number sequence. * Builds a validator function that requires the input value to begin with the given number sequence.
* @param num * @param num
*/ */
export function begins(num: number): ValidatorFunction { function begins(num: number): ValidatorFunction {
return function begins(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( String(inputValue).startsWith(String(num)) ) { if ( String(inputValue).startsWith(String(num)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must begin with "${num}"` message: `must begin with "${num}"`,
} }
} }
} }
@@ -158,22 +157,22 @@ export namespace Num {
* Builds a validator function that requires the input value to be a multiple of the given number. * Builds a validator function that requires the input value to be a multiple of the given number.
* @param num * @param num
*/ */
export function multipleOf(num: number): ValidatorFunction { function multipleOf(num: number): ValidatorFunction {
return function multipleOf(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue % num === 0 ) { if ( parseFloat(String(inputValue)) % num === 0 ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be a multiple of ${num}` message: `must be a multiple of ${num}`,
} }
} }
} }
/** Requires the input value to be even. */ /** Requires the input value to be even. */
export function even(fieldName: string, inputValue: any): ValidationResult { function even(fieldName: string, inputValue: unknown): ValidationResult {
if ( inputValue % 2 === 0 ) { if ( parseFloat(String(inputValue)) % 2 === 0 ) {
return { valid: true } return { valid: true }
} }
@@ -184,8 +183,8 @@ export namespace Num {
} }
/** Requires the input value to be odd. */ /** Requires the input value to be odd. */
export function odd(fieldName: string, inputValue: any): ValidationResult { function odd(fieldName: string, inputValue: unknown): ValidationResult {
if ( inputValue % 2 === 0 ) { if ( parseFloat(String(inputValue)) % 2 === 0 ) {
return { valid: true } return { valid: true }
} }
@@ -194,4 +193,18 @@ export namespace Num {
message: 'must be odd', message: 'must be odd',
} }
} }
export const Num = {
greaterThan,
atLeast,
lessThan,
atMost,
digits,
digitsMin,
digitsMax,
ends,
begins,
multipleOf,
even,
odd,
} }

View File

@@ -1,46 +1,45 @@
import {ValidationResult, ValidatorFunction} from "./types"; import {ValidationResult, ValidatorFunction} from './types'
import {UniversalPath} from '../../util' import {UniversalPath} from '../../util'
export namespace Is {
/** Requires the given input value to be some form of affirmative boolean. */ /** Requires the given input value to be some form of affirmative boolean. */
export function accepted(fieldName: string, inputValue: any): ValidationResult { function accepted(fieldName: string, inputValue: unknown): ValidationResult {
if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(inputValue) ) { if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(String(inputValue)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'must be accepted' message: 'must be accepted',
} }
} }
/** Requires the given input value to be some form of boolean. */ /** Requires the given input value to be some form of boolean. */
export function boolean(fieldName: string, inputValue: any): ValidationResult { function boolean(fieldName: string, inputValue: unknown): ValidationResult {
const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0] const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0]
if ( boolish.includes(inputValue) ) { if ( boolish.includes(String(inputValue)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'must be true or false' message: 'must be true or false',
} }
} }
/** Requires the input value to be of type string. */ /** Requires the input value to be of type string. */
export function string(fieldName: string, inputValue: any): ValidationResult { function string(fieldName: string, inputValue: unknown): ValidationResult {
if ( typeof inputValue === 'string' ) { if ( typeof inputValue === 'string' ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'must be a string' message: 'must be a string',
} }
} }
/** Requires the given input value to be present and non-nullish. */ /** Requires the given input value to be present and non-nullish. */
export function required(fieldName: string, inputValue: any): ValidationResult { function required(fieldName: string, inputValue: unknown): ValidationResult {
if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) { if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) {
return { valid: true } return { valid: true }
} }
@@ -53,17 +52,17 @@ export namespace Is {
} }
/** Alias of required(). */ /** Alias of required(). */
export function present(fieldName: string, inputValue: any): ValidationResult { function present(fieldName: string, inputValue: unknown): ValidationResult {
return required(fieldName, inputValue) return required(fieldName, inputValue)
} }
/** Alias of required(). */ /** Alias of required(). */
export function filled(fieldName: string, inputValue: any): ValidationResult { function filled(fieldName: string, inputValue: unknown): ValidationResult {
return required(fieldName, inputValue) return required(fieldName, inputValue)
} }
/** Requires the given input value to be absent or nullish. */ /** Requires the given input value to be absent or nullish. */
export function prohibited(fieldName: string, inputValue: any): ValidationResult { function prohibited(fieldName: string, inputValue: unknown): ValidationResult {
if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) { if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) {
return { valid: true } return { valid: true }
} }
@@ -76,12 +75,12 @@ export namespace Is {
} }
/** Alias of prohibited(). */ /** Alias of prohibited(). */
export function absent(fieldName: string, inputValue: any): ValidationResult { function absent(fieldName: string, inputValue: unknown): ValidationResult {
return prohibited(fieldName, inputValue) return prohibited(fieldName, inputValue)
} }
/** Alias of prohibited(). */ /** Alias of prohibited(). */
export function empty(fieldName: string, inputValue: any): ValidationResult { function empty(fieldName: string, inputValue: unknown): ValidationResult {
return prohibited(fieldName, inputValue) return prohibited(fieldName, inputValue)
} }
@@ -89,15 +88,15 @@ export namespace Is {
* Builds a validator function that requires the given input to be found in an array of values. * Builds a validator function that requires the given input to be found in an array of values.
* @param values * @param values
*/ */
export function foundIn(values: any[]): ValidatorFunction { function foundIn(values: any[]): ValidatorFunction {
return function foundIn(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( values.includes(inputValue) ) { if ( values.includes(inputValue) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be one of: ${values.join(', ')}` message: `must be one of: ${values.join(', ')}`,
} }
} }
} }
@@ -106,22 +105,22 @@ export namespace Is {
* Builds a validator function that requires the given input NOT to be found in an array of values. * Builds a validator function that requires the given input NOT to be found in an array of values.
* @param values * @param values
*/ */
export function notFoundIn(values: any[]): ValidatorFunction { function notFoundIn(values: any[]): ValidatorFunction {
return function foundIn(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( values.includes(inputValue) ) { if ( values.includes(inputValue) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be one of: ${values.join(', ')}` message: `must be one of: ${values.join(', ')}`,
} }
} }
} }
/** Requires the input value to be number-like. */ /** Requires the input value to be number-like. */
export function numeric(fieldName: string, inputValue: any): ValidationResult { function numeric(fieldName: string, inputValue: unknown): ValidationResult {
if ( !isNaN(parseFloat(inputValue)) ) { if ( !isNaN(parseFloat(String(inputValue))) ) {
return { valid: true } return { valid: true }
} }
@@ -132,8 +131,8 @@ export namespace Is {
} }
/** Requires the given input value to be integer-like. */ /** Requires the given input value to be integer-like. */
export function integer(fieldName: string, inputValue: any): ValidationResult { function integer(fieldName: string, inputValue: unknown): ValidationResult {
if ( !isNaN(parseInt(inputValue)) && parseInt(inputValue) === parseFloat(inputValue) ) { if ( !isNaN(parseInt(String(inputValue), 10)) && parseInt(String(inputValue), 10) === parseFloat(String(inputValue)) ) {
return { valid: true } return { valid: true }
} }
@@ -144,14 +143,14 @@ export namespace Is {
} }
/** Requires the given input value to be a UniversalPath. */ /** Requires the given input value to be a UniversalPath. */
export function file(fieldName: string, inputValue: any): ValidationResult { function file(fieldName: string, inputValue: unknown): ValidationResult {
if ( inputValue instanceof UniversalPath ) { if ( inputValue instanceof UniversalPath ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'must be a file' message: 'must be a file',
} }
} }
@@ -162,7 +161,7 @@ export namespace Is {
* @param fieldName * @param fieldName
* @param inputValue * @param inputValue
*/ */
export function optional(fieldName: string, inputValue: any): ValidationResult { function optional(fieldName: string, inputValue: unknown): ValidationResult {
if ( inputValue ?? true ) { if ( inputValue ?? true ) {
return { return {
valid: true, valid: true,
@@ -172,4 +171,21 @@ export namespace Is {
return { valid: true } return { valid: true }
} }
export const Is = {
accepted,
boolean,
string,
required,
present,
filled,
prohibited,
absent,
empty,
foundIn,
notFoundIn,
numeric,
integer,
file,
optional,
} }

View File

@@ -1,93 +1,92 @@
import {ValidationResult, ValidatorFunction} from "./types"; import {ValidationResult, ValidatorFunction} from './types'
import {isJSON} from '../../util' import {isJSON} from '../../util'
/** /**
* String-related validation rules. * String-related validation rules.
*/ */
export namespace Str {
const regexes: {[key: string]: RegExp} = { const regexes: {[key: string]: RegExp} = {
'string.is.alpha': /[a-zA-Z]*/, 'string.is.alpha': /[a-zA-Z]*/,
'string.is.alpha_num': /[a-zA-Z0-9]*/, 'string.is.alpha_num': /[a-zA-Z0-9]*/,
'string.is.alpha_dash': /[a-zA-Z\-]*/, 'string.is.alpha_dash': /[a-zA-Z-]*/,
'string.is.alpha_score': /[a-zA-Z_]*/, 'string.is.alpha_score': /[a-zA-Z_]*/,
'string.is.alpha_num_dash_score': /[a-zA-Z\-_0-9]*/, '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.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': /((^\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.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.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.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.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}$/, '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: any, message: string): ValidationResult { function validateRex(key: string, inputValue: unknown, message: string): ValidationResult {
if ( regexes[key].test(inputValue) ) { if ( regexes[key].test(String(inputValue)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message message,
} }
} }
/** Requires the input value to be alphabetical characters only. */ /** Requires the input value to be alphabetical characters only. */
export function alpha(fieldName: string, inputValue: any): ValidationResult { function alpha(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.alpha', inputValue, 'must be alphabetical only') return validateRex('string.is.alpha', inputValue, 'must be alphabetical only')
} }
/** Requires the input value to be alphanumeric characters only. */ /** Requires the input value to be alphanumeric characters only. */
export function alphaNum(fieldName: string, inputValue: any): ValidationResult { function alphaNum(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only') return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only')
} }
/** Requires the input value to be alphabetical characters or the "-" character only. */ /** Requires the input value to be alphabetical characters or the "-" character only. */
export function alphaDash(fieldName: string, inputValue: any): ValidationResult { function alphaDash(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.alpha_dash', inputValue, 'must be alphabetical and dashes only') 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. */ /** Requires the input value to be alphabetical characters or the "_" character only. */
export function alphaScore(fieldName: string, inputValue: any): ValidationResult { function alphaScore(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.alpha_score', inputValue, 'must be alphabetical and underscores only') 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. */ /** Requires the input value to be alphabetical characters, numeric characters, "-", or "_" only. */
export function alphaNumDashScore(fieldName: string, inputValue: any): ValidationResult { function alphaNumDashScore(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.alpha_num_dash_score', inputValue, 'must be alphanumeric, dashes, and underscores only') 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. */ /** Requires the input value to be a valid RFC email address format. */
export function email(fieldName: string, inputValue: any): ValidationResult { function email(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.email', inputValue, 'must be an email address') return validateRex('string.is.email', inputValue, 'must be an email address')
} }
/** Requires the input value to be a valid IPv4 or IPv6 address. */ /** Requires the input value to be a valid IPv4 or IPv6 address. */
export function ip(fieldName: string, inputValue: any): ValidationResult { function ip(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.ip', inputValue, 'must be a valid IP address') return validateRex('string.is.ip', inputValue, 'must be a valid IP address')
} }
/** Requires the input value to be a valid IPv4 address. */ /** Requires the input value to be a valid IPv4 address. */
export function ipv4(fieldName: string, inputValue: any): ValidationResult { function ipv4(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.ip.v4', inputValue, 'must be a valid IP version 4 address') 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. */ /** Requires the input value to be a valid IPv6 address. */
export function ipv6(fieldName: string, inputValue: any): ValidationResult { function ipv6(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.ip.v6', inputValue, 'must be a valid IP version 6 address') 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. */ /** Requires the input value to be a valid file MIME type. */
export function mime(fieldName: string, inputValue: any): ValidationResult { function mime(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type') return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type')
} }
/** Requires the input value to be a valid RFC URL format. */ /** Requires the input value to be a valid RFC URL format. */
export function url(fieldName: string, inputValue: any): ValidationResult { function url(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.url', inputValue, 'must be a valid URL') return validateRex('string.is.url', inputValue, 'must be a valid URL')
} }
/** Requires the input value to be a valid RFC UUID format. */ /** Requires the input value to be a valid RFC UUID format. */
export function uuid(fieldName: string, inputValue: any): ValidationResult { function uuid(fieldName: string, inputValue: unknown): ValidationResult {
return validateRex('string.is.uuid', inputValue, 'must be a valid UUID') return validateRex('string.is.uuid', inputValue, 'must be a valid UUID')
} }
@@ -95,15 +94,15 @@ export namespace Str {
* Builds a validation function that requires the input value to match the given regex. * Builds a validation function that requires the input value to match the given regex.
* @param rex * @param rex
*/ */
export function regex(rex: RegExp): ValidatorFunction { function regex(rex: RegExp): ValidatorFunction {
return function regex(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( rex.test(inputValue) ) { if ( rex.test(String(inputValue)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'is not valid' message: 'is not valid',
} }
} }
} }
@@ -112,15 +111,15 @@ export namespace Str {
* Builds a validation function that requires the input to NOT match the given regex. * Builds a validation function that requires the input to NOT match the given regex.
* @param rex * @param rex
*/ */
export function notRegex(rex: RegExp): ValidatorFunction { function notRegex(rex: RegExp): ValidatorFunction {
return function notRegex(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( !rex.test(inputValue) ) { if ( !rex.test(String(inputValue)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'is not valid' message: 'is not valid',
} }
} }
} }
@@ -129,15 +128,15 @@ export namespace Str {
* Builds a validation function that requires the given input to end with the substring. * Builds a validation function that requires the given input to end with the substring.
* @param substr * @param substr
*/ */
export function ends(substr: string): ValidatorFunction { function ends(substr: string): ValidatorFunction {
return function ends(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( String(inputValue).endsWith(substr) ) { if ( String(inputValue).endsWith(substr) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must end with "${substr}"` message: `must end with "${substr}"`,
} }
} }
} }
@@ -146,28 +145,28 @@ export namespace Str {
* Builds a validation function that requires the given input to begin with the substring. * Builds a validation function that requires the given input to begin with the substring.
* @param substr * @param substr
*/ */
export function begins(substr: string): ValidatorFunction { function begins(substr: string): ValidatorFunction {
return function begins(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( String(inputValue).startsWith(substr) ) { if ( String(inputValue).startsWith(substr) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must begin with "${substr}"` message: `must begin with "${substr}"`,
} }
} }
} }
/** Requires the input value to be a valid JSON string. */ /** Requires the input value to be a valid JSON string. */
export function json(fieldName: string, inputValue: any): ValidationResult { function json(fieldName: string, inputValue: unknown): ValidationResult {
if ( isJSON(inputValue) ) { if ( isJSON(String(inputValue)) ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: 'must be valid JSON' message: 'must be valid JSON',
} }
} }
@@ -175,15 +174,15 @@ export namespace Str {
* Builds a validator function that requires the input value to have exactly len many characters. * Builds a validator function that requires the input value to have exactly len many characters.
* @param len * @param len
*/ */
export function length(len: number): ValidatorFunction { function length(len: number): ValidatorFunction {
return function length(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue.length === len ) { if ( String(inputValue).length === len ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be exactly of length ${len}` message: `must be exactly of length ${len}`,
} }
} }
} }
@@ -192,15 +191,15 @@ export namespace Str {
* Builds a validator function that requires the input value to have at least len many characters. * Builds a validator function that requires the input value to have at least len many characters.
* @param len * @param len
*/ */
export function lengthMin(len: number): ValidatorFunction { function lengthMin(len: number): ValidatorFunction {
return function lengthMin(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue.length >= len ) { if ( String(inputValue).length >= len ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be at least length ${len}` message: `must be at least length ${len}`,
} }
} }
} }
@@ -209,16 +208,38 @@ export namespace Str {
* Builds a validator function that requires the input value to have at most len many characters. * Builds a validator function that requires the input value to have at most len many characters.
* @param len * @param len
*/ */
export function lengthMax(len: number): ValidatorFunction { function lengthMax(len: number): ValidatorFunction {
return function lengthMax(fieldName: string, inputValue: any): ValidationResult { return (fieldName: string, inputValue: unknown): ValidationResult => {
if ( inputValue.length <= len ) { if ( String(inputValue).length <= len ) {
return { valid: true } return { valid: true }
} }
return { return {
valid: false, valid: false,
message: `must be at most length ${len}` 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,
} }

View File

@@ -1,14 +1,12 @@
import {UniversalPath} from '../../util'
import {Template} from '../../cli' import {Template} from '../../cli'
const form_template: Template = { const templateForm: Template = {
name: 'form', name: 'form',
fileSuffix: '.form.ts', fileSuffix: '.form.ts',
description: 'Create a new form request validator', description: 'Create a new form request validator',
baseAppPath: ['http', 'forms'], baseAppPath: ['http', 'forms'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) { render(name: string) {
return `import {FormRequest, ValidationRules, Rule} from '@extollo/forms' return `import {Injectable, FormRequest, ValidationRules, Rule} from '@extollo/lib'
import {Injectable} from '@extollo/di'
/** /**
* ${name} object * ${name} object
@@ -40,7 +38,7 @@ export class ${name}FormRequest extends FormRequest<${name}Form> {
} }
} }
` `
} },
} }
export { form_template } export { templateForm }

View File

@@ -1,8 +1,8 @@
import {Singleton, Inject} from '../../di' import {Singleton, Inject} from '../../di'
import {CommandLine} from '../../cli' import {CommandLine} from '../../cli'
import {form_template} from '../templates/form' import {templateForm} from '../templates/form'
import {Unit} from "../../lifecycle/Unit"; import {Unit} from '../../lifecycle/Unit'
import {Logging} from "../../service/Logging"; import {Logging} from '../../service/Logging'
@Singleton() @Singleton()
export class Forms extends Unit { export class Forms extends Unit {
@@ -13,6 +13,6 @@ export class Forms extends Unit {
protected readonly logging!: Logging protected readonly logging!: Logging
public async up(): Promise<void> { public async up(): Promise<void> {
this.cli.registerTemplate(form_template) this.cli.registerTemplate(templateForm)
} }
} }

View File

@@ -1,5 +1,6 @@
import {AppClass} from "../lifecycle/AppClass"; import {AppClass} from '../lifecycle/AppClass'
import {Request} from "./lifecycle/Request"; import {Request} from './lifecycle/Request'
import {Container} from '../di'
/** /**
* Base class for controllers that define methods that * Base class for controllers that define methods that
@@ -7,10 +8,12 @@ import {Request} from "./lifecycle/Request";
*/ */
export class Controller extends AppClass { export class Controller extends AppClass {
constructor( constructor(
protected readonly request: Request protected readonly request: Request,
) { super() } ) {
super()
}
protected container() { protected container(): Container {
return this.request return this.request
} }
} }

View File

@@ -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. * An error class that has an associated HTTP status.
@@ -9,7 +9,7 @@ import {ErrorWithContext, HTTPStatus, HTTPMessage} from "../util"
export class HTTPError extends ErrorWithContext { export class HTTPError extends ErrorWithContext {
constructor( constructor(
public readonly status: HTTPStatus = 500, public readonly status: HTTPStatus = 500,
public readonly message: string = '' public readonly message: string = '',
) { ) {
super('HTTP ERROR') super('HTTP ERROR')
this.message = message || HTTPMessage[status] this.message = message || HTTPMessage[status]

View File

@@ -1,5 +1,5 @@
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
import {uninfer, infer, uuid_v4} from "../../util"; import {uninfer, infer, uuid4} from '../../util'
/** /**
* Base type representing a parsed cookie. * Base type representing a parsed cookie.
@@ -61,7 +61,7 @@ export class HTTPCookieJar {
* @param value * @param value
* @param options * @param options
*/ */
set(name: string, value: any, options?: HTTPCookieOptions) { set(name: string, value: unknown, options?: HTTPCookieOptions): this {
this.parsed[name] = { this.parsed[name] = {
key: name, key: name,
value, value,
@@ -69,14 +69,16 @@ export class HTTPCookieJar {
exists: false, exists: false,
options, options,
} }
return this
} }
/** /**
* Returns true if a cookie exists with the given name. * Returns true if a cookie exists with the given name.
* @param name * @param name
*/ */
has(name: string) { has(name: string): boolean {
return !!this.parsed[name] return Boolean(this.parsed[name])
} }
/** /**
@@ -88,17 +90,21 @@ export class HTTPCookieJar {
* @param name * @param name
* @param options * @param options
*/ */
clear(name: string, options?: HTTPCookieOptions) { clear(name: string, options?: HTTPCookieOptions): this {
if ( !options ) options = {} if ( !options ) {
options = {}
}
options.expires = new Date(0) options.expires = new Date(0)
this.parsed[name] = { this.parsed[name] = {
key: name, key: name,
value: undefined, value: undefined,
originalValue: uuid_v4(), originalValue: uuid4(),
exists: false, exists: false,
options, options,
} }
return this
} }
/** /**
@@ -108,10 +114,14 @@ export class HTTPCookieJar {
const headers: string[] = [] const headers: string[] = []
for ( const key in this.parsed ) { 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] const cookie = this.parsed[key]
if ( cookie.exists ) continue if ( cookie.exists ) {
continue
}
const parts = [] const parts = []
parts.push(`${key}=${encodeURIComponent(cookie.originalValue)}`) parts.push(`${key}=${encodeURIComponent(cookie.originalValue)}`)
@@ -144,7 +154,7 @@ export class HTTPCookieJar {
const map = { const map = {
strict: 'Strict', strict: 'Strict',
lax: 'Lax', lax: 'Lax',
'none-secure': 'None; Secure' 'none-secure': 'None; Secure',
} }
parts.push(map[cookie.options.sameSite]) parts.push(map[cookie.options.sameSite])
@@ -163,7 +173,9 @@ export class HTTPCookieJar {
const parts = cookie.split('=') const parts = cookie.split('=')
const key = parts.shift()?.trim() const key = parts.shift()?.trim()
if ( !key ) return; if ( !key ) {
return
}
const value = decodeURI(parts.join('=')) const value = decodeURI(parts.join('='))

View File

@@ -1,10 +1,10 @@
import {Inject, Instantiable, Singleton} from "../../di" import {Inject, Instantiable, Singleton} from '../../di'
import {Collection, HTTPStatus} from "../../util" import {Collection, HTTPStatus} from '../../util'
import {HTTPKernelModule} from "./HTTPKernelModule"; import {HTTPKernelModule} from './HTTPKernelModule'
import {Logging} from "../../service/Logging"; import {Logging} from '../../service/Logging'
import {AppClass} from "../../lifecycle/AppClass"; import {AppClass} from '../../lifecycle/AppClass'
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
import {error} from "../response/ErrorResponseFactory"; import {error} from '../response/ErrorResponseFactory'
/** /**
* Interface for fluently registering kernel modules into the kernel. * Interface for fluently registering kernel modules into the kernel.
@@ -105,7 +105,8 @@ export class HTTPKernel extends AppClass {
} }
} catch (e: any) { } catch (e: any) {
this.logging.error(e) 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') this.logging.verbose('Finished kernel lifecycle')
@@ -127,16 +128,16 @@ export class HTTPKernel extends AppClass {
return this return this
} }
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other) let foundIdx = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
if ( typeof found_index !== 'undefined' ) { if ( typeof foundIdx !== 'undefined' ) {
this.preflight = this.preflight.put(found_index, this.app().make(module)) this.preflight = this.preflight.put(foundIdx, this.app().make(module))
return this return this
} else { } 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' ) { if ( typeof foundIdx !== 'undefined' ) {
this.postflight = this.postflight.put(found_index, this.app().make(module)) this.postflight = this.postflight.put(foundIdx, this.app().make(module))
} else { } else {
throw new KernelModuleNotFoundError(other.name) throw new KernelModuleNotFoundError(other.name)
} }
@@ -149,16 +150,16 @@ export class HTTPKernel extends AppClass {
return this return this
} }
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other) let foundIdx = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
if ( typeof found_index !== 'undefined' ) { if ( typeof foundIdx !== 'undefined' ) {
this.preflight = this.preflight.put(found_index + 1, this.app().make(module)) this.preflight = this.preflight.put(foundIdx + 1, this.app().make(module))
return this return this
} else { } 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' ) { if ( typeof foundIdx !== 'undefined' ) {
this.postflight = this.postflight.put(found_index + 1, this.app().make(module)) this.postflight = this.postflight.put(foundIdx + 1, this.app().make(module))
} else { } else {
throw new KernelModuleNotFoundError(other.name) throw new KernelModuleNotFoundError(other.name)
} }

View File

@@ -1,7 +1,7 @@
import {Injectable} from "../../di"; import {Injectable} from '../../di'
import {AppClass} from "../../lifecycle/AppClass"; import {AppClass} from '../../lifecycle/AppClass'
import {HTTPKernel} from "./HTTPKernel"; import {HTTPKernel} from './HTTPKernel'
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
/** /**
* Base class for modules that define logic that is applied to requests * Base class for modules that define logic that is applied to requests
@@ -23,7 +23,7 @@ export class HTTPKernelModule extends AppClass {
* @param {Request} request * @param {Request} request
* @return Promise<boolean> * @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 return true
} }
@@ -40,7 +40,7 @@ export class HTTPKernelModule extends AppClass {
* Register this module with the given HTTP kernel. * Register this module with the given HTTP kernel.
* @param {HTTPKernel} kernel * @param {HTTPKernel} kernel
*/ */
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).before() kernel.register(this).before()
} }
} }

View File

@@ -1,9 +1,9 @@
import {HTTPKernelModule} from "../HTTPKernelModule"; import {HTTPKernelModule} from '../HTTPKernelModule'
import {ResponseObject} from "../../routing/Route"; import {ResponseObject} from '../../routing/Route'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {plaintext} from "../../response/StringResponseFactory"; import {plaintext} from '../../response/StringResponseFactory'
import {ResponseFactory} from "../../response/ResponseFactory"; import {ResponseFactory} from '../../response/ResponseFactory'
import {json} from "../../response/JSONResponseFactory"; import {json} from '../../response/JSONResponseFactory'
/** /**
* Base class for HTTP kernel modules that apply some response from a route handler to the request. * 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 * @param request
* @protected * @protected
*/ */
protected async applyResponseObject(object: ResponseObject, request: Request) { protected async applyResponseObject(object: ResponseObject, request: Request): Promise<void> {
if ( (typeof object === 'string') || (typeof object === 'number') ) { if ( (typeof object === 'string') || (typeof object === 'number') ) {
object = plaintext(String(object)) object = plaintext(String(object))
} }

View File

@@ -1,10 +1,10 @@
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {ActivatedRoute} from "../../routing/ActivatedRoute"; import {ActivatedRoute} from '../../routing/ActivatedRoute'
import {ResponseObject} from "../../routing/Route"; import {ResponseObject} from '../../routing/Route'
import {http} from "../../response/HTTPErrorResponseFactory"; import {http} from '../../response/HTTPErrorResponseFactory'
import {HTTPStatus} from "../../../util"; import {HTTPStatus} from '../../../util'
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule"; import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
/** /**
* HTTP kernel module that runs the handler for the request's route. * 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. * In most cases, this is the controller method defined by the route.
*/ */
export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRouteHandlerHTTPModule { export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).core() kernel.register(this).core()
} }
public async apply(request: Request) { public async apply(request: Request): Promise<Request> {
if ( request.hasInstance(ActivatedRoute) ) { if ( request.hasInstance(ActivatedRoute) ) {
const route = <ActivatedRoute> request.make(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) await this.applyResponseObject(object, request)
} else { } else {

View File

@@ -1,9 +1,9 @@
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {ActivatedRoute} from "../../routing/ActivatedRoute"; import {ActivatedRoute} from '../../routing/ActivatedRoute'
import {ResponseObject} from "../../routing/Route"; import {ResponseObject} from '../../routing/Route'
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule"; import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule"; import {PersistSessionHTTPModule} from './PersistSessionHTTPModule'
/** /**
* HTTP kernel module that executes the postflight handlers for the route. * HTTP kernel module that executes the postflight handlers for the route.
@@ -11,18 +11,18 @@ import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
* Usually, this is post middleware. * Usually, this is post middleware.
*/ */
export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule { export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).before(PersistSessionHTTPModule) kernel.register(this).before(PersistSessionHTTPModule)
} }
public async apply(request: Request) { public async apply(request: Request): Promise<Request> {
if ( request.hasInstance(ActivatedRoute) ) { if ( request.hasInstance(ActivatedRoute) ) {
const route = <ActivatedRoute> request.make(ActivatedRoute) const route = <ActivatedRoute> request.make(ActivatedRoute)
const postflight = route.postflight const postflight = route.postflight
for ( const handler of postflight ) { for ( const handler of postflight ) {
const result: ResponseObject = await handler(request) const result: ResponseObject = await handler(request)
if ( typeof result !== "undefined" ) { if ( typeof result !== 'undefined' ) {
await this.applyResponseObject(result, request) await this.applyResponseObject(result, request)
request.response.blockingWriteback(true) request.response.blockingWriteback(true)
} }

View File

@@ -1,9 +1,9 @@
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {MountActivatedRouteHTTPModule} from "./MountActivatedRouteHTTPModule"; import {MountActivatedRouteHTTPModule} from './MountActivatedRouteHTTPModule'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {ActivatedRoute} from "../../routing/ActivatedRoute"; import {ActivatedRoute} from '../../routing/ActivatedRoute'
import {ResponseObject} from "../../routing/Route"; import {ResponseObject} from '../../routing/Route'
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule"; import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
/** /**
* HTTP Kernel module that executes the preflight handlers for the route. * 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. * Usually, this is the pre middleware.
*/ */
export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule { export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).after(MountActivatedRouteHTTPModule) kernel.register(this).after(MountActivatedRouteHTTPModule)
} }
public async apply(request: Request) { public async apply(request: Request): Promise<Request> {
if ( request.hasInstance(ActivatedRoute) ) { if ( request.hasInstance(ActivatedRoute) ) {
const route = <ActivatedRoute> request.make(ActivatedRoute) const route = <ActivatedRoute> request.make(ActivatedRoute)
const preflight = route.preflight const preflight = route.preflight
for ( const handler of preflight ) { for ( const handler of preflight ) {
const result: ResponseObject = await handler(request) const result: ResponseObject = await handler(request)
if ( typeof result !== "undefined" ) { if ( typeof result !== 'undefined' ) {
await this.applyResponseObject(result, request) await this.applyResponseObject(result, request)
request.response.blockingWriteback(true) request.response.blockingWriteback(true)
} }

View File

@@ -1,11 +1,11 @@
import {HTTPKernelModule} from "../HTTPKernelModule"; import {HTTPKernelModule} from '../HTTPKernelModule'
import {Injectable} from "../../../di" import {Injectable} from '../../../di'
import {ErrorWithContext} from "../../../util" import {ErrorWithContext} from '../../../util'
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {SetSessionCookieHTTPModule} from "./SetSessionCookieHTTPModule"; import {SetSessionCookieHTTPModule} from './SetSessionCookieHTTPModule'
import {SessionFactory} from "../../session/SessionFactory"; import {SessionFactory} from '../../session/SessionFactory'
import {Session} from "../../session/Session"; import {Session} from '../../session/Session'
/** /**
* HTTP kernel middleware that creates the session using the configured driver * 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 { export class InjectSessionHTTPModule extends HTTPKernelModule {
public readonly executeWithBlockingWriteback = true public readonly executeWithBlockingWriteback = true
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).after(SetSessionCookieHTTPModule) kernel.register(this).after(SetSessionCookieHTTPModule)
} }
public async apply(request: Request) { public async apply(request: Request): Promise<Request> {
request.registerFactory(new SessionFactory()) request.registerFactory(new SessionFactory())
const session = <Session> request.make(Session) const session = <Session> request.make(Session)

View File

@@ -1,10 +1,10 @@
import {Injectable, Inject} from "../../../di" import {Injectable, Inject} from '../../../di'
import {HTTPKernelModule} from "../HTTPKernelModule"; import {HTTPKernelModule} from '../HTTPKernelModule'
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {Routing} from "../../../service/Routing"; import {Routing} from '../../../service/Routing'
import {ActivatedRoute} from "../../routing/ActivatedRoute"; import {ActivatedRoute} from '../../routing/ActivatedRoute'
import {Logging} from "../../../service/Logging"; import {Logging} from '../../../service/Logging'
/** /**
* HTTP kernel middleware that tries to find a registered route matching the request's * HTTP kernel middleware that tries to find a registered route matching the request's
@@ -20,7 +20,7 @@ export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).before() kernel.register(this).before()
} }

View File

@@ -1,16 +1,16 @@
import {HTTPKernelModule} from "../HTTPKernelModule" import {HTTPKernelModule} from '../HTTPKernelModule'
import {HTTPKernel} from "../HTTPKernel" import {HTTPKernel} from '../HTTPKernel'
import * as Busboy from "busboy" import * as Busboy from 'busboy'
import {Request} from "../../lifecycle/Request" import {Request} from '../../lifecycle/Request'
import {infer, uuid_v4} from "../../../util" import {infer, uuid4} from '../../../util'
import {Files} from "../../../service/Files" import {Files} from '../../../service/Files'
import {Config} from "../../../service/Config" import {Config} from '../../../service/Config'
import {Logging} from "../../../service/Logging" import {Logging} from '../../../service/Logging'
import {Injectable, Inject, Container} from "../../../di" import {Injectable, Inject, Container} from '../../../di'
@Injectable() @Injectable()
export class ParseIncomingBodyHTTPModule extends HTTPKernelModule { export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
static register(kernel: HTTPKernel) { static register(kernel: HTTPKernel): void {
const files = <Files> Container.getContainer().make(Files) const files = <Files> Container.getContainer().make(Files)
const logging = <Logging> Container.getContainer().make(Logging) const logging = <Logging> Container.getContainer().make(Logging)
if ( !files.hasFilesystem() ) { if ( !files.hasFilesystem() ) {
@@ -32,8 +32,11 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
public async apply(request: Request): Promise<Request> { public async apply(request: Request): Promise<Request> {
const contentType = request.getHeader('content-type') const contentType = request.getHeader('content-type')
const contentTypes = (Array.isArray(contentType) ? contentType : [contentType]) const contentTypes = (Array.isArray(contentType) ? contentType : [contentType])
.filter(Boolean).map(x => x!.toLowerCase().split(';')[0]) .filter(Boolean).map(x => String(x).toLowerCase()
if ( !contentType ) return request .split(';')[0])
if ( !contentType ) {
return request
}
if ( if (
contentTypes.includes('multipart/form-data') contentTypes.includes('multipart/form-data')
@@ -65,7 +68,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
try { try {
const body = JSON.parse(data) const body = JSON.parse(data)
for ( const key in body ) { for ( const key in body ) {
if ( !body.hasOwnProperty(key) ) continue if ( !Object.prototype.hasOwnProperty.call(body, key) ) {
continue
}
request.parsedInput[key] = body[key] request.parsedInput[key] = body[key]
} }
res() res()
@@ -94,8 +100,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
request.parsedInput[field] = infer(val) request.parsedInput[field] = infer(val)
}) })
busboy.on('file', async (field, file, filename, encoding, mimetype) => { busboy.on('file', async (field, file, filename, encoding, mimetype) => { // eslint-disable-line max-params
if ( !this.files.hasFilesystem() ) return if ( !this.files.hasFilesystem() ) {
return
}
if ( !config?.enable ) { if ( !config?.enable ) {
this.logging.warn(`Skipping uploaded file '${filename}' because uploading is disabled. Set the server.uploads.enable config to allow uploads.`) 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 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}`) 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 file.pipe(await fs.putStoreFileAsStream({ storePath })) // FIXME might need to revisit this to ensure we don't res() before pipe finishes

View File

@@ -1,8 +1,8 @@
import {HTTPKernelModule} from "../HTTPKernelModule"; import {HTTPKernelModule} from '../HTTPKernelModule'
import {Injectable} from "../../../di" import {Injectable} from '../../../di'
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {Session} from "../../session/Session"; import {Session} from '../../session/Session'
/** /**
* HTTP kernel module that runs after the main logic in the request to persist * 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 { export class PersistSessionHTTPModule extends HTTPKernelModule {
public readonly executeWithBlockingWriteback = true public readonly executeWithBlockingWriteback = true
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).last() kernel.register(this).last()
} }

View File

@@ -1,8 +1,8 @@
import {HTTPKernelModule} from "../HTTPKernelModule"; import {HTTPKernelModule} from '../HTTPKernelModule'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {Injectable, Inject} from "../../../di" import {Injectable, Inject} from '../../../di'
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {Config} from "../../../service/Config"; import {Config} from '../../../service/Config'
/** /**
* HTTP kernel middleware that sets the `X-Powered-By` header. * HTTP kernel middleware that sets the `X-Powered-By` header.
@@ -14,11 +14,11 @@ export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
@Inject() @Inject()
protected readonly config!: Config; protected readonly config!: Config;
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).after() kernel.register(this).after()
} }
public async apply(request: Request) { public async apply(request: Request): Promise<Request> {
if ( !this.config.get('server.poweredBy.hide', false) ) { if ( !this.config.get('server.poweredBy.hide', false) ) {
request.response.setHeader('X-Powered-By', this.config.get('server.poweredBy.header', 'Extollo')) request.response.setHeader('X-Powered-By', this.config.get('server.poweredBy.header', 'Extollo'))
} }

View File

@@ -1,9 +1,9 @@
import {HTTPKernelModule} from "../HTTPKernelModule"; import {HTTPKernelModule} from '../HTTPKernelModule'
import {Injectable, Inject} from "../../../di"; import {Injectable, Inject} from '../../../di'
import {uuid_v4} from "../../../util"; import {uuid4} from '../../../util'
import {HTTPKernel} from "../HTTPKernel"; import {HTTPKernel} from '../HTTPKernel'
import {Request} from "../../lifecycle/Request"; import {Request} from '../../lifecycle/Request'
import {Logging} from "../../../service/Logging"; import {Logging} from '../../../service/Logging'
/** /**
* HTTP kernel middleware that tries to look up the session ID from the request. * HTTP kernel middleware that tries to look up the session ID from the request.
@@ -16,13 +16,13 @@ export class SetSessionCookieHTTPModule extends HTTPKernelModule {
@Inject() @Inject()
protected readonly logging!: Logging protected readonly logging!: Logging
public static register(kernel: HTTPKernel) { public static register(kernel: HTTPKernel): void {
kernel.register(this).first() kernel.register(this).first()
} }
public async apply(request: Request) { public async apply(request: Request): Promise<Request> {
if ( !request.cookies.has('extollo.session') ) { if ( !request.cookies.has('extollo.session') ) {
const session = `${uuid_v4()}-${uuid_v4()}` const session = `${uuid4()}-${uuid4()}`
this.logging.verbose(`Starting session: ${session}`) this.logging.verbose(`Starting session: ${session}`)
request.cookies.set('extollo.session', session) request.cookies.set('extollo.session', session)

View File

@@ -1,11 +1,11 @@
import {Injectable, ScopedContainer, Container} from "../../di" import {Injectable, ScopedContainer, Container} from '../../di'
import {infer, UniversalPath} from "../../util" import {infer, UniversalPath} from '../../util'
import {IncomingMessage, ServerResponse} from "http" import {IncomingMessage, ServerResponse} from 'http'
import {HTTPCookieJar} from "../kernel/HTTPCookieJar"; import {HTTPCookieJar} from '../kernel/HTTPCookieJar'
import {TLSSocket} from "tls"; import {TLSSocket} from 'tls'
import * as url from "url"; import * as url from 'url'
import {Response} from "./Response"; import {Response} from './Response'
import * as Negotiator from "negotiator"; import * as Negotiator from 'negotiator'
/** /**
* Enumeration of different HTTP verbs. * 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. * Returns true if the given item is a valid HTTP verb.
* @param what * @param what
*/ */
export function isHTTPMethod(what: any): what is HTTPMethod { export function isHTTPMethod(what: unknown): what is HTTPMethod {
return ['post', 'get', 'patch', 'put', 'delete'].includes(what) 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} = {} public readonly uploadedFiles: {[key: string]: UniversalPath} = {}
/** If true, the response lifecycle will not time out and send errors. */ /** If true, the response lifecycle will not time out and send errors. */
public bypassTimeout: boolean = false public bypassTimeout = false
constructor( constructor(
/** The native Node.js request. */ /** The native Node.js request. */
@@ -109,7 +109,7 @@ export class Request extends ScopedContainer implements DataContainer {
) { ) {
super(Container.getContainer()) super(Container.getContainer())
this.secure = !!(clientRequest.connection as TLSSocket).encrypted this.secure = Boolean((clientRequest.connection as TLSSocket).encrypted)
this.cookies = new HTTPCookieJar(this) this.cookies = new HTTPCookieJar(this)
this.url = String(clientRequest.url) this.url = String(clientRequest.url)
@@ -137,6 +137,10 @@ export class Request extends ScopedContainer implements DataContainer {
const query: {[key: string]: any} = {} const query: {[key: string]: any} = {}
for ( const key in this.rawQueryData ) { for ( const key in this.rawQueryData ) {
if ( !Object.prototype.hasOwnProperty.call(this.rawQueryData, key) ) {
continue
}
const value = this.rawQueryData[key] const value = this.rawQueryData[key]
if ( Array.isArray(value) ) { if ( Array.isArray(value) ) {
@@ -151,12 +155,11 @@ export class Request extends ScopedContainer implements DataContainer {
this.query = query this.query = query
this.isXHR = String(this.clientRequest.headers['x-requested-with']).toLowerCase() === 'xmlhttprequest' 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() as any
const {address = '0.0.0.0', family = 'IPv4', port = 0} = this.clientRequest.connection.address()
this.address = { this.address = {
address, address,
family, family,
port port,
} }
this.mediaTypes = (new Negotiator(clientRequest)).mediaTypes() 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. */ /** 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()] return this.clientRequest.headers[name.toLowerCase()]
} }
/** Get the native Node.js IncomingMessage object. */ /** Get the native Node.js IncomingMessage object. */
public toNative() { public toNative(): IncomingMessage {
return this.clientRequest 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. * Get the value of an input field on the request. Spans multiple input sources.
* @param key * @param key
*/ */
public input(key?: string) { public input(key?: string): unknown {
if ( !key ) { if ( !key ) {
return { return {
...this.parsedInput, ...this.parsedInput,
@@ -206,17 +209,21 @@ export class Request extends ScopedContainer implements DataContainer {
* Returns true if the request accepts the given media type. * Returns true if the request accepts the given media type.
* @param type - a mimetype, or the short forms json, xml, or html * @param type - a mimetype, or the short forms json, xml, or html
*/ */
accepts(type: string) { accepts(type: string): boolean {
if ( type === 'json' ) type = 'application/json' if ( type === 'json' ) {
else if ( type === 'xml' ) type = 'application/xml' type = 'application/json'
else if ( type === 'html' ) type = 'text/html' } else if ( type === 'xml' ) {
type = 'application/xml'
} else if ( type === 'html' ) {
type = 'text/html'
}
type = type.toLowerCase() type = type.toLowerCase()
const possible = [ const possible = [
type, type,
type.split('/')[0] + '/*', type.split('/')[0] + '/*',
'*/*' '*/*',
] ]
return this.mediaTypes.some(media => possible.includes(media.toLowerCase())) 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 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('*/*') const htmlIdx = this.mediaTypes.indexOf('text/html') ?? this.mediaTypes.indexOf('text/*') ?? this.mediaTypes.indexOf('*/*')
if ( htmlIdx >= 0 && htmlIdx <= jsonIdx && htmlIdx <= xmlIdx ) return 'html' if ( htmlIdx >= 0 && htmlIdx <= jsonIdx && htmlIdx <= xmlIdx ) {
if ( jsonIdx >= 0 && jsonIdx <= htmlIdx && jsonIdx <= xmlIdx ) return 'json' return 'html'
if ( xmlIdx >= 0 && xmlIdx <= jsonIdx && xmlIdx <= htmlIdx ) return 'xml' }
if ( jsonIdx >= 0 && jsonIdx <= htmlIdx && jsonIdx <= xmlIdx ) {
return 'json'
}
if ( xmlIdx >= 0 && xmlIdx <= jsonIdx && xmlIdx <= htmlIdx ) {
return 'xml'
}
return 'unknown' return 'unknown'
} }

View File

@@ -1,14 +1,14 @@
import {Request} from "./Request"; import {Request} from './Request'
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "../../util" import {ErrorWithContext, HTTPStatus, BehaviorSubject} from '../../util'
import {ServerResponse} from "http" import {ServerResponse} from 'http'
import {HTTPCookieJar} from "../kernel/HTTPCookieJar"; import {HTTPCookieJar} from '../kernel/HTTPCookieJar'
/** /**
* Error thrown when the server tries to re-send headers after they have been sent once. * Error thrown when the server tries to re-send headers after they have been sent once.
*/ */
export class HeadersAlreadySentError extends ErrorWithContext { export class HeadersAlreadySentError extends ErrorWithContext {
constructor(response: Response, headerName?: string) { 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 } 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. * Error thrown when the server tries to re-send a response that has already been sent.
*/ */
export class ResponseAlreadySentError extends ErrorWithContext { export class ResponseAlreadySentError extends ErrorWithContext {
constructor(response: Response) { constructor(public readonly response: Response) {
super(`Cannot modify or re-send response as it has already ended.`); 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[]} = {} private headers: {[key: string]: string | string[]} = {}
/** True if the headers have been sent. */ /** True if the headers have been sent. */
private _sentHeaders: boolean = false private sentHeaders = false
/** True if the response has been sent and closed. */ /** 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. */ /** 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 * If this is true, then some module in the kernel has flagged the response
@@ -44,10 +44,10 @@ export class Response {
* the response. * the response.
* @private * @private
*/ */
private _blockingWriteback: boolean = false private isBlockingWriteback = false
/** The body contents that should be written to the response. */ /** 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. * Behavior subject fired right before the response content is written.
@@ -68,14 +68,18 @@ export class Response {
) { } ) { }
/** Get the currently set response status. */ /** Get the currently set response status. */
public getStatus() { public getStatus(): HTTPStatus {
return this._status return this.status
} }
/** Set a new response status. */ /** Set a new response status. */
public setStatus(status: HTTPStatus) { public setStatus(status: HTTPStatus): this {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status') if ( this.sentHeaders ) {
this._status = status throw new HeadersAlreadySentError(this, 'status')
}
this.status = status
return this
} }
/** Get the HTTPCookieJar for the client. */ /** Get the HTTPCookieJar for the client. */
@@ -89,8 +93,11 @@ export class Response {
} }
/** Set the value of the response header. */ /** Set the value of the response header. */
public setHeader(name: string, value: string | string[]) { public setHeader(name: string, value: string | string[]): this {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name) if ( this.sentHeaders ) {
throw new HeadersAlreadySentError(this, name)
}
this.headers[name] = value this.headers[name] = value
return this return this
} }
@@ -99,9 +106,13 @@ export class Response {
* Bulk set the specified headers in the response. * Bulk set the specified headers in the response.
* @param data * @param data
*/ */
public setHeaders(data: {[name: string]: string | string[]}) { public setHeaders(data: {[name: string]: string | string[]}): this {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this) if ( this.sentHeaders ) {
this.headers = {...this.headers, ...data} throw new HeadersAlreadySentError(this)
}
this.headers = {...this.headers,
...data}
return this return this
} }
@@ -110,67 +121,88 @@ export class Response {
* @param name * @param name
* @param value * @param value
*/ */
public appendHeader(name: string, value: string | string[]) { public appendHeader(name: string, value: string | string[]): this {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name) if ( this.sentHeaders ) {
if ( !Array.isArray(value) ) value = [value] throw new HeadersAlreadySentError(this, name)
}
if ( !Array.isArray(value) ) {
value = [value]
}
let existing = this.headers[name] ?? [] let existing = this.headers[name] ?? []
if ( !Array.isArray(existing) ) existing = [existing] if ( !Array.isArray(existing) ) {
existing = [existing]
}
existing = [...existing, ...value] existing = [...existing, ...value]
if ( existing.length === 1 ) existing = existing[0] if ( existing.length === 1 ) {
existing = existing[0]
}
this.headers[name] = existing this.headers[name] = existing
return this
} }
/** /**
* Write the headers to the client. * Write the headers to the client.
*/ */
public sendHeaders() { public sendHeaders(): this {
const headers = {} as any const headers = {} as any
const setCookieHeaders = this.cookies.getSetCookieHeaders() 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 ) { 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] headers[key] = this.headers[key]
} }
this.serverResponse.writeHead(this._status, headers) this.serverResponse.writeHead(this.status, headers)
this._sentHeaders = true this.sentHeaders = true
return this
} }
/** Returns true if the headers have been sent. */ /** Returns true if the headers have been sent. */
public hasSentHeaders() { public hasSentHeaders(): boolean {
return this._sentHeaders return this.sentHeaders
} }
/** Returns true if a body has been set in the response. */ /** Returns true if a body has been set in the response. */
public hasBody() { public hasBody(): boolean {
return !!this.body return Boolean(this.body)
} }
/** /**
* Get or set the flag for whether the writeback should be blocked. * Get or set the flag for whether the writeback should be blocked.
* @param set - if this is specified, the value will be set. * @param set - if this is specified, the value will be set.
*/ */
public blockingWriteback(set?: boolean) { public blockingWriteback(set?: boolean): boolean {
if ( typeof set !== 'undefined' ) { 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. * Write the headers and specified data to the client.
* @param data * @param data
*/ */
public async write(data: any) { public async write(data: unknown): Promise<void> {
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
if ( !this._sentHeaders ) this.sendHeaders() if ( !this.sentHeaders ) {
this.sendHeaders()
}
this.serverResponse.write(data, error => { this.serverResponse.write(data, error => {
if ( error ) rej(error) if ( error ) {
else res() rej(error)
} else {
res()
}
}) })
}) })
} }
@@ -178,7 +210,7 @@ export class Response {
/** /**
* Send the response to the client, writing the headers and configured body. * 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) await this.sending$.next(this)
this.setHeader('Content-Length', String(this.body?.length ?? 0)) this.setHeader('Content-Length', String(this.body?.length ?? 0))
await this.write(this.body ?? '') await this.write(this.body ?? '')
@@ -189,10 +221,14 @@ export class Response {
/** /**
* Mark the response as ended and close the socket. * Mark the response as ended and close the socket.
*/ */
public end() { public end(): this {
if ( this._responseEnded ) throw new ResponseAlreadySentError(this) if ( this.responseEnded ) {
this._sentHeaders = true throw new ResponseAlreadySentError(this)
}
this.sentHeaders = true
this.serverResponse.end() this.serverResponse.end()
return this
} }
// location? // location?

View File

@@ -1,6 +1,6 @@
import {ResponseFactory} from "./ResponseFactory" import {ResponseFactory} from './ResponseFactory'
import {Rehydratable} from "../../util" import {Rehydratable} from '../../util'
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
/** /**
* Helper function that creates a DehydratedStateResponseFactory. * Helper function that creates a DehydratedStateResponseFactory.
@@ -15,10 +15,12 @@ export function dehydrate(value: Rehydratable): DehydratedStateResponseFactory {
*/ */
export class DehydratedStateResponseFactory extends ResponseFactory { export class DehydratedStateResponseFactory extends ResponseFactory {
constructor( constructor(
public readonly rehydratable: Rehydratable public readonly rehydratable: Rehydratable,
) { super() } ) {
super()
}
public async write(request: Request) { public async write(request: Request): Promise<Request> {
request = await super.write(request) request = await super.write(request)
request.response.body = JSON.stringify(this.rehydratable.dehydrate()) request.response.body = JSON.stringify(this.rehydratable.dehydrate())
request.response.setHeader('Content-Type', 'application/json') request.response.setHeader('Content-Type', 'application/json')

View File

@@ -1,21 +1,23 @@
import {ResponseFactory} from "./ResponseFactory" import {ResponseFactory} from './ResponseFactory'
import {ErrorWithContext, HTTPStatus} from "../../util" import {ErrorWithContext, HTTPStatus} from '../../util'
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
import * as api from "./api" import * as api from './api'
/** /**
* Helper to create a new ErrorResponseFactory, with the given HTTP status and output format. * Helper to create a new ErrorResponseFactory, with the given HTTP status and output format.
* @param error * @param thrownError
* @param status * @param status
* @param output * @param output
*/ */
export function error( export function error(
error: Error | string, thrownError: Error | string,
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR, status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
output: 'json' | 'html' | 'auto' = 'auto' output: 'json' | 'html' | 'auto' = 'auto',
): ErrorResponseFactory { ): ErrorResponseFactory {
if ( typeof error === 'string' ) error = new Error(error) if ( typeof thrownError === 'string' ) {
return new ErrorResponseFactory(error, status, output) 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' protected targetMode: 'json' | 'html' | 'auto' = 'auto'
constructor( constructor(
public readonly error: Error, public readonly thrownError: Error,
status: HTTPStatus, status: HTTPStatus,
output: 'json' | 'html' | 'auto' = 'auto' output: 'json' | 'html' | 'auto' = 'auto',
) { ) {
super() super()
this.status(status) this.status(status)
@@ -39,16 +41,16 @@ export class ErrorResponseFactory extends ResponseFactory {
return this return this
} }
public async write(request: Request) { public async write(request: Request): Promise<Request> {
request = await super.write(request) request = await super.write(request)
const wants = request.wants() const wants = request.wants()
if ( this.targetMode === 'json' || (this.targetMode === 'auto' && wants === 'json') ) { if ( this.targetMode === 'json' || (this.targetMode === 'auto' && wants === 'json') ) {
request.response.setHeader('Content-Type', 'application/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')) ) { } else if ( this.targetMode === 'html' || (this.targetMode === 'auto' && (wants === 'html' || wants === 'unknown')) ) {
request.response.setHeader('Content-Type', 'text/html') request.response.setHeader('Content-Type', 'text/html')
request.response.body = this.buildHTML(this.error) request.response.body = this.buildHTML(this.thrownError)
} }
// FIXME XML support // FIXME XML support
@@ -61,12 +63,12 @@ export class ErrorResponseFactory extends ResponseFactory {
* @param {Error} error * @param {Error} error
* @return string * @return string
*/ */
protected buildHTML(error: Error) { protected buildHTML(thrownError: Error): string {
let context: any let context: any
if ( error instanceof ErrorWithContext ) { if ( thrownError instanceof ErrorWithContext ) {
context = error.context context = thrownError.context
if ( error.originalError ) { if ( thrownError.originalError ) {
error = error.originalError thrownError = thrownError.originalError
} }
} }
@@ -74,10 +76,11 @@ export class ErrorResponseFactory extends ResponseFactory {
<b>Sorry, an unexpected error occurred while processing your request.</b> <b>Sorry, an unexpected error occurred while processing your request.</b>
<br> <br>
<pre><code> <pre><code>
Name: ${error.name} Name: ${thrownError.name}
Message: ${error.message} Message: ${thrownError.message}
Stack trace: 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> </code></pre>
` `
@@ -85,7 +88,8 @@ Stack trace:
str += ` str += `
<pre><code> <pre><code>
Context: Context:
${Object.keys(context).map(key => ` - ${key} : ${context[key]}`).join('\n')} ${Object.keys(context).map(key => ` - ${key} : ${context[key]}`)
.join('\n')}
</code></pre> </code></pre>
` `
} }
@@ -93,7 +97,7 @@ ${Object.keys(context).map(key => ` - ${key} : ${context[key]}`).join('\n')}
return str return str
} }
protected buildJSON(error: Error) { protected buildJSON(thrownError: Error): string {
return JSON.stringify(api.error(error)) return JSON.stringify(api.error(thrownError))
} }
} }

View File

@@ -1,5 +1,5 @@
import {ResponseFactory} from "./ResponseFactory"; import {ResponseFactory} from './ResponseFactory'
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
/** /**
* Helper function that creates a new HTMLResponseFactory. * Helper function that creates a new HTMLResponseFactory.
@@ -15,9 +15,11 @@ export function html(value: string): HTMLResponseFactory {
export class HTMLResponseFactory extends ResponseFactory { export class HTMLResponseFactory extends ResponseFactory {
constructor( constructor(
public readonly value: string, public readonly value: string,
) { super() } ) {
super()
}
public async write(request: Request) { public async write(request: Request): Promise<Request> {
request = await super.write(request) request = await super.write(request)
request.response.setHeader('Content-Type', 'text/html; charset=utf-8') request.response.setHeader('Content-Type', 'text/html; charset=utf-8')
request.response.body = this.value request.response.body = this.value

View File

@@ -1,6 +1,6 @@
import {ErrorResponseFactory} from "./ErrorResponseFactory"; import {ErrorResponseFactory} from './ErrorResponseFactory'
import {HTTPError} from "../HTTPError"; import {HTTPError} from '../HTTPError'
import {HTTPStatus} from "../../util" import {HTTPStatus} from '../../util'
/** /**
* Helper that generates a new HTTPErrorResponseFactory given the HTTP status and message. * Helper that generates a new HTTPErrorResponseFactory given the HTTP status and message.

View File

@@ -1,11 +1,11 @@
import {ResponseFactory} from "./ResponseFactory"; import {ResponseFactory} from './ResponseFactory'
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
/** /**
* Helper function to create a new JSONResponseFactory of the given value. * Helper function to create a new JSONResponseFactory of the given value.
* @param value * @param value
*/ */
export function json(value: any): JSONResponseFactory { export function json(value: unknown): JSONResponseFactory {
return new JSONResponseFactory(value) return new JSONResponseFactory(value)
} }
@@ -14,10 +14,12 @@ export function json(value: any): JSONResponseFactory {
*/ */
export class JSONResponseFactory extends ResponseFactory { export class JSONResponseFactory extends ResponseFactory {
constructor( constructor(
public readonly value: any public readonly value: unknown,
) { super() } ) {
super()
}
public async write(request: Request) { public async write(request: Request): Promise<Request> {
request = await super.write(request) request = await super.write(request)
request.response.setHeader('Content-Type', 'application/json') request.response.setHeader('Content-Type', 'application/json')
request.response.body = JSON.stringify(this.value) request.response.body = JSON.stringify(this.value)

View File

@@ -1,5 +1,5 @@
import {HTTPStatus} from "../../util" import {HTTPStatus} from '../../util'
import {Request} from "../lifecycle/Request" import {Request} from '../lifecycle/Request'
/** /**
* Abstract class that defines "factory" that knows how to write a particular * 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. */ /** Set the target status of this factory. */
public status(status: HTTPStatus) { public status(status: HTTPStatus): this {
this.targetStatus = status this.targetStatus = status
return this return this
} }

View File

@@ -1,5 +1,5 @@
import {ResponseFactory} from "./ResponseFactory"; import {ResponseFactory} from './ResponseFactory'
import {Request} from "../lifecycle/Request"; import {Request} from '../lifecycle/Request'
/** /**
* Helper function that creates a new StringResponseFactory for the given string value. * Helper function that creates a new StringResponseFactory for the given string value.
@@ -16,9 +16,11 @@ export class StringResponseFactory extends ResponseFactory {
constructor( constructor(
/** The string to write as the body. */ /** The string to write as the body. */
public readonly value: string, public readonly value: string,
) { super() } ) {
super()
}
public async write(request: Request) { public async write(request: Request): Promise<Request> {
request = await super.write(request) request = await super.write(request)
request.response.setHeader('Content-Type', 'text/plain') request.response.setHeader('Content-Type', 'text/plain')
request.response.body = this.value request.response.body = this.value

Some files were not shown because too many files have changed in this diff Show More