Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0b86d796e8
|
|||
|
1d5056b753
|
|||
|
82e7a1f299
|
|||
|
4849016784
|
|||
|
0dde436b4c
|
|||
|
4d39637f30
|
|||
|
9be9c44a32
|
|||
|
26d54033af
|
|||
|
574ddbe9cb
|
83
.drone.yml
@@ -1,3 +1,86 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: docs
|
||||||
|
steps:
|
||||||
|
# ============ BUILD STEPS ===============
|
||||||
|
- name: build documentation
|
||||||
|
image: glmdev/node-pnpm:latest
|
||||||
|
commands:
|
||||||
|
- pnpm i --silent
|
||||||
|
- pnpm docs:build
|
||||||
|
- cd docs && tar czf ../extollo_api_documentation.tar.gz www
|
||||||
|
|
||||||
|
# =============== DEPLOY STEPS ===============
|
||||||
|
- name: copy artifacts to static host
|
||||||
|
image: appleboy/drone-scp
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
from_secret: docs_deploy_host
|
||||||
|
username:
|
||||||
|
from_secret: docs_deploy_user
|
||||||
|
key:
|
||||||
|
from_secret: docs_deploy_key
|
||||||
|
port: 22
|
||||||
|
source: extollo_api_documentation.tar.gz
|
||||||
|
target: /var/nfs/general/static/sites/extollo
|
||||||
|
when:
|
||||||
|
event: promote
|
||||||
|
target: docs
|
||||||
|
|
||||||
|
- name: deploy artifacts on static host
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
from_secret: docs_deploy_host
|
||||||
|
username:
|
||||||
|
from_secret: docs_deploy_user
|
||||||
|
key:
|
||||||
|
from_secret: docs_deploy_key
|
||||||
|
port: 22
|
||||||
|
script:
|
||||||
|
- cd /var/nfs/general/static/sites/extollo
|
||||||
|
- rm -rf docs
|
||||||
|
- tar xzf extollo_api_documentation.tar.gz
|
||||||
|
- rm -rf extollo_api_documentation.tar.gz
|
||||||
|
- mv www docs
|
||||||
|
when:
|
||||||
|
event: promote
|
||||||
|
target: docs
|
||||||
|
|
||||||
|
# =============== BUILD NOTIFICATIONS ===============
|
||||||
|
- name: send build success notifications
|
||||||
|
image: plugins/webhook
|
||||||
|
settings:
|
||||||
|
urls:
|
||||||
|
from_secret: notify_webhook_url
|
||||||
|
content_type: application/json
|
||||||
|
template: |
|
||||||
|
{
|
||||||
|
"title": "Drone-CI [extollo/docs @ ${DRONE_BUILD_NUMBER}]",
|
||||||
|
"message": "Build & deploy completed successfully.",
|
||||||
|
"priority": 4
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
status: success
|
||||||
|
event:
|
||||||
|
- promote
|
||||||
|
|
||||||
|
- name: send build error notifications
|
||||||
|
image: plugins/webhook
|
||||||
|
settings:
|
||||||
|
urls:
|
||||||
|
from_secret: notify_webhook_url
|
||||||
|
content_type: application/json
|
||||||
|
template: |
|
||||||
|
{
|
||||||
|
"title": "Drone-CI [extollo/docs @ ${DRONE_BUILD_NUMBER}]",
|
||||||
|
"message": "Documentation build failed!",
|
||||||
|
"priority": 6
|
||||||
|
}
|
||||||
|
when:
|
||||||
|
status: failure
|
||||||
|
---
|
||||||
|
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: default
|
name: default
|
||||||
type: docker
|
type: docker
|
||||||
|
|||||||
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
lib
|
||||||
|
dist
|
||||||
113
.eslintrc.json
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"allowTemplateLiterals": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"no-console": "error",
|
||||||
|
"curly": "error",
|
||||||
|
"eqeqeq": "error",
|
||||||
|
"guard-for-in": "error",
|
||||||
|
"no-alert": "error",
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-constructor-return": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-implicit-coercion": "error",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"no-invalid-this": "error",
|
||||||
|
"no-return-await": "error",
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
"no-useless-call": "error",
|
||||||
|
"radix": "error",
|
||||||
|
"yoda": "error",
|
||||||
|
"@typescript-eslint/no-shadow": "error",
|
||||||
|
"brace-style": "error",
|
||||||
|
"camelcase": "error",
|
||||||
|
"comma-dangle": [
|
||||||
|
"error",
|
||||||
|
"always-multiline"
|
||||||
|
],
|
||||||
|
"comma-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"before": false,
|
||||||
|
"after": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"comma-style": [
|
||||||
|
"error",
|
||||||
|
"last"
|
||||||
|
],
|
||||||
|
"computed-property-spacing": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"eol-last": "error",
|
||||||
|
"func-call-spacing": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"keyword-spacing": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"before": true,
|
||||||
|
"after": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lines-between-class-members": "error",
|
||||||
|
"max-params": [
|
||||||
|
"error",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"new-parens": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"newline-per-chained-call": "error",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"no-underscore-dangle": "error",
|
||||||
|
"no-unneeded-ternary": "error",
|
||||||
|
"no-whitespace-before-property": "error",
|
||||||
|
"object-property-newline": "error",
|
||||||
|
"prefer-exponentiation-operator": "error",
|
||||||
|
"prefer-object-spread": "error",
|
||||||
|
"spaced-comment": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"prefer-const": "error",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
www*
|
||||||
31
docs/HOME.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<center>
|
||||||
|
<br>
|
||||||
|
<img alt="The Extollo logo" src="https://static.garrettmills.dev/sites/extollo/docs/assets/logo/svg/Extollo-Icon-and-Text-LIGHT-Final.svg" height="150">
|
||||||
|
<br><br>
|
||||||
|
<b>extollo</b> - (v. <em>latin</em>) - to lift up, to elevate
|
||||||
|
<br><br>
|
||||||
|
Extollo is a <a href="https://www.gnu.org/philosophy/floss-and-foss.en.html" target="_blank">free & libre</a> application framework in TypeScript.
|
||||||
|
</center>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
Built on principles of modularity, strict-typing, inversion-of-control, and developer ergonomics, Extollo enables developers to build maintainable, scalable, and expressive applications.
|
||||||
|
|
||||||
|
Node.js provides an excellent platform for quickly getting an application up and running, but this loose minimalism can lead to larger, more unweildy code-bases as your application grows. Extollo fixes this by providing an opinionated, robust framework and first-party modules that provide, among other things:
|
||||||
|
|
||||||
|
- Type-based dependency injection
|
||||||
|
- Strongly-typed ORM with an expressive query-builder and models
|
||||||
|
- Customizable session & caching interfaces
|
||||||
|
- Modular, pre-compiled, nest-able routes
|
||||||
|
- First-party, extensible command line tools
|
||||||
|
- Unit-based application structure
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
Writing an application with Extollo is very straightforward if you are familiar with Node.js/TypeScript, or similar frameworks like Laravel.
|
||||||
|
|
||||||
|
Check out the [Getting Started](https://extollo.garrettmills.dev/pages/Documentation/Getting-Started.html) page site for more information.
|
||||||
|
|
||||||
|
## License & Philosophy
|
||||||
|
The Extollo project is, and will always be, free & libre software. The framework itself is open-source available [here](https://code.garrettmills.dev/Extollo), and is licensed under the terms of the MIT license. See the LICENSE file for more information.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Have an improvement or fix to Extollo? Contributors are always welcome. See the CONTRIBUTING.md file for next steps.
|
||||||
1
docs/pages/About-Extollo.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# About the Extollo Project
|
||||||
7
docs/pages/Getting-Started.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Getting Started with Extollo
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js v14 or later
|
||||||
|
- [PNPM](https://pnpm.js.org/) (not NPM/Yarn)
|
||||||
|
- Postgres credentials (if you want to use [@extollo/orm](../modules/orm_src.html))
|
||||||
6
docs/sourcefile-map.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"pattern": "^",
|
||||||
|
"replace": "https://code.garrettmills.dev/extollo/lib/src/branch/master/"
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
docs/static/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 206 KiB |
19
docs/static/humans.txt
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* PROJECT */
|
||||||
|
|
||||||
|
Site Name: The Extollo Framework
|
||||||
|
Site URL: https://extollo.garrettmills.dev/
|
||||||
|
Created: 2021/03/24
|
||||||
|
Standards: HTML5, CSS3
|
||||||
|
Software: TypeDoc
|
||||||
|
|
||||||
|
/* AUTHOR */
|
||||||
|
|
||||||
|
Name: Garrett Mills
|
||||||
|
Location: Lawrence, Kansas
|
||||||
|
Site: https://garrettmills.dev/
|
||||||
|
Blog: https://garrettmills.dev/blog/
|
||||||
|
Contact: https://garrettmills.dev/#contact
|
||||||
|
|
||||||
|
/* THANKS */
|
||||||
|
|
||||||
|
To Piper Mills for the excellent font, color, and logo design.
|
||||||
2727
docs/theme/assets/css/main.css
vendored
Normal file
64
docs/theme/assets/css/pages.css
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
h2 code {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 code {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation.primary ul {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation.primary li {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.label.pp-nav.pp-group:first-child span {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.label.pp-nav.pp-group {
|
||||||
|
font-weight: 700;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.label.pp-nav.pp-group span {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.pp-nav.pp-page.current {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-left: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.pp-nav.pp-page.current a {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.pp-nav.pp-page.pp-parent.pp-active {
|
||||||
|
border-left: 2px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.pp-nav.pp-page.pp-child {
|
||||||
|
border-left: 2px solid #eee;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-navigation li.pp-nav.pp-page.pp-child.current {
|
||||||
|
border-left: 2px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tsd-kind-page .tsd-kind-icon:before {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
content: "";
|
||||||
|
background-image: url("../images/page-icon.svg");
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tsd-search .results span.parent {
|
||||||
|
color: #b3b2b2 !important;
|
||||||
|
}
|
||||||
BIN
docs/theme/assets/font/Extatica-Bold.otf
vendored
Normal file
BIN
docs/theme/assets/font/Extatica-Regular.otf
vendored
Normal file
BIN
docs/theme/assets/images/icons.png
vendored
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
docs/theme/assets/images/icons@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
1
docs/theme/assets/images/page-icon.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#AA43FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||||
|
After Width: | Height: | Size: 468 B |
BIN
docs/theme/assets/images/widgets.png
vendored
Normal file
|
After Width: | Height: | Size: 480 B |
BIN
docs/theme/assets/images/widgets@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 855 B |
1
docs/theme/assets/js/main.js
vendored
Normal file
BIN
docs/theme/assets/logo/png/Extollo-Icon-NO-TEXT-light-and-dark-Final.png
vendored
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Icon-and-Text-DARK-Final.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Icon-and-Text-LIGHT-Final.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Text-NO-ICON-Dark-final.png
vendored
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
docs/theme/assets/logo/png/Extollo-Text-NO-ICON-Light-final.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
1
docs/theme/assets/logo/svg/Extollo-Icon-NO-TEXT-light-and-dark-Final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 336.67 432"><defs><style>.cls-1{fill:#49686a;}.cls-2{fill:#f2c373;}.cls-3{fill:#2e5252;}.cls-4{fill:#ffe293;}</style></defs><polygon class="cls-1" points="39.28 359.8 202.72 4.89 336.67 303.76 280.63 303.76 194.94 129.42 98.43 359.8 39.28 359.8"/><polygon class="cls-2" points="335.04 310.28 308.57 366.31 252.54 366.31 279 310.28 335.04 310.28"/><polygon class="cls-3" points="246.02 363.06 272.49 307.02 194.94 145.7 165.56 215.83 246.02 363.06"/><polygon class="cls-3" points="194.58 0 140.1 0 0 298.88 31.13 354.92 194.58 0"/><ellipse class="cls-4" cx="170.63" cy="420.6" rx="122.95" ry="11.4"/></svg>
|
||||||
|
After Width: | Height: | Size: 691 B |
1
docs/theme/assets/logo/svg/Extollo-Icon-and-Text-DARK-Final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 646.49 291.04"><defs><style>.cls-1{fill:#49686a;}.cls-2{fill:#f2c373;}.cls-3{fill:#2e5252;}.cls-4{fill:#ffe293;}</style></defs><polygon class="cls-1" points="26.46 242.4 136.57 3.29 226.81 204.64 189.06 204.64 131.33 87.19 66.31 242.4 26.46 242.4"/><polygon class="cls-2" points="225.72 209.03 207.89 246.79 170.13 246.79 187.96 209.03 225.72 209.03"/><polygon class="cls-3" points="165.75 244.59 183.57 206.84 131.33 98.16 111.54 145.41 165.75 244.59"/><polygon class="cls-3" points="131.09 0 94.38 0 0 201.35 20.97 239.11 131.09 0"/><ellipse class="cls-4" cx="114.95" cy="283.36" rx="82.83" ry="7.68"/><path class="cls-2" d="M290.79,131.24H344v14.32h-35.6v27.38h32.06V187.4H308.37v28.79H344v14.33H290.79Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M353.19,220.59l15.46-27-13.9-24.11V159.6h13.33l14.75,27.66,14.75-27.66h13.33v9.93L397,193.64l15.46,27v9.93H399.14L382.83,200l-16.31,30.49H353.19Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M426.93,172.94h-9.5V159.6h9.5V144h19.15L438,159.6h20v13.34H443.39V211.8a5.11,5.11,0,0,0,5.39,5.39H458v13.33H445.66q-8.37,0-13.55-5.18t-5.18-13.54Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M475,167.05q8.65-8.73,23-8.72,14.17,0,22.83,8.72T529.48,190v10.21q0,14.18-8.66,22.9T498,231.79q-14.33,0-23-8.72t-8.65-22.9V190Q466.36,175.77,475,167.05Zm12,46.24a14.85,14.85,0,0,0,11,4.18q6.81,0,10.92-4.18A14.83,14.83,0,0,0,513,202.44V187.69q0-6.81-4.11-10.93T498,172.65a15,15,0,0,0-11,4.11q-4.19,4.13-4.19,10.93v14.75A14.69,14.69,0,0,0,487,213.29Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M541.53,127.69H558V230.52H541.53Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M573,127.69h16.45V230.52H573Z" transform="translate(-18 -34.48)"/><path class="cls-2" d="M610,167.05q8.64-8.73,23-8.72t22.84,8.72q8.65,8.72,8.65,22.91v10.21q0,14.18-8.65,22.9T633,231.79q-14.32,0-23-8.72t-8.65-22.9V190Q601.38,175.77,610,167.05Zm12,46.24a14.85,14.85,0,0,0,11,4.18q6.81,0,10.92-4.18A14.8,14.8,0,0,0,648,202.44V187.69q0-6.81-4.12-10.93T633,172.65a15,15,0,0,0-11,4.11q-4.18,4.13-4.18,10.93v14.75A14.68,14.68,0,0,0,622,213.29Z" transform="translate(-18 -34.48)"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
1
docs/theme/assets/logo/svg/Extollo-Icon-and-Text-LIGHT-Final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 648 290.82"><defs><style>.cls-1{fill:#49686a;}.cls-2{fill:#f2c373;}.cls-3{fill:#2e5252;}.cls-4{fill:#ffe293;}</style></defs><polygon class="cls-1" points="26.44 242.22 136.47 3.29 226.64 204.49 188.92 204.49 131.23 87.12 66.26 242.22 26.44 242.22"/><polygon class="cls-2" points="225.54 208.88 207.73 246.6 170 246.6 187.82 208.88 225.54 208.88"/><polygon class="cls-3" points="165.62 244.41 183.43 206.68 131.23 98.09 111.45 145.3 165.62 244.41"/><polygon class="cls-3" points="130.99 0 94.31 0 0 201.2 20.96 238.93 130.99 0"/><ellipse class="cls-4" cx="114.87" cy="283.14" rx="82.77" ry="7.67"/><path class="cls-3" d="M292.58,131.27h53.14v14.32H310.15v27.35h32V187.4h-32v28.77h35.57v14.31H292.58Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M354.93,220.56l15.45-26.93-13.89-24.09v-9.92h13.32l14.74,27.64,14.74-27.64h13.32v9.92l-13.89,24.09,15.45,26.93v9.92H400.85L384.55,200l-16.3,30.47H354.93Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M428.62,172.94h-9.49V159.62h9.49V144h19.14l-8.08,15.59h20v13.32h-14.6v38.83a5.11,5.11,0,0,0,5.39,5.39h9.21v13.32H447.33q-8.35,0-13.53-5.17t-5.18-13.54Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M476.67,167.06q8.64-8.72,22.95-8.72t22.82,8.72q8.65,8.72,8.65,22.89v10.2q0,14.17-8.65,22.89t-22.82,8.72q-14.31,0-22.95-8.72T468,200.15V190Q468,175.78,476.67,167.06Zm12,46.2a14.84,14.84,0,0,0,11,4.18q6.81,0,10.92-4.18a14.81,14.81,0,0,0,4.11-10.84V187.68q0-6.8-4.11-10.91t-10.92-4.11a15,15,0,0,0-11,4.11q-4.18,4.11-4.18,10.91v14.74A14.66,14.66,0,0,0,488.64,213.26Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M543.13,127.73h16.44V230.48H543.13Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M574.59,127.73H591V230.48H574.59Z" transform="translate(-18 -34.59)"/><path class="cls-3" d="M611.58,167.06q8.64-8.72,23-8.72t22.81,8.72Q666,175.78,666,190v10.2q0,14.17-8.65,22.89t-22.81,8.72q-14.32,0-23-8.72t-8.65-22.89V190Q602.93,175.78,611.58,167.06Zm12,46.2a14.85,14.85,0,0,0,11,4.18q6.8,0,10.91-4.18a14.81,14.81,0,0,0,4.11-10.84V187.68q0-6.8-4.11-10.91t-10.91-4.11a15,15,0,0,0-11,4.11q-4.17,4.11-4.18,10.91v14.74A14.7,14.7,0,0,0,623.55,213.26Z" transform="translate(-18 -34.59)"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
1
docs/theme/assets/logo/svg/Extollo-Text-NO-ICON-Dark-final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 432 162.99"><defs><style>.cls-1{fill:#49686a;opacity:0.75;}.cls-2{fill:#f2c373;}</style></defs><ellipse class="cls-1" cx="214.64" cy="153.99" rx="180" ry="9"/><path class="cls-2" d="M5,9H66.44V25.53H25.29V57.17H62.34V73.89H25.29v33.29H66.44v16.56H5Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M77.09,112.26,95,81.11,78.9,53.23V41.76H94.31l17,32,17-32h15.41V53.23L127.75,81.11l17.88,31.15v11.48H130.21L111.36,88.49,92.5,123.74H77.09Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M162.34,57.17h-11V41.76h11v-18h22.14l-9.35,18h23.12V57.17H181.36v44.92a5.9,5.9,0,0,0,6.23,6.23h10.66v15.42H184q-9.68,0-15.66-6t-6-15.66Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M217.92,50.36q10-10.08,26.56-10.08,16.4,0,26.4,10.08t10,26.48V88.65q0,16.39-10,26.48t-26.4,10.08q-16.56,0-26.56-10.08t-10-26.48V76.84Q207.92,60.44,217.92,50.36Zm13.86,53.45q4.83,4.84,12.7,4.84t12.63-4.84q4.75-4.83,4.75-12.54v-17q0-7.87-4.75-12.62t-12.63-4.76q-7.86,0-12.7,4.76t-4.84,12.62v17A16.93,16.93,0,0,0,231.78,103.81Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M294.81,4.87h19V123.74h-19Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M331.21,4.87h19V123.74h-19Z" transform="translate(-4.96 -4.87)"/><path class="cls-2" d="M374,50.36q10-10.08,26.56-10.08,16.4,0,26.39,10.08t10,26.48V88.65q0,16.39-10,26.48t-26.39,10.08q-16.56,0-26.56-10.08T364,88.65V76.84Q364,60.44,374,50.36Zm13.85,53.45q4.84,4.84,12.71,4.84t12.62-4.84q4.76-4.83,4.76-12.54v-17q0-7.87-4.76-12.62t-12.62-4.76q-7.87,0-12.71,4.76T383,74.22v17A17,17,0,0,0,387.85,103.81Z" transform="translate(-4.96 -4.87)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
1
docs/theme/assets/logo/svg/Extollo-Text-NO-ICON-Light-final.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 432 162.99"><defs><style>.cls-1{fill:#ffe293;}.cls-2{fill:#2e5252;}</style></defs><ellipse class="cls-1" cx="213.64" cy="153.99" rx="180" ry="9"/><path class="cls-2" d="M3.6,9H65.08V25.56H23.93V57.21H61V73.93h-37v33.28H65.08v16.56H3.6Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M75.73,112.3,93.61,81.14,77.54,53.27V41.79H93l17,32,17-32h15.42V53.27L126.4,81.14l17.87,31.16v11.47H128.86L110,88.52,91.15,123.77H75.73Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M161,57.21H150V41.79h11v-18h22.13l-9.34,18h23.11V57.21H180v44.92a5.9,5.9,0,0,0,6.23,6.23h10.65v15.41H182.63q-9.67,0-15.66-6t-6-15.66Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M216.57,50.4q10-10.08,26.56-10.08,16.39,0,26.39,10.08t10,26.48V88.69q0,16.4-10,26.47t-26.39,10.09q-16.56,0-26.56-10.09t-10-26.47V76.88Q206.56,60.49,216.57,50.4Zm13.85,53.45q4.83,4.84,12.71,4.84t12.62-4.84q4.75-4.83,4.75-12.54v-17q0-7.87-4.75-12.63t-12.62-4.75q-7.87,0-12.71,4.75t-4.84,12.63v17A16.93,16.93,0,0,0,230.42,103.85Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M293.45,4.9h19V123.77h-19Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M329.85,4.9h19V123.77h-19Z" transform="translate(-3.6 -4.9)"/><path class="cls-2" d="M372.64,50.4q10-10.08,26.56-10.08,16.4,0,26.4,10.08t10,26.48V88.69q0,16.4-10,26.47t-26.4,10.09q-16.56,0-26.56-10.09t-10-26.47V76.88Q362.64,60.49,372.64,50.4Zm13.86,53.45q4.83,4.84,12.7,4.84t12.63-4.84q4.76-4.83,4.75-12.54v-17q0-7.87-4.75-12.63T399.2,56.88q-7.87,0-12.7,4.75t-4.84,12.63v17A16.93,16.93,0,0,0,386.5,103.85Z" transform="translate(-3.6 -4.9)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
51
docs/theme/layouts/default.hbs
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html class="default no-js">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>{{#ifCond model.name '==' project.name}}{{project.name}}{{else}}{{model.name}} | {{project.name}}{{/ifCond}}</title>
|
||||||
|
<meta name="description" content="Documentation for {{project.name}}">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{relativeURL "assets/css/main.css"}}">
|
||||||
|
<link rel="author" href="{{relativeURL "humans.txt"}}">
|
||||||
|
<script async src="{{relativeURL "assets/js/search.js"}}" id="search-script"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{> header}}
|
||||||
|
|
||||||
|
<div class="container container-main">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-8 col-content">
|
||||||
|
{{{contents}}}
|
||||||
|
</div>
|
||||||
|
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
|
||||||
|
<nav class="tsd-navigation primary">
|
||||||
|
<ul>
|
||||||
|
{{#each navigation.children}}
|
||||||
|
{{> navigation}}
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<nav class="tsd-navigation secondary menu-sticky">
|
||||||
|
<ul class="before-current">
|
||||||
|
{{#each toc.children}}
|
||||||
|
{{> toc.root}}
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{> footer}}
|
||||||
|
|
||||||
|
<div class="overlay"></div>
|
||||||
|
<script src="{{relativeURL "assets/js/main.js"}}"></script>
|
||||||
|
|
||||||
|
{{> analytics}}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
docs/theme/partials/footer.hbs
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Legend</h2>
|
||||||
|
<div class="tsd-legend-group">
|
||||||
|
{{#each legend}}
|
||||||
|
<ul class="tsd-legend">
|
||||||
|
{{#each .}}
|
||||||
|
<li class="{{#compact}}{{#each classes}} {{.}}{{/each}}{{/compact}}"><span class="tsd-kind-icon">{{name}}</span></li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{{#unless settings.hideGenerator}}
|
||||||
|
<div class="tsd-generator extollo-end-cap">
|
||||||
|
<img src="{{relativeURL "assets/logo/svg/Extollo-Icon-and-Text-DARK-Final.svg"}}" style="max-height: 100px" class="svg-filter-white" alt="Extollo Logo">
|
||||||
|
<p><b>extollo</b> (v. <em>latin</em>) - to lift up, to elevate</p>
|
||||||
|
<p>
|
||||||
|
Extollo is a <a href="https://www.gnu.org/philosophy/floss-and-foss.en.html" target="_blank">free & libre</a> application framework in TypeScript.
|
||||||
|
</p>
|
||||||
|
<p class="list-of-links">
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{relativeURL "/"}}">Home</a></li>
|
||||||
|
<li><a href="{{relativeURL "pages/Documentation/Getting-Started.html"}}">Getting Started</a></li>
|
||||||
|
<li><a href="{{relativeURL "pages/Documentation/About-Extollo.html"}}">About Extollo</a></li>
|
||||||
|
<li><a href="https://code.garrettmills.dev/Extollo" target="_blank">Source Code</a></li>
|
||||||
|
<li><a href="https://code.garrettmills.dev/extollo/extollo/src/branch/master/CONTRIBUTING.md" target="_blank">Contributing</a></li>
|
||||||
|
<li><a href="https://code.garrettmills.dev/Extollo/docs" target="_blank">Build These Docs</a></li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
71
docs/theme/partials/header.hbs
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<header>
|
||||||
|
<div class="tsd-page-toolbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="table-wrap">
|
||||||
|
<div class="table-cell" id="tsd-search" data-index="{{relativeURL "assets/js/search.json"}}" data-base="{{relativeURL "./"}}">
|
||||||
|
<div class="field">
|
||||||
|
<label for="tsd-search-field" class="tsd-widget search no-caption">Search</label>
|
||||||
|
<input id="tsd-search-field" type="text" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="results">
|
||||||
|
<li class="state loading">Preparing search index...</li>
|
||||||
|
<li class="state failure">The search index is not available</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<img src="{{relativeURL "assets/logo/svg/Extollo-Icon-NO-TEXT-light-and-dark-Final.svg"}}" alt="Extollo Icon" class="token-logo" style="max-height: 30px; margin-bottom: -10px; padding-right: 10px;">
|
||||||
|
<a href="{{relativeURL "index.html"}}" class="title">{{project.name}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-cell" id="tsd-widgets">
|
||||||
|
<div id="tsd-filter">
|
||||||
|
<a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a>
|
||||||
|
<div class="tsd-filter-group">
|
||||||
|
<div class="tsd-select" id="tsd-filter-visibility">
|
||||||
|
<span class="tsd-select-label">All</span>
|
||||||
|
<ul class="tsd-select-list">
|
||||||
|
<li data-value="public">Public</li>
|
||||||
|
<li data-value="protected">Public/Protected</li>
|
||||||
|
<li data-value="private" class="selected">All</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="checkbox" id="tsd-filter-inherited" checked />
|
||||||
|
<label class="tsd-widget" for="tsd-filter-inherited">Inherited</label>
|
||||||
|
|
||||||
|
{{#unless settings.excludeExternals}}
|
||||||
|
<input type="checkbox" id="tsd-filter-externals" checked />
|
||||||
|
<label class="tsd-widget" for="tsd-filter-externals">Externals</label>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tsd-page-title">
|
||||||
|
<div class="container">
|
||||||
|
{{#if model.parent}} {{! Don't show breadcrumbs on main project page, it is the root page. !}}
|
||||||
|
<ul class="tsd-breadcrumb">
|
||||||
|
{{#with model}}{{> breadcrumb}}{{/with}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
<h1>{{#compact}}
|
||||||
|
{{#ifCond model.kindString "!==" "Project" }}
|
||||||
|
{{model.kindString}}
|
||||||
|
{{/ifCond}}
|
||||||
|
{{model.name}}
|
||||||
|
{{#if model.typeParameters}}
|
||||||
|
<
|
||||||
|
{{#each model.typeParameters}}
|
||||||
|
{{#if @index}}, {{/if}}
|
||||||
|
{{name}}
|
||||||
|
{{/each}}
|
||||||
|
>
|
||||||
|
{{/if}}
|
||||||
|
{{/compact}}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
3
docs/theme/templates/markdown-page.hbs
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="tsd-panel tsd-typography">
|
||||||
|
{{#markdown}}{{{model.pagesPlugin.item.contents}}}{{/markdown}}
|
||||||
|
</div>
|
||||||
1338
package-lock.json
generated
35
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@extollo/lib",
|
"name": "@extollo/lib",
|
||||||
"version": "0.1.3",
|
"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",
|
||||||
@@ -8,25 +8,43 @@
|
|||||||
"lib": "lib"
|
"lib": "lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@extollo/di": "git+https://code.garrettmills.dev/extollo/di",
|
|
||||||
"@extollo/util": "git+https://code.garrettmills.dev/extollo/util",
|
|
||||||
"@types/busboy": "^0.2.3",
|
"@types/busboy": "^0.2.3",
|
||||||
|
"@types/mkdirp": "^1.0.1",
|
||||||
"@types/negotiator": "^0.6.1",
|
"@types/negotiator": "^0.6.1",
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^14.14.37",
|
||||||
|
"@types/pg": "^8.6.0",
|
||||||
|
"@types/pluralize": "^0.0.29",
|
||||||
"@types/pug": "^2.0.4",
|
"@types/pug": "^2.0.4",
|
||||||
|
"@types/rimraf": "^3.0.0",
|
||||||
|
"@types/ssh2": "^0.5.46",
|
||||||
|
"@types/uuid": "^8.3.0",
|
||||||
"busboy": "^0.3.1",
|
"busboy": "^0.3.1",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
"mkdirp": "^1.0.4",
|
||||||
"negotiator": "^0.6.2",
|
"negotiator": "^0.6.2",
|
||||||
|
"pg": "^8.6.0",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ssh2": "^1.1.0",
|
||||||
"ts-node": "^9.1.1",
|
"ts-node": "^9.1.1",
|
||||||
"typescript": "^4.2.3"
|
"typedoc": "^0.20.36",
|
||||||
|
"typedoc-plugin-pages-fork": "^0.0.1",
|
||||||
|
"typedoc-plugin-sourcefile-url": "^1.0.6",
|
||||||
|
"typescript": "^4.2.3",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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/**/*"
|
||||||
@@ -38,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
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1508
pnpm-lock.yaml
generated
466
src/cli/Directive.ts
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
import {Injectable, Inject} from '../di'
|
||||||
|
import {infer, ErrorWithContext} from '../util'
|
||||||
|
import {CLIOption} from './directive/options/CLIOption'
|
||||||
|
import {PositionalOption} from './directive/options/PositionalOption'
|
||||||
|
import {FlagOption} from './directive/options/FlagOption'
|
||||||
|
import {AppClass} from '../lifecycle/AppClass'
|
||||||
|
import {Logging} from '../service/Logging'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type alias for a definition of a command-line option.
|
||||||
|
*
|
||||||
|
* This can be either an instance of CLIOption or a string describing an option.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Some examples of positional/flag options defined by strings:
|
||||||
|
* `'{file name} | canonical name of the resource to create'`
|
||||||
|
*
|
||||||
|
* `'--push -p {value} | the value to be pushed'`
|
||||||
|
*
|
||||||
|
* `'--force -f | do a force push'`
|
||||||
|
*/
|
||||||
|
export type OptionDefinition = CLIOption<any> | string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error thrown when an invalid option was detected.
|
||||||
|
*/
|
||||||
|
export class OptionValidationError extends ErrorWithContext {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class representing a sub-command in the command-line utility.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export abstract class Directive extends AppClass {
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
/** Parsed option values. */
|
||||||
|
private optionValues: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the keyword or array of keywords that will specify this directive.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* If this returns `['up', 'start']`, the directive can be run by either of:
|
||||||
|
*
|
||||||
|
* ```shell
|
||||||
|
* ./ex up
|
||||||
|
* ./ex start
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public abstract getKeywords(): string | string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the usage description of this directive. Should be brief (1 sentence).
|
||||||
|
*/
|
||||||
|
public abstract getDescription(): string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally, specify a longer usage text that is shown on the directive's `--help` page.
|
||||||
|
*/
|
||||||
|
public getHelpText(): string {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of options defined for this command.
|
||||||
|
*/
|
||||||
|
public getOptions(): OptionDefinition[] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the directive is run from the command line.
|
||||||
|
*
|
||||||
|
* The raw arguments are provided as `argv`, but you are encouraged to use
|
||||||
|
* `getOptions()` and `option()` helpers to access the parsed options instead.
|
||||||
|
*
|
||||||
|
* @param argv
|
||||||
|
*/
|
||||||
|
public abstract handle(argv: string[]): void | Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the parsed option values.
|
||||||
|
* @param optionValues
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private setOptionValues(optionValues: any) {
|
||||||
|
this.optionValues = optionValues
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of a parsed option. If none exists, return `defaultValue`.
|
||||||
|
* @param name
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
|
public option(name: string, defaultValue?: unknown): any {
|
||||||
|
if ( name in this.optionValues ) {
|
||||||
|
return this.optionValues[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke this directive with the specified arguments.
|
||||||
|
*
|
||||||
|
* If usage was requested (see `didRequestUsage()`), it prints the extended usage info.
|
||||||
|
*
|
||||||
|
* Otherwise, it parses the options from `argv` and calls `handle()`.
|
||||||
|
*
|
||||||
|
* @param argv
|
||||||
|
*/
|
||||||
|
async invoke(argv: string[]): Promise<void> {
|
||||||
|
const options = this.getResolvedOptions()
|
||||||
|
|
||||||
|
if ( this.didRequestUsage(argv) ) {
|
||||||
|
const positionalArguments: PositionalOption<any>[] = []
|
||||||
|
options.forEach(opt => {
|
||||||
|
if ( opt instanceof PositionalOption ) {
|
||||||
|
positionalArguments.push(opt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const flagArguments: FlagOption<any>[] = []
|
||||||
|
options.forEach(opt => {
|
||||||
|
if ( opt instanceof FlagOption ) {
|
||||||
|
flagArguments.push(opt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const positionalDisplay: string = positionalArguments.map(x => `<${x.getArgumentName()}>`).join(' ')
|
||||||
|
const flagDisplay: string = flagArguments.length ? ' [...flags]' : ''
|
||||||
|
|
||||||
|
this.nativeOutput([
|
||||||
|
'',
|
||||||
|
`DIRECTIVE: ${this.getMainKeyword()} - ${this.getDescription()}`,
|
||||||
|
'',
|
||||||
|
`USAGE: ${this.getMainKeyword()} ${positionalDisplay}${flagDisplay}`,
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
if ( positionalArguments.length ) {
|
||||||
|
this.nativeOutput([
|
||||||
|
'',
|
||||||
|
`POSITIONAL ARGUMENTS:`,
|
||||||
|
...(positionalArguments.map(arg => {
|
||||||
|
return ` ${arg.getArgumentName()}${arg.message ? ' - ' + arg.message : ''}`
|
||||||
|
})),
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( flagArguments.length ) {
|
||||||
|
this.nativeOutput([
|
||||||
|
'',
|
||||||
|
`FLAGS:`,
|
||||||
|
...(flagArguments.map(arg => {
|
||||||
|
return ` ${arg.shortFlag ? arg.shortFlag + ', ' : ''}${arg.longFlag}${arg.argumentDescription ? ' {' + arg.argumentDescription + '}' : ''}${arg.message ? ' - ' + arg.message : ''}`
|
||||||
|
})),
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const help = this.getHelpText()
|
||||||
|
if ( help ) {
|
||||||
|
this.nativeOutput('\n' + help)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nativeOutput('\n')
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const optionValues = this.parseOptions(options, argv)
|
||||||
|
this.setOptionValues(optionValues)
|
||||||
|
await this.handle(argv)
|
||||||
|
} catch (e) {
|
||||||
|
this.nativeOutput(e.message)
|
||||||
|
if ( e instanceof OptionValidationError ) {
|
||||||
|
// expecting, value, requirements
|
||||||
|
if ( e.context.expecting ) {
|
||||||
|
this.nativeOutput(` - Expecting: ${e.context.expecting}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( e.context.requirements && Array.isArray(e.context.requirements) ) {
|
||||||
|
for ( const req of e.context.requirements ) {
|
||||||
|
this.nativeOutput(` - ${req}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( e.context.value ) {
|
||||||
|
this.nativeOutput(` - ${e.context.value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nativeOutput('\nUse --help for more info.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the array of option definitions to CLIOption instances.
|
||||||
|
* Of note, this resolves the string-form definitions to actual CLIOption instances.
|
||||||
|
*/
|
||||||
|
public getResolvedOptions(): CLIOption<any>[] {
|
||||||
|
return this.getOptions().map(option => {
|
||||||
|
if ( typeof option === 'string' ) {
|
||||||
|
return this.instantiateOptionFromString(option)
|
||||||
|
} else {
|
||||||
|
return option
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the main keyword displayed for this directive.
|
||||||
|
* @example
|
||||||
|
* If `getKeywords()` returns `['up', 'start']`, this will return `'up'`.
|
||||||
|
*/
|
||||||
|
public getMainKeyword(): string {
|
||||||
|
const kws = this.getKeywords()
|
||||||
|
|
||||||
|
if ( Array.isArray(kws) ) {
|
||||||
|
return kws[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return kws
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given keyword should invoke this directive.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
public matchesKeyword(name: string): boolean {
|
||||||
|
let kws = this.getKeywords()
|
||||||
|
if ( !Array.isArray(kws) ) {
|
||||||
|
kws = [kws]
|
||||||
|
}
|
||||||
|
return kws.includes(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the given output to the log as success text.
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
|
success(output: unknown): void {
|
||||||
|
this.logging.success(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the given output to the log as error text.
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
|
error(output: unknown): void {
|
||||||
|
this.logging.error(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the given output to the log as warning text.
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
|
warn(output: unknown): void {
|
||||||
|
this.logging.warn(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the given output to the log as info text.
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
|
info(output: unknown): void {
|
||||||
|
this.logging.info(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the given output to the log as debugging text.
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
|
debug(output: unknown): void {
|
||||||
|
this.logging.debug(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print the given output to the log as verbose text.
|
||||||
|
* @param output
|
||||||
|
*/
|
||||||
|
verbose(output: unknown): void {
|
||||||
|
this.logging.verbose(output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the flag option that signals help. Usually, this is named 'help'
|
||||||
|
* and supports the flags '--help' and '-?'.
|
||||||
|
*/
|
||||||
|
getHelpOption(): FlagOption<any> {
|
||||||
|
return new FlagOption('--help', '-?', 'usage information about this directive')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the raw CLI arguments using an array of option class instances to build
|
||||||
|
* a mapping of option names to provided values.
|
||||||
|
*/
|
||||||
|
parseOptions(options: CLIOption<any>[], args: string[]): {[key: string]: any} {
|
||||||
|
let positionalArguments: PositionalOption<any>[] = []
|
||||||
|
options.forEach(opt => {
|
||||||
|
if ( opt instanceof PositionalOption ) {
|
||||||
|
positionalArguments.push(opt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const flagArguments: FlagOption<any>[] = []
|
||||||
|
options.forEach(opt => {
|
||||||
|
if ( opt instanceof FlagOption ) {
|
||||||
|
flagArguments.push(opt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const optionValue: any = {}
|
||||||
|
|
||||||
|
flagArguments.push(this.getHelpOption())
|
||||||
|
|
||||||
|
let expectingFlagArgument = false
|
||||||
|
let positionalFlagName = ''
|
||||||
|
for ( const value of args ) {
|
||||||
|
if ( value.startsWith('--') ) {
|
||||||
|
if ( expectingFlagArgument ) {
|
||||||
|
throw new OptionValidationError(`Unexpected flag argument. Expecting argument for flag: ${positionalFlagName}`, {
|
||||||
|
expecting: positionalFlagName,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const flagArgument = flagArguments.filter(x => x.longFlag === value)
|
||||||
|
if ( flagArgument.length < 1 ) {
|
||||||
|
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if ( flagArgument[0].argumentDescription ) {
|
||||||
|
positionalFlagName = flagArgument[0].getArgumentName()
|
||||||
|
expectingFlagArgument = true
|
||||||
|
} else {
|
||||||
|
optionValue[flagArgument[0].getArgumentName()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ( value.startsWith('-') ) {
|
||||||
|
if ( expectingFlagArgument ) {
|
||||||
|
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||||
|
expecting: positionalFlagName,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const flagArgument = flagArguments.filter(x => x.shortFlag === value)
|
||||||
|
if ( flagArgument.length < 1 ) {
|
||||||
|
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if ( flagArgument[0].argumentDescription ) {
|
||||||
|
positionalFlagName = flagArgument[0].getArgumentName()
|
||||||
|
expectingFlagArgument = true
|
||||||
|
} else {
|
||||||
|
optionValue[flagArgument[0].getArgumentName()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ( expectingFlagArgument ) {
|
||||||
|
const inferredValue = infer(value)
|
||||||
|
const optionInstance = flagArguments.filter(x => x.getArgumentName() === positionalFlagName)[0]
|
||||||
|
if ( !optionInstance.validate(inferredValue) ) {
|
||||||
|
throw new OptionValidationError(`Invalid value for argument: ${positionalFlagName}`, {
|
||||||
|
requirements: optionInstance.getRequirementDisplays(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
optionValue[positionalFlagName] = inferredValue
|
||||||
|
expectingFlagArgument = false
|
||||||
|
} else {
|
||||||
|
if ( positionalArguments.length < 1 ) {
|
||||||
|
throw new OptionValidationError(`Unknown positional argument: ${value}`, {
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const inferredValue = infer(value)
|
||||||
|
if ( !positionalArguments[0].validate(inferredValue) ) {
|
||||||
|
throw new OptionValidationError(`Invalid value for argument: ${positionalArguments[0].getArgumentName()}`, {
|
||||||
|
requirements: positionalArguments[0].getRequirementDisplays(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
optionValue[positionalArguments[0].getArgumentName()] = infer(value)
|
||||||
|
positionalArguments = positionalArguments.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( expectingFlagArgument ) {
|
||||||
|
throw new OptionValidationError(`Missing argument for flag: ${positionalFlagName}`, {
|
||||||
|
expecting: positionalFlagName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( positionalArguments.length > 0 ) {
|
||||||
|
throw new OptionValidationError(`Missing required argument: ${positionalArguments[0].getArgumentName()}`, {
|
||||||
|
expecting: positionalArguments[0].getArgumentName(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of CLIOption based on a string definition of a particular format.
|
||||||
|
*
|
||||||
|
* e.g. '{file name} | canonical name of the resource to create'
|
||||||
|
* e.g. '--push -p {value} | the value to be pushed'
|
||||||
|
* e.g. '--force -f | do a force push'
|
||||||
|
*
|
||||||
|
* @param string
|
||||||
|
*/
|
||||||
|
protected instantiateOptionFromString(string: string): CLIOption<any> {
|
||||||
|
if ( string.startsWith('{') ) {
|
||||||
|
// The string is a positional argument
|
||||||
|
const stringParts = string.split('|').map(x => x.trim())
|
||||||
|
const name = stringParts[0].replace(/\{|\}/g, '')
|
||||||
|
return stringParts.length > 1 ? (new PositionalOption(name, stringParts[1])) : (new PositionalOption(name))
|
||||||
|
} else {
|
||||||
|
// The string is a flag argument
|
||||||
|
const stringParts = string.split('|').map(x => x.trim())
|
||||||
|
|
||||||
|
// Parse the flag parts first
|
||||||
|
const hasArgument = stringParts[0].indexOf('{') >= 0
|
||||||
|
const flagString = hasArgument ? stringParts[0].substr(0, stringParts[0].indexOf('{')).trim() : stringParts[0].trim()
|
||||||
|
const flagParts = flagString.split(' ')
|
||||||
|
|
||||||
|
let longFlag = flagParts[0].startsWith('--') ? flagParts[0] : undefined
|
||||||
|
if ( !longFlag && flagParts.length > 1 ) {
|
||||||
|
if ( flagParts[1].startsWith('--') ) {
|
||||||
|
longFlag = flagParts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let shortFlag = flagParts[0].length === 2 ? flagParts[0] : undefined
|
||||||
|
if ( !shortFlag && flagParts.length > 1 ) {
|
||||||
|
if ( flagParts[1].length === 2 ) {
|
||||||
|
shortFlag = flagParts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const argumentDescription = hasArgument ? stringParts[0].substring(stringParts[0].indexOf('{')+1, stringParts[0].indexOf('}')) : undefined
|
||||||
|
const description = stringParts.length > 1 ? stringParts[1] : undefined
|
||||||
|
|
||||||
|
return new FlagOption(longFlag, shortFlag, description, argumentDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if, at any point in the arguments, the help option's short or long flag appears.
|
||||||
|
* @returns {boolean} - true if the help flag appeared
|
||||||
|
*/
|
||||||
|
didRequestUsage(argv: string[]): boolean {
|
||||||
|
const helpOption = this.getHelpOption()
|
||||||
|
for ( const arg of argv ) {
|
||||||
|
if ( arg.trim() === helpOption.longFlag || arg.trim() === helpOption.shortFlag ) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected nativeOutput(...outputs: any[]): void {
|
||||||
|
console.log(...outputs) // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/cli/Template.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {UniversalPath} from '../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining a template that can be generated using the TemplateDirective.
|
||||||
|
*/
|
||||||
|
export interface Template {
|
||||||
|
/**
|
||||||
|
* The name of the template as it will be specified from the command line.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* If this is `'mytemplate'`, then the template will be created with:
|
||||||
|
*
|
||||||
|
* ```shell
|
||||||
|
* ./ex new mytemplate some:path
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
name: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The suffix of the file generated by this template.
|
||||||
|
* @example `.mytemplate.ts`
|
||||||
|
* @example `.controller.ts`
|
||||||
|
*/
|
||||||
|
fileSuffix: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Brief description of the template displayed on the --help page for the TemplateDirective.
|
||||||
|
* Should be brief (1 sentence).
|
||||||
|
*/
|
||||||
|
description: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of path-strings that are resolved relative to the base `app` directory.
|
||||||
|
* @example `['http', 'controllers']`
|
||||||
|
* @example `['units']`
|
||||||
|
*/
|
||||||
|
baseAppPath: string[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the given template to a string which will be written to the file.
|
||||||
|
* Note: this method should NOT write the contents to `targetFilePath`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* If the user enters:
|
||||||
|
*
|
||||||
|
* ```shell
|
||||||
|
* ./ex new mytemplate path:to:NewInstance
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Then, the following params are:
|
||||||
|
* ```typescript
|
||||||
|
* {
|
||||||
|
* name: 'NewInstance',
|
||||||
|
* fullCanonicalPath: 'path:to:NewInstance',
|
||||||
|
* targetFilePath: UniversalPath { }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param name - the singular name of the resource
|
||||||
|
* @param fullCanonicalName - the full canonical name of the resource
|
||||||
|
* @param targetFilePath - the UniversalPath where the file will be written
|
||||||
|
*/
|
||||||
|
render: (name: string, fullCanonicalName: string, targetFilePath: UniversalPath) => string | Promise<string>
|
||||||
|
}
|
||||||
31
src/cli/directive/RunDirective.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {Directive} from '../Directive'
|
||||||
|
import {CommandLineApplication} from '../service'
|
||||||
|
import {Injectable} from '../../di'
|
||||||
|
import {ErrorWithContext} from '../../util'
|
||||||
|
import {Unit} from '../../lifecycle/Unit'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directive that starts the framework's final target normally.
|
||||||
|
* In most cases, this runs the HTTP server, which would have been replaced
|
||||||
|
* by the CommandLineApplication unit.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class RunDirective extends Directive {
|
||||||
|
getDescription(): string {
|
||||||
|
return 'run the application normally'
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeywords(): string | string[] {
|
||||||
|
return ['run', 'up']
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(): Promise<void> {
|
||||||
|
if ( !CommandLineApplication.getReplacement() ) {
|
||||||
|
throw new ErrorWithContext(`Cannot run application: no run target specified.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit = <Unit> this.make(CommandLineApplication.getReplacement())
|
||||||
|
await this.app().startUnit(unit)
|
||||||
|
await this.app().stopUnit(unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/cli/directive/ShellDirective.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {Directive} from '../Directive'
|
||||||
|
import * as colors from 'colors/safe'
|
||||||
|
import * as repl from 'repl'
|
||||||
|
import {DependencyKey} from '../../di'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch an interactive REPL shell from within the application.
|
||||||
|
* This is very useful for debugging and testing things during development.
|
||||||
|
*/
|
||||||
|
export class ShellDirective extends Directive {
|
||||||
|
protected options: any = {
|
||||||
|
welcome: `powered by Extollo, © ${(new Date()).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`,
|
||||||
|
prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The created Node.js REPL server.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected repl?: repl.REPLServer
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return 'launch an interactive shell inside your application'
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeywords(): string | string[] {
|
||||||
|
return ['shell']
|
||||||
|
}
|
||||||
|
|
||||||
|
getHelpText(): string {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(): Promise<void> {
|
||||||
|
const state: any = {
|
||||||
|
app: this.app(),
|
||||||
|
make: (target: DependencyKey, ...parameters: any[]) => this.make(target, ...parameters),
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>(res => {
|
||||||
|
this.nativeOutput(this.options.welcome)
|
||||||
|
this.repl = repl.start(this.options.prompt)
|
||||||
|
Object.assign(this.repl.context, state)
|
||||||
|
this.repl.on('exit', () => res())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/cli/directive/TemplateDirective.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import {Directive, OptionDefinition} from '../Directive'
|
||||||
|
import {PositionalOption} from './options/PositionalOption'
|
||||||
|
import {CommandLine} from '../service'
|
||||||
|
import {Inject, Injectable} from '../../di'
|
||||||
|
import {ErrorWithContext} from '../../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new file based on a template registered with the CommandLine service.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class TemplateDirective extends Directive {
|
||||||
|
@Inject()
|
||||||
|
protected readonly cli!: CommandLine
|
||||||
|
|
||||||
|
getKeywords(): string | string[] {
|
||||||
|
return ['new', 'make']
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return 'create a new file from a registered template'
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions(): OptionDefinition[] {
|
||||||
|
const registeredTemplates = this.cli.getTemplates()
|
||||||
|
const template = new PositionalOption('template_name', 'the template to base the new file on (e.g. model, controller)')
|
||||||
|
template.whitelist(...registeredTemplates.pluck('name').all())
|
||||||
|
|
||||||
|
const destination = new PositionalOption('file_name', 'canonical name of the file to create (e.g. auth:Group, dash:Activity)')
|
||||||
|
|
||||||
|
return [template, destination]
|
||||||
|
}
|
||||||
|
|
||||||
|
getHelpText(): string {
|
||||||
|
const registeredTemplates = this.cli.getTemplates()
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Modules in Extollo register templates that can be used to quickly create common file types.',
|
||||||
|
'',
|
||||||
|
'For example, you can create a new model from @extollo/orm using the "model" template:',
|
||||||
|
'',
|
||||||
|
'./ex new model auth:Group',
|
||||||
|
'',
|
||||||
|
'This would create a new Group model in the ./src/app/models/auth/Group.model.ts file.',
|
||||||
|
'',
|
||||||
|
'AVAILABLE TEMPLATES:',
|
||||||
|
'',
|
||||||
|
...(registeredTemplates.map(template => {
|
||||||
|
return ` - ${template.name}: ${template.description}`
|
||||||
|
}).all()),
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(): Promise<void> {
|
||||||
|
const templateName: string = this.option('template_name')
|
||||||
|
const destinationName: string = this.option('file_name')
|
||||||
|
|
||||||
|
if ( destinationName.includes('/') || destinationName.includes('\\') ) {
|
||||||
|
this.error(`The destination should be a canonical name, not a file path.`)
|
||||||
|
this.error(`Reference sub-directories using the : character instead.`)
|
||||||
|
this.error(`Did you mean ${destinationName.replace(/\/|\\/g, ':')}?`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = this.cli.getTemplate(templateName)
|
||||||
|
if ( !template ) {
|
||||||
|
throw new ErrorWithContext(`Unable to find template supposedly registered with name: ${templateName}`, {
|
||||||
|
templateName,
|
||||||
|
destinationName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = destinationName.split(':').reverse()[0]
|
||||||
|
const path = this.app().path('..', 'src', 'app', ...template.baseAppPath, ...(`${destinationName}${template.fileSuffix}`).split(':'))
|
||||||
|
|
||||||
|
if ( await path.exists() ) {
|
||||||
|
this.error(`File already exists: ${path}`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the parent direction exists
|
||||||
|
await path.concat('..').mkdir()
|
||||||
|
|
||||||
|
const contents = await template.render(name, destinationName, path.clone())
|
||||||
|
await path.write(contents)
|
||||||
|
|
||||||
|
this.success(`Created new ${template.name} in ${path}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/cli/directive/UsageDirective.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {Directive} from '../Directive'
|
||||||
|
import {Injectable, Inject} from '../../di'
|
||||||
|
import {padRight} from '../../util'
|
||||||
|
import {CommandLine} from '../service'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive that prints the help message and usage information about
|
||||||
|
* directives registered with the command line utility.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class UsageDirective extends Directive {
|
||||||
|
@Inject()
|
||||||
|
protected readonly cli!: CommandLine
|
||||||
|
|
||||||
|
public getKeywords(): string | string[] {
|
||||||
|
return 'help'
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDescription(): string {
|
||||||
|
return 'print information about available commands'
|
||||||
|
}
|
||||||
|
|
||||||
|
public handle(): void | Promise<void> {
|
||||||
|
const directiveStrings = this.cli.getDirectives()
|
||||||
|
.map(cls => this.make<Directive>(cls))
|
||||||
|
.map<[string, string]>(dir => {
|
||||||
|
return [dir.getMainKeyword(), dir.getDescription()]
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxLen = directiveStrings.max<number>(x => x[0].length)
|
||||||
|
|
||||||
|
const printStrings = directiveStrings.map(grp => {
|
||||||
|
return [padRight(grp[0], maxLen + 1), grp[1]]
|
||||||
|
})
|
||||||
|
.map(grp => {
|
||||||
|
return ` ${grp[0]}: ${grp[1]}`
|
||||||
|
})
|
||||||
|
.toArray()
|
||||||
|
|
||||||
|
this.nativeOutput(this.cli.getASCIILogo())
|
||||||
|
this.nativeOutput([
|
||||||
|
'',
|
||||||
|
'Welcome to Extollo! Specify a command to get started.',
|
||||||
|
'',
|
||||||
|
`USAGE: ex <directive> [..options]`,
|
||||||
|
'',
|
||||||
|
...printStrings,
|
||||||
|
'',
|
||||||
|
'For usage information about a particular command, pass the --help flag.',
|
||||||
|
'-------------------------------------------',
|
||||||
|
`powered by Extollo, © ${(new Date()).getFullYear()} Garrett Mills`,
|
||||||
|
].join('\n'))
|
||||||
|
}
|
||||||
|
}
|
||||||
251
src/cli/directive/options/CLIOption.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* A CLI option. Supports basic comparative, and set-based validation.
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
export abstract class CLIOption<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the whitelist?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected useWhitelist = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the blacklist?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected useBlacklist = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the less-than comparison?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected useLessThan = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the greater-than comparison?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected useGreaterThan = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do we use the equality operator?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected useEquality = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this option optional?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected isOptional = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelisted values.
|
||||||
|
* @type {Array<*>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected whitelistItems: T[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blacklisted values.
|
||||||
|
* @type {Array<*>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected blacklistItems: T[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value to be compared in less than.
|
||||||
|
* @type {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected lessThanValue?: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the less than will be less than or equal to.
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected lessThanBit = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value to be compared in greater than.
|
||||||
|
* @type {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected greaterThanValue?: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the greater than will be greater than or equal to.
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected greaterThanBit = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value to be used to check equality.
|
||||||
|
* @type {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
protected equalityValue?: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelist the specified item or items and enable the whitelist.
|
||||||
|
* @param {...*} items - the items to whitelist
|
||||||
|
*/
|
||||||
|
whitelist(...items: T[]): this {
|
||||||
|
this.useWhitelist = true
|
||||||
|
items.forEach(item => this.whitelistItems.push(item))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blacklist the specified item or items and enable the blacklist.
|
||||||
|
* @param {...*} items - the items to blacklist
|
||||||
|
*/
|
||||||
|
blacklist(...items: T[]): this {
|
||||||
|
this.useBlacklist = true
|
||||||
|
items.forEach(item => this.blacklistItems.push(item))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in less-than comparison and enables less-than comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
lessThan(value: T): this {
|
||||||
|
this.useLessThan = true
|
||||||
|
this.lessThanValue = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in less-than or equal-to comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
lessThanOrEqualTo(value: T): this {
|
||||||
|
this.lessThanBit = true
|
||||||
|
this.lessThan(value)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in greater-than comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
greaterThan(value: T): this {
|
||||||
|
this.useGreaterThan = true
|
||||||
|
this.greaterThanValue = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in greater-than or equal-to comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
greaterThanOrEqualTo(value: T): this {
|
||||||
|
this.greaterThanBit = true
|
||||||
|
this.greaterThan(value)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the value to be used in equality comparison and enables that comparison.
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
equals(value: T): this {
|
||||||
|
this.useEquality = true
|
||||||
|
this.equalityValue = value
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified value passes the configured comparisons.
|
||||||
|
* @param value
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
validate(value: T): boolean {
|
||||||
|
let isValid = true
|
||||||
|
if ( this.useEquality ) {
|
||||||
|
isValid = isValid && (this.equalityValue === value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useLessThan && typeof this.lessThanValue !== 'undefined' ) {
|
||||||
|
if ( this.lessThanBit ) {
|
||||||
|
isValid = isValid && (value <= this.lessThanValue)
|
||||||
|
} else {
|
||||||
|
isValid = isValid && (value < this.lessThanValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useGreaterThan && typeof this.greaterThanValue !== 'undefined' ) {
|
||||||
|
if ( this.greaterThanBit ) {
|
||||||
|
isValid = isValid && (value >= this.greaterThanValue)
|
||||||
|
} else {
|
||||||
|
isValid = isValid && (value > this.greaterThanValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useWhitelist ) {
|
||||||
|
isValid = isValid && this.whitelistItems.some(x => {
|
||||||
|
return x === value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useBlacklist ) {
|
||||||
|
isValid = isValid && !(this.blacklistItems.some(x => x === value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Option as optional.
|
||||||
|
*/
|
||||||
|
optional(): this {
|
||||||
|
this.isOptional = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the argument name. Should be overridden by child classes.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
abstract getArgumentName(): string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of strings denoting the human-readable requirements for this option to be valid.
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
getRequirementDisplays(): string[] {
|
||||||
|
const clauses = []
|
||||||
|
|
||||||
|
if ( this.useBlacklist ) {
|
||||||
|
clauses.push(`must not be one of: ${this.blacklistItems.map(x => String(x)).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useWhitelist ) {
|
||||||
|
clauses.push(`must be one of: ${this.whitelistItems.map(x => String(x)).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useGreaterThan ) {
|
||||||
|
clauses.push(`must be greater than${this.greaterThanBit ? ' or equal to' : ''}: ${String(this.greaterThanValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useLessThan ) {
|
||||||
|
clauses.push(`must be less than${this.lessThanBit ? ' or equal to' : ''}: ${String(this.lessThanValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.useEquality ) {
|
||||||
|
clauses.push(`must be equal to: ${String(this.equalityValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clauses
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/cli/directive/options/FlagOption.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {CLIOption} from './CLIOption'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-positional, flag-based CLI option.
|
||||||
|
*/
|
||||||
|
export class FlagOption<T> extends CLIOption<T> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The long-form flag for this option.
|
||||||
|
* @example --path, --create
|
||||||
|
*/
|
||||||
|
public readonly longFlag?: string,
|
||||||
|
/**
|
||||||
|
* The short-form flag for this option.
|
||||||
|
* @example -p, -c
|
||||||
|
*/
|
||||||
|
public readonly shortFlag?: string,
|
||||||
|
/**
|
||||||
|
* Usage message describing this flag.
|
||||||
|
*/
|
||||||
|
public readonly message?: string,
|
||||||
|
/**
|
||||||
|
* Description of the argument required by this flag.
|
||||||
|
* If this is set, the flag will expect a positional argument to follow as a param.
|
||||||
|
*/
|
||||||
|
public readonly argumentDescription?: string,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the referential name for this option.
|
||||||
|
* Defaults to the long flag (without the '--'). If this cannot
|
||||||
|
* be found, the short flag (without the '-') is used.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getArgumentName(): string {
|
||||||
|
if ( this.longFlag ) {
|
||||||
|
return this.longFlag.replace('--', '')
|
||||||
|
} else if ( this.shortFlag ) {
|
||||||
|
return this.shortFlag.replace('-', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Missing either a long- or short-flag for FlagOption.')
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/cli/directive/options/PositionalOption.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {CLIOption} from './CLIOption'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A positional CLI option. Defined without a flag.
|
||||||
|
*/
|
||||||
|
export class PositionalOption<T> extends CLIOption<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate the option.
|
||||||
|
* @param {string} name - the name of the option
|
||||||
|
* @param {string} message - message describing the option
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The display name of this positional argument.
|
||||||
|
* @example path, filename
|
||||||
|
*/
|
||||||
|
public readonly name: string,
|
||||||
|
/**
|
||||||
|
* A usage message describing this parameter.
|
||||||
|
*/
|
||||||
|
public readonly message: string = '',
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the option.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getArgumentName(): string {
|
||||||
|
return this.name
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/cli/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export * from './Directive'
|
||||||
|
export * from './Template'
|
||||||
|
|
||||||
|
export * from './service/CommandLineApplication'
|
||||||
|
export * from './service/CommandLine'
|
||||||
|
|
||||||
|
export * from './directive/options/CLIOption'
|
||||||
|
export * from './directive/options/FlagOption'
|
||||||
|
export * from './directive/options/PositionalOption'
|
||||||
|
|
||||||
|
export * from './directive/ShellDirective'
|
||||||
|
export * from './directive/TemplateDirective'
|
||||||
|
export * from './directive/UsageDirective'
|
||||||
130
src/cli/service/CommandLine.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import {Singleton, Instantiable, Inject} from '../../di'
|
||||||
|
import {Collection} from '../../util'
|
||||||
|
import {CommandLineApplication} from './CommandLineApplication'
|
||||||
|
import {Directive} from '../Directive'
|
||||||
|
import {Template} from '../Template'
|
||||||
|
import {templateDirective} from '../templates/directive'
|
||||||
|
import {templateUnit} from '../templates/unit'
|
||||||
|
import {templateController} from '../templates/controller'
|
||||||
|
import {templateMiddleware} from '../templates/middleware'
|
||||||
|
import {templateRoutes} from '../templates/routes'
|
||||||
|
import {templateConfig} from '../templates/config'
|
||||||
|
import {Unit} from '../../lifecycle/Unit'
|
||||||
|
import {Logging} from '../../service/Logging'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing directives, templates, and other resources related
|
||||||
|
* to the command line utilities.
|
||||||
|
*/
|
||||||
|
@Singleton()
|
||||||
|
export class CommandLine extends Unit {
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
/** Directive classes registered with the CLI command. */
|
||||||
|
protected directives: Collection<Instantiable<Directive>> = new Collection<Instantiable<Directive>>()
|
||||||
|
|
||||||
|
/** Templates registered with the CLI command. These can be created with the TemplateDirective. */
|
||||||
|
protected templates: Collection<Template> = new Collection<Template>()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.registerTemplate(templateDirective)
|
||||||
|
this.registerTemplate(templateUnit)
|
||||||
|
this.registerTemplate(templateController)
|
||||||
|
this.registerTemplate(templateMiddleware)
|
||||||
|
this.registerTemplate(templateRoutes)
|
||||||
|
this.registerTemplate(templateConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the application was started from the command line.
|
||||||
|
*/
|
||||||
|
public isCLI(): boolean {
|
||||||
|
return this.app().hasUnit(CommandLineApplication)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string containing the Extollo ASCII logo.
|
||||||
|
*/
|
||||||
|
public getASCIILogo(): string {
|
||||||
|
return ` _
|
||||||
|
/ /\\ ______ _ _ _
|
||||||
|
/ / \\ | ____| | | | | |
|
||||||
|
/ / /\\ \\ | |__ __ _| |_ ___ | | | ___
|
||||||
|
/ / /\\ \\ \\ | __| \\ \\/ / __/ _ \\| | |/ _ \\
|
||||||
|
/ / / \\ \\_\\ | |____ > <| || (_) | | | (_) |
|
||||||
|
\\/_/ \\/_/ |______/_/\\_\\\\__\\___/|_|_|\\___/
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a Directive class with this service. This will make
|
||||||
|
* the directive available for use on the CLI.
|
||||||
|
* @param directiveClass
|
||||||
|
*/
|
||||||
|
public registerDirective(directiveClass: Instantiable<Directive>): this {
|
||||||
|
if ( !this.directives.includes(directiveClass) ) {
|
||||||
|
this.directives.push(directiveClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given directive is registered with this service.
|
||||||
|
* @param directiveClass
|
||||||
|
*/
|
||||||
|
public hasDirective(directiveClass: Instantiable<Directive>): boolean {
|
||||||
|
return this.directives.includes(directiveClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a collection of all registered directives.
|
||||||
|
*/
|
||||||
|
public getDirectives(): Collection<Instantiable<Directive>> {
|
||||||
|
return this.directives.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the given template with this service. This makes the template
|
||||||
|
* available for use with the TemplateDirective service.
|
||||||
|
* @param template
|
||||||
|
*/
|
||||||
|
public registerTemplate(template: Template): this {
|
||||||
|
if ( !this.templates.firstWhere('name', '=', template.name) ) {
|
||||||
|
this.templates.push(template)
|
||||||
|
} else {
|
||||||
|
this.logging.warn(`Duplicate template will not be registered: ${template.name}`)
|
||||||
|
this.logging.debug(`Duplicate template registered at: ${(new Error()).stack}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a template with the given name exists.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
public hasTemplate(name: string): boolean {
|
||||||
|
return Boolean(this.templates.firstWhere('name', '=', name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the template with the given name, if one exists.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
public getTemplate(name: string): Template | undefined {
|
||||||
|
return this.templates.firstWhere('name', '=', name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a collection of all registered templates.
|
||||||
|
*/
|
||||||
|
public getTemplates(): Collection<Template> {
|
||||||
|
return this.templates.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/cli/service/CommandLineApplication.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import {Unit} from '../../lifecycle/Unit'
|
||||||
|
import {Logging} from '../../service/Logging'
|
||||||
|
import {Singleton, Inject} from '../../di/decorator/injection'
|
||||||
|
import {CommandLine} from './CommandLine'
|
||||||
|
import {UsageDirective} from '../directive/UsageDirective'
|
||||||
|
import {Directive} from '../Directive'
|
||||||
|
import {ShellDirective} from '../directive/ShellDirective'
|
||||||
|
import {TemplateDirective} from '../directive/TemplateDirective'
|
||||||
|
import {RunDirective} from '../directive/RunDirective'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit that takes the place of the final unit in the application that handles
|
||||||
|
* invocations from the command line.
|
||||||
|
*/
|
||||||
|
@Singleton()
|
||||||
|
export class CommandLineApplication extends Unit {
|
||||||
|
/** The unit that was replaced by the CLI app. */
|
||||||
|
private static replacement?: typeof Unit
|
||||||
|
|
||||||
|
/** Set the replaced unit. */
|
||||||
|
public static setReplacement(unitClass?: typeof Unit): void {
|
||||||
|
this.replacement = unitClass
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the replaced unit. */
|
||||||
|
public static getReplacement(): typeof Unit | undefined {
|
||||||
|
return this.replacement
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly cli!: CommandLine
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async up(): Promise<void> {
|
||||||
|
this.cli.registerDirective(UsageDirective)
|
||||||
|
this.cli.registerDirective(ShellDirective)
|
||||||
|
this.cli.registerDirective(TemplateDirective)
|
||||||
|
this.cli.registerDirective(RunDirective)
|
||||||
|
|
||||||
|
const argv = process.argv.slice(2)
|
||||||
|
const match = this.cli.getDirectives()
|
||||||
|
.map(dirCls => this.make<Directive>(dirCls))
|
||||||
|
.firstWhere(dir => dir.matchesKeyword(argv[0]))
|
||||||
|
|
||||||
|
if ( match ) {
|
||||||
|
await match.invoke(argv.slice(1))
|
||||||
|
} else {
|
||||||
|
const usage = this.make<UsageDirective>(UsageDirective)
|
||||||
|
await usage.handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/cli/service/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './CommandLine'
|
||||||
|
export * from './CommandLineApplication'
|
||||||
21
src/cli/templates/config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {Template} from '../Template'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A template that generates a new configuration file in the app/configs directory.
|
||||||
|
*/
|
||||||
|
const templateConfig: Template = {
|
||||||
|
name: 'config',
|
||||||
|
fileSuffix: '.config.ts',
|
||||||
|
description: 'Create a new config file.',
|
||||||
|
baseAppPath: ['configs'],
|
||||||
|
render() {
|
||||||
|
return `import { env } from '@extollo/lib'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
key: env('VALUE_ENV_VAR', 'default value'),
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { templateConfig }
|
||||||
29
src/cli/templates/controller.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {Template} from '../Template'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template that generates a new controller in the app/http/controllers directory.
|
||||||
|
*/
|
||||||
|
const templateController: Template = {
|
||||||
|
name: 'controller',
|
||||||
|
fileSuffix: '.controller.ts',
|
||||||
|
description: 'Create a controller class that can be used to handle requests.',
|
||||||
|
baseAppPath: ['http', 'controllers'],
|
||||||
|
render(name: string) {
|
||||||
|
return `import {Controller, view, Inject, Injectable} from '@extollo/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${name} Controller
|
||||||
|
* ------------------------------------
|
||||||
|
* Put some description here.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ${name} extends Controller {
|
||||||
|
public ${name.toLowerCase()}() {
|
||||||
|
return view('${name.toLowerCase()}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { templateController }
|
||||||
41
src/cli/templates/directive.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {Template} from '../Template'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template that generates a new Directive class in the app/directives directory.
|
||||||
|
*/
|
||||||
|
const templateDirective: Template = {
|
||||||
|
name: 'directive',
|
||||||
|
fileSuffix: '.directive.ts',
|
||||||
|
description: 'Create a new Directive class which adds functionality to the ./ex command.',
|
||||||
|
baseAppPath: ['directives'],
|
||||||
|
render(name: string) {
|
||||||
|
return `import {Directive, OptionDefinition, Injectable} from '@extollo/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${name} Directive
|
||||||
|
* ---------------------------------------------------
|
||||||
|
* Put some description here.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ${name}Directive extends Directive {
|
||||||
|
getKeywords(): string | string[] {
|
||||||
|
return ['${name.toLowerCase()}']
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions(): OptionDefinition[] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(argv: string[]) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { templateDirective }
|
||||||
29
src/cli/templates/middleware.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {Template} from '../Template'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template that generates a new middleware class in app/http/middlewares.
|
||||||
|
*/
|
||||||
|
const templateMiddleware: Template = {
|
||||||
|
name: 'middleware',
|
||||||
|
fileSuffix: '.middleware.ts',
|
||||||
|
description: 'Create a middleware class that can be applied to routes.',
|
||||||
|
baseAppPath: ['http', 'middlewares'],
|
||||||
|
render(name: string) {
|
||||||
|
return `import {Middleware, Injectable} from '@extollo/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${name} Middleware
|
||||||
|
* --------------------------------------------
|
||||||
|
* Put some description here.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ${name} extends Middleware {
|
||||||
|
public async apply() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { templateMiddleware }
|
||||||
25
src/cli/templates/routes.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {Template} from '../Template'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template that generates a new route definition file in app/http/routes.
|
||||||
|
*/
|
||||||
|
const templateRoutes: Template = {
|
||||||
|
name: 'routes',
|
||||||
|
fileSuffix: '.routes.ts',
|
||||||
|
description: 'Create a file for route definitions.',
|
||||||
|
baseAppPath: ['http', 'routes'],
|
||||||
|
render(name: string) {
|
||||||
|
return `import {Route} from '@extollo/lib'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ${name} Routes
|
||||||
|
* -------------------------------
|
||||||
|
* Put some description here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { templateRoutes }
|
||||||
36
src/cli/templates/unit.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {Template} from '../Template'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template that generates a new application unit class in app/units.
|
||||||
|
*/
|
||||||
|
const templateUnit: Template = {
|
||||||
|
name: 'unit',
|
||||||
|
fileSuffix: '.ts',
|
||||||
|
description: 'Create a service unit that will start and stop with your application.',
|
||||||
|
baseAppPath: ['units'],
|
||||||
|
render(name: string) {
|
||||||
|
return `import {Singleton, Inject, Unit, Logging} from '@extollo/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${name} Unit
|
||||||
|
* ---------------------------------------
|
||||||
|
* Put some description here.
|
||||||
|
*/
|
||||||
|
@Singleton()
|
||||||
|
export class ${name} extends Unit {
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
public async up() {
|
||||||
|
this.logging.info('${name} has started!')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down() {
|
||||||
|
this.logging.info('${name} has stopped!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { templateUnit }
|
||||||
11
src/cli/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
300
src/di/Container.ts
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import {DependencyKey, InstanceRef, Instantiable, isInstantiable} from './types'
|
||||||
|
import {AbstractFactory} from './factory/AbstractFactory'
|
||||||
|
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
|
||||||
|
import {Factory} from './factory/Factory'
|
||||||
|
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
|
||||||
|
import {ClosureFactory} from './factory/ClosureFactory'
|
||||||
|
import NamedFactory from './factory/NamedFactory'
|
||||||
|
import SingletonFactory from './factory/SingletonFactory'
|
||||||
|
import {InvalidDependencyKeyError} from './error/InvalidDependencyKeyError'
|
||||||
|
|
||||||
|
export type MaybeFactory<T> = AbstractFactory<T> | undefined
|
||||||
|
export type MaybeDependency = any | undefined
|
||||||
|
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container of resolve-able dependencies that are created via inversion-of-control.
|
||||||
|
*/
|
||||||
|
export class Container {
|
||||||
|
/**
|
||||||
|
* Get the global instance of this container.
|
||||||
|
*/
|
||||||
|
public static getContainer(): Container {
|
||||||
|
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||||
|
if ( !existing ) {
|
||||||
|
const container = new Container()
|
||||||
|
globalRegistry.setGlobal('extollo/injector', container)
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of factories registered with this container.
|
||||||
|
* @type Collection<AbstractFactory>
|
||||||
|
*/
|
||||||
|
protected factories: Collection<AbstractFactory<unknown>> = new Collection<AbstractFactory<unknown>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of singleton instances produced by this container.
|
||||||
|
* @type Collection<InstanceRef>
|
||||||
|
*/
|
||||||
|
protected instances: Collection<InstanceRef> = new Collection<InstanceRef>()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.registerSingletonInstance<Container>(Container, this)
|
||||||
|
this.registerSingleton('injector', this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a basic instantiable class as a standard Factory with this container.
|
||||||
|
* @param {Instantiable} dependency
|
||||||
|
*/
|
||||||
|
register(dependency: Instantiable<any>): this {
|
||||||
|
if ( this.resolve(dependency) ) {
|
||||||
|
throw new DuplicateFactoryKeyError(dependency)
|
||||||
|
}
|
||||||
|
|
||||||
|
const factory = new Factory(dependency)
|
||||||
|
this.factories.push(factory)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the given function as a factory within the container.
|
||||||
|
* @param {string} name - unique name to identify the factory in the container
|
||||||
|
* @param {function} producer - factory to produce a value
|
||||||
|
*/
|
||||||
|
registerProducer(name: DependencyKey, producer: () => any): this {
|
||||||
|
if ( this.resolve(name) ) {
|
||||||
|
throw new DuplicateFactoryKeyError(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const factory = new ClosureFactory(name, producer)
|
||||||
|
this.factories.push(factory)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a basic instantiable class as a standard Factory with this container,
|
||||||
|
* identified by a string name rather than static class.
|
||||||
|
* @param {string} name - unique name to identify the factory in the container
|
||||||
|
* @param {Instantiable} dependency
|
||||||
|
*/
|
||||||
|
registerNamed(name: string, dependency: Instantiable<any>): this {
|
||||||
|
if ( this.resolve(name) ) {
|
||||||
|
throw new DuplicateFactoryKeyError(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const factory = new NamedFactory(name, dependency)
|
||||||
|
this.factories.push(factory)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a value as a singleton in the container. It will not be instantiated, but
|
||||||
|
* can be injected by its unique name.
|
||||||
|
* @param {string} key - unique name to identify the singleton in the container
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
registerSingleton<T>(key: DependencyKey, value: T): this {
|
||||||
|
if ( this.resolve(key) ) {
|
||||||
|
throw new DuplicateFactoryKeyError(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.factories.push(new SingletonFactory(key, value))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a static class to the container along with its already-instantiated
|
||||||
|
* instance that will be used to resolve the class.
|
||||||
|
* @param staticClass
|
||||||
|
* @param instance
|
||||||
|
*/
|
||||||
|
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
|
||||||
|
if ( this.resolve(staticClass) ) {
|
||||||
|
throw new DuplicateFactoryKeyError(staticClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.register(staticClass)
|
||||||
|
this.instances.push({
|
||||||
|
key: staticClass,
|
||||||
|
value: instance,
|
||||||
|
})
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a given factory with the container.
|
||||||
|
* @param {AbstractFactory} factory
|
||||||
|
*/
|
||||||
|
registerFactory(factory: AbstractFactory<unknown>): this {
|
||||||
|
if ( !this.factories.includes(factory) ) {
|
||||||
|
this.factories.push(factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the container has an already-produced value for the given key.
|
||||||
|
* @param {DependencyKey} key
|
||||||
|
*/
|
||||||
|
hasInstance(key: DependencyKey): boolean {
|
||||||
|
return this.instances.where('key', '=', key).isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the container has a factory for the given key.
|
||||||
|
* @param {DependencyKey} key
|
||||||
|
*/
|
||||||
|
hasKey(key: DependencyKey): boolean {
|
||||||
|
return Boolean(this.resolve(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the already-produced value for the given key, if one exists.
|
||||||
|
* @param {DependencyKey} key
|
||||||
|
*/
|
||||||
|
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||||
|
const instances = this.instances.where('key', '=', key)
|
||||||
|
if ( instances.isNotEmpty() ) {
|
||||||
|
return instances.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the factory for the given key, if one is registered with this container.
|
||||||
|
* @param {DependencyKey} key
|
||||||
|
*/
|
||||||
|
resolve(key: DependencyKey): MaybeFactory<unknown> {
|
||||||
|
const factory = this.factories.firstWhere(item => item.match(key))
|
||||||
|
if ( factory ) {
|
||||||
|
return factory
|
||||||
|
} else {
|
||||||
|
logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the dependency key. If a singleton value for that key already exists in this container,
|
||||||
|
* return that value. Otherwise, use the factory an given parameters to produce and return the value.
|
||||||
|
* @param {DependencyKey} key
|
||||||
|
* @param {...any} parameters
|
||||||
|
*/
|
||||||
|
resolveAndCreate(key: DependencyKey, ...parameters: any[]): any {
|
||||||
|
logIfDebugging('extollo.di.injector', 'resolveAndCreate', key, {parameters})
|
||||||
|
|
||||||
|
// If we've already instantiated this, just return that
|
||||||
|
const instance = this.getExistingInstance(key)
|
||||||
|
logIfDebugging('extollo.di.injector', 'resolveAndCreate existing instance?', instance)
|
||||||
|
if ( typeof instance !== 'undefined' ) {
|
||||||
|
return instance.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, attempt to create it
|
||||||
|
const factory = this.resolve(key)
|
||||||
|
logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory)
|
||||||
|
if ( !factory ) {
|
||||||
|
throw new InvalidDependencyKeyError(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce and store a new instance
|
||||||
|
const newInstance = this.produceFactory(factory, parameters)
|
||||||
|
this.instances.push({
|
||||||
|
key,
|
||||||
|
value: newInstance,
|
||||||
|
})
|
||||||
|
|
||||||
|
return newInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a factory and manually-provided parameters, resolve the dependencies for the
|
||||||
|
* factory and produce its value.
|
||||||
|
* @param {AbstractFactory} factory
|
||||||
|
* @param {array} parameters
|
||||||
|
*/
|
||||||
|
protected produceFactory<T>(factory: AbstractFactory<T>, parameters: any[]): T {
|
||||||
|
// Create the dependencies for the factory
|
||||||
|
const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key))
|
||||||
|
const dependencies = keys.map<ResolvedDependency>(req => {
|
||||||
|
return {
|
||||||
|
paramIndex: req.paramIndex,
|
||||||
|
key: req.key,
|
||||||
|
resolved: this.resolveAndCreate(req.key),
|
||||||
|
}
|
||||||
|
}).sortBy('paramIndex')
|
||||||
|
|
||||||
|
// Build the arguments for the factory, using dependencies in the
|
||||||
|
// correct paramIndex positions, or parameters of we don't have
|
||||||
|
// the dependency.
|
||||||
|
const constructorArguments = []
|
||||||
|
const params = collect(parameters).reverse()
|
||||||
|
for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) {
|
||||||
|
const dep = dependencies.firstWhere('paramIndex', '=', i)
|
||||||
|
if ( dep ) {
|
||||||
|
constructorArguments.push(dep.resolved)
|
||||||
|
} else {
|
||||||
|
constructorArguments.push(params.pop())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce a new instance
|
||||||
|
const inst = factory.produce(constructorArguments, params.reverse().all())
|
||||||
|
|
||||||
|
factory.getInjectedProperties().each(dependency => {
|
||||||
|
if ( dependency.key && inst ) {
|
||||||
|
(inst as any)[dependency.property] = this.resolveAndCreate(dependency.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return inst
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of the given target. The target can either be a DependencyKey registered with
|
||||||
|
* this container (in which case, the singleton value will be returned), or an instantiable class.
|
||||||
|
*
|
||||||
|
* If the instantiable class has the Injectable decorator, its injectable parameters will be automatically
|
||||||
|
* injected into the instance.
|
||||||
|
* @param {DependencyKey} target
|
||||||
|
* @param {...any} parameters
|
||||||
|
*/
|
||||||
|
make<T>(target: DependencyKey, ...parameters: any[]): T {
|
||||||
|
if ( this.hasKey(target) ) {
|
||||||
|
return this.resolveAndCreate(target, ...parameters)
|
||||||
|
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
|
||||||
|
return this.produceFactory(new Factory(target), parameters)
|
||||||
|
} else {
|
||||||
|
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.
|
||||||
|
* @param {DependencyKey} target
|
||||||
|
*/
|
||||||
|
getDependencies(target: DependencyKey): Collection<DependencyKey> {
|
||||||
|
const factory = this.resolve(target)
|
||||||
|
|
||||||
|
if ( !factory ) {
|
||||||
|
throw new InvalidDependencyKeyError(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory.getDependencyKeys().pluck('key')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a different container, copy the factories and instances from this container over to it.
|
||||||
|
* @param container
|
||||||
|
*/
|
||||||
|
cloneTo(container: Container): this {
|
||||||
|
container.factories = this.factories.clone()
|
||||||
|
container.instances = this.instances.clone()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/di/ScopedContainer.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {Container, MaybeDependency, MaybeFactory} from './Container'
|
||||||
|
import {DependencyKey} from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container that uses some parent container as a base, but
|
||||||
|
* can have other factories distinct from that parent.
|
||||||
|
*
|
||||||
|
* If an instance is not found in this container, it will be resolved from
|
||||||
|
* the parent container.
|
||||||
|
*
|
||||||
|
* However, if an instance IS found in this container, it will ALWAYS be
|
||||||
|
* resolved from this container, rather than the parent.
|
||||||
|
*
|
||||||
|
* This can be used to create scope-specific containers that can still resolve
|
||||||
|
* the global dependencies, while keeping scope-specific dependencies separate.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* The Request class from @extollo/lib is a ScopedContainer. It can resolve
|
||||||
|
* all dependencies that exist in the global Container, but it can also have
|
||||||
|
* request-specific services (like the Session) injected into it.
|
||||||
|
*
|
||||||
|
* @extends Container
|
||||||
|
*/
|
||||||
|
export class ScopedContainer extends Container {
|
||||||
|
/**
|
||||||
|
* Create a new scoped container based on a parent container instance.
|
||||||
|
* @param container
|
||||||
|
*/
|
||||||
|
public static fromParent(container: Container): ScopedContainer {
|
||||||
|
return new ScopedContainer(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private parentContainer: Container,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.registerSingletonInstance<ScopedContainer>(ScopedContainer, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasInstance(key: DependencyKey): boolean {
|
||||||
|
return super.hasInstance(key) || this.parentContainer.hasInstance(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasKey(key: DependencyKey): boolean {
|
||||||
|
return super.hasKey(key) || this.parentContainer.hasKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||||
|
const inst = super.getExistingInstance(key)
|
||||||
|
if ( inst ) {
|
||||||
|
return inst
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parentContainer.getExistingInstance(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(key: DependencyKey): MaybeFactory<any> {
|
||||||
|
const factory = super.resolve(key)
|
||||||
|
if ( factory ) {
|
||||||
|
return factory
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parentContainer?.resolve(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/di/decorator/injection.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
import {collect, Collection} from '../../util'
|
||||||
|
import {
|
||||||
|
DependencyKey,
|
||||||
|
DependencyRequirement,
|
||||||
|
DEPENDENCY_KEYS_METADATA_KEY,
|
||||||
|
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
|
||||||
|
isInstantiable,
|
||||||
|
InjectionType,
|
||||||
|
DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
|
||||||
|
PropertyDependency,
|
||||||
|
} from '../types'
|
||||||
|
import {Container} from '../Container'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a collection of dependency requirements for the given target object.
|
||||||
|
* @param {Object} target
|
||||||
|
* @return Collection<DependencyRequirement>
|
||||||
|
*/
|
||||||
|
function initDependencyMetadata(target: unknown): Collection<DependencyRequirement> {
|
||||||
|
const paramTypes = Reflect.getMetadata('design:paramtypes', target as any)
|
||||||
|
return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((type, idx) => {
|
||||||
|
return {
|
||||||
|
paramIndex: idx,
|
||||||
|
key: type,
|
||||||
|
overridden: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class decorator that marks a class as injectable. When this is applied, dependency
|
||||||
|
* metadata for the constructors params is resolved and stored in metadata.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const Injectable = (): ClassDecorator => {
|
||||||
|
return (target) => {
|
||||||
|
const meta = initDependencyMetadata(target)
|
||||||
|
const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
|
||||||
|
const newMetadata = new Collection<DependencyRequirement>()
|
||||||
|
|
||||||
|
if ( existing ) {
|
||||||
|
const maxNew = meta.max('paramIndex')
|
||||||
|
const maxExisting = existing.max('paramIndex')
|
||||||
|
for ( let i = 0; i <= Math.max(maxNew, maxExisting); i++ ) {
|
||||||
|
const existingDR = existing.firstWhere('paramIndex', '=', i)
|
||||||
|
const newDR = meta.firstWhere('paramIndex', '=', i)
|
||||||
|
|
||||||
|
if ( existingDR && !newDR ) {
|
||||||
|
newMetadata.push(existingDR)
|
||||||
|
} else if ( newDR && !existingDR ) {
|
||||||
|
newMetadata.push(newDR)
|
||||||
|
} else if ( newDR && existingDR ) {
|
||||||
|
if ( existingDR.overridden ) {
|
||||||
|
newMetadata.push(existingDR)
|
||||||
|
} else {
|
||||||
|
newMetadata.push(newDR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newMetadata.concat(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, newMetadata, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the given class property to be injected by the container.
|
||||||
|
* If a `key` is specified, that DependencyKey will be injected.
|
||||||
|
* Otherwise, the DependencyKey is inferred from the type annotation.
|
||||||
|
* @param key
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const Inject = (key?: DependencyKey): PropertyDecorator => {
|
||||||
|
return (target, property) => {
|
||||||
|
let propertyMetadata = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, target?.constructor || target) as Collection<PropertyDependency>
|
||||||
|
if ( !propertyMetadata ) {
|
||||||
|
propertyMetadata = new Collection<PropertyDependency>()
|
||||||
|
Reflect.defineMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, propertyMetadata, target?.constructor || target)
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = Reflect.getMetadata('design:type', target, property)
|
||||||
|
if ( !key && type ) {
|
||||||
|
key = type
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( key ) {
|
||||||
|
const existing = propertyMetadata.firstWhere('property', '=', property)
|
||||||
|
if ( existing ) {
|
||||||
|
existing.key = key
|
||||||
|
} else {
|
||||||
|
propertyMetadata.push({ property,
|
||||||
|
key })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Reflect.defineMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, propertyMetadata, target?.constructor || target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameter decorator to manually mark a parameter as being an injection target on injectable
|
||||||
|
* classes. This can be used to override the dependency key of a given parameter.
|
||||||
|
* @param {DependencyKey} key
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const InjectParam = (key: DependencyKey): ParameterDecorator => {
|
||||||
|
return (target, property, paramIndex) => {
|
||||||
|
if ( !Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) ) {
|
||||||
|
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, initDependencyMetadata(target), target)
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
|
||||||
|
const req = meta.firstWhere('paramIndex', '=', paramIndex)
|
||||||
|
if ( req ) {
|
||||||
|
req.key = key
|
||||||
|
req.overridden = true
|
||||||
|
} else {
|
||||||
|
meta.push({
|
||||||
|
paramIndex,
|
||||||
|
key,
|
||||||
|
overridden: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, meta, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class decorator that registers the class as a singleton instance in the global container.
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
export const Singleton = (name?: string): ClassDecorator => {
|
||||||
|
return (target) => {
|
||||||
|
if ( isInstantiable(target) ) {
|
||||||
|
const injectionType: InjectionType = {
|
||||||
|
type: name ? 'named' : 'singleton',
|
||||||
|
...(name ? { name } : {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)
|
||||||
|
Injectable()(target)
|
||||||
|
|
||||||
|
if ( name ) {
|
||||||
|
Container.getContainer().registerNamed(name, target)
|
||||||
|
} else {
|
||||||
|
Container.getContainer().register(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/di/error/DuplicateFactoryKeyError.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {DependencyKey} from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when a factory is registered with a duplicate dependency key.
|
||||||
|
* @extends Error
|
||||||
|
*/
|
||||||
|
export class DuplicateFactoryKeyError extends Error {
|
||||||
|
constructor(key: DependencyKey) {
|
||||||
|
super(`A factory definition already exists with the key for ${key}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/di/error/InvalidDependencyKeyError.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {DependencyKey} from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when a dependency key that has not been registered is passed to a resolver.
|
||||||
|
* @extends Error
|
||||||
|
*/
|
||||||
|
export class InvalidDependencyKeyError extends Error {
|
||||||
|
constructor(key: DependencyKey) {
|
||||||
|
super(`No such dependency is registered with this container: ${key}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/di/factory/AbstractFactory.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||||
|
import { Collection } from '../../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for dependency container factories.
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
export abstract class AbstractFactory<T> {
|
||||||
|
protected constructor(
|
||||||
|
/**
|
||||||
|
* Token that was registered for this factory. In most cases, this is the static
|
||||||
|
* form of the item that is to be produced by this factory.
|
||||||
|
* @var
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected token: DependencyKey,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce an instance of the token.
|
||||||
|
* @param {Array} dependencies - the resolved dependencies, in order
|
||||||
|
* @param {Array} parameters - the bound constructor parameters, in order
|
||||||
|
*/
|
||||||
|
abstract produce(dependencies: any[], parameters: any[]): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should return true if the given identifier matches the token for this factory.
|
||||||
|
* @param something
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
abstract match(something: unknown): boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependency requirements required by this factory's token.
|
||||||
|
* @return Collection<DependencyRequirement>
|
||||||
|
*/
|
||||||
|
abstract getDependencyKeys(): Collection<DependencyRequirement>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the property dependencies that should be injected to the created instance.
|
||||||
|
* @return Collection<PropertyDependency>
|
||||||
|
*/
|
||||||
|
abstract getInjectedProperties(): Collection<PropertyDependency>
|
||||||
|
}
|
||||||
43
src/di/factory/ClosureFactory.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {AbstractFactory} from './AbstractFactory'
|
||||||
|
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||||
|
import {Collection} from '../../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory whose token is produced by calling a function.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* let i = 0
|
||||||
|
* const fact = new ClosureFactory('someName', () => {
|
||||||
|
* i += 1
|
||||||
|
* return i * 2
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* fact.produce([], []) // => 2
|
||||||
|
* fact.produce([], []) // => 4
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class ClosureFactory<T> extends AbstractFactory<T> {
|
||||||
|
constructor(
|
||||||
|
protected readonly name: DependencyKey,
|
||||||
|
protected readonly token: () => T,
|
||||||
|
) {
|
||||||
|
super(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
produce(): any {
|
||||||
|
return this.token()
|
||||||
|
}
|
||||||
|
|
||||||
|
match(something: unknown): boolean {
|
||||||
|
return something === this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||||
|
return new Collection<DependencyRequirement>()
|
||||||
|
}
|
||||||
|
|
||||||
|
getInjectedProperties(): Collection<PropertyDependency> {
|
||||||
|
return new Collection<PropertyDependency>()
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/di/factory/Factory.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {AbstractFactory} from './AbstractFactory'
|
||||||
|
import {
|
||||||
|
DEPENDENCY_KEYS_METADATA_KEY,
|
||||||
|
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
|
||||||
|
DependencyRequirement,
|
||||||
|
Instantiable,
|
||||||
|
PropertyDependency,
|
||||||
|
} from '../types'
|
||||||
|
import {Collection} from '../../util'
|
||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard static-class factory. The token of this factory is a reference to a
|
||||||
|
* static class that is instantiated when the factory produces.
|
||||||
|
*
|
||||||
|
* Dependency keys are inferred from injection metadata on the constructor's params,
|
||||||
|
* as are the injected properties.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class A {
|
||||||
|
* constructor(
|
||||||
|
* protected readonly myService: MyService
|
||||||
|
* ) { }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* const fact = new Factory(A)
|
||||||
|
*
|
||||||
|
* fact.produce([myServiceInstance], []) // => A { myService: myServiceInstance }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Factory<T> extends AbstractFactory<T> {
|
||||||
|
constructor(
|
||||||
|
protected readonly token: Instantiable<T>,
|
||||||
|
) {
|
||||||
|
super(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
produce(dependencies: any[], parameters: any[]): any {
|
||||||
|
return new this.token(...dependencies, ...parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
match(something: unknown): boolean {
|
||||||
|
return something === this.token // || (something?.name && something.name === this.token.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||||
|
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token)
|
||||||
|
if ( meta ) {
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
return new Collection<DependencyRequirement>()
|
||||||
|
}
|
||||||
|
|
||||||
|
getInjectedProperties(): Collection<PropertyDependency> {
|
||||||
|
const meta = new Collection<PropertyDependency>()
|
||||||
|
let currentToken = this.token
|
||||||
|
|
||||||
|
do {
|
||||||
|
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
|
||||||
|
if ( loadedMeta ) {
|
||||||
|
meta.concat(loadedMeta)
|
||||||
|
}
|
||||||
|
currentToken = Object.getPrototypeOf(currentToken)
|
||||||
|
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/di/factory/NamedFactory.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {Factory} from './Factory'
|
||||||
|
import {Instantiable} from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container factory that produces an instance of the token, however the token
|
||||||
|
* is identified by a string name rather than a class reference.
|
||||||
|
* @extends Factory
|
||||||
|
*/
|
||||||
|
export default class NamedFactory<T> extends Factory<T> {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* The name identifying this factory in the container.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
protected name: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The token to be instantiated.
|
||||||
|
* @type {Instantiable}
|
||||||
|
*/
|
||||||
|
protected token: Instantiable<T>,
|
||||||
|
) {
|
||||||
|
super(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
match(something: unknown): boolean {
|
||||||
|
return something === this.name
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/di/factory/SingletonFactory.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Factory } from './Factory'
|
||||||
|
import { Collection } from '../../util'
|
||||||
|
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container factory which returns its token as its value, without attempting
|
||||||
|
* to instantiate anything. This is used to register already-produced-singletons
|
||||||
|
* with the container.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class A {}
|
||||||
|
* const exactlyThisInstanceOfA = new A()
|
||||||
|
*
|
||||||
|
* const fact = new SingletonFactory(A, a)
|
||||||
|
*
|
||||||
|
* fact.produce([], []) // => exactlyThisInstanceOfA
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @extends Factory
|
||||||
|
*/
|
||||||
|
export default class SingletonFactory<T> extends Factory<T> {
|
||||||
|
constructor(
|
||||||
|
/**
|
||||||
|
* Token identifying this singleton.
|
||||||
|
*/
|
||||||
|
protected token: DependencyKey,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of this singleton.
|
||||||
|
*/
|
||||||
|
protected value: T,
|
||||||
|
) {
|
||||||
|
super(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
produce(): T {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
|
||||||
|
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||||
|
return new Collection<DependencyRequirement>()
|
||||||
|
}
|
||||||
|
|
||||||
|
getInjectedProperties(): Collection<PropertyDependency> {
|
||||||
|
return new Collection<PropertyDependency>()
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/di/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export * from './error/DuplicateFactoryKeyError'
|
||||||
|
export * from './error/InvalidDependencyKeyError'
|
||||||
|
|
||||||
|
export * from './factory/AbstractFactory'
|
||||||
|
export * from './factory/ClosureFactory'
|
||||||
|
export * from './factory/Factory'
|
||||||
|
export * from './factory/NamedFactory'
|
||||||
|
export * from './factory/SingletonFactory'
|
||||||
|
|
||||||
|
export * from './Container'
|
||||||
|
export * from './ScopedContainer'
|
||||||
|
export * from './types'
|
||||||
|
|
||||||
|
export * from './decorator/injection'
|
||||||
77
src/di/types.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor'
|
||||||
|
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties'
|
||||||
|
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that designates a particular value as able to be constructed.
|
||||||
|
*/
|
||||||
|
export interface Instantiable<T> {
|
||||||
|
new(...args: any[]): T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given value is instantiable.
|
||||||
|
* @param what
|
||||||
|
*/
|
||||||
|
export function isInstantiable<T>(what: unknown): what is Instantiable<T> {
|
||||||
|
return (
|
||||||
|
Boolean(what)
|
||||||
|
&& (typeof what === 'object' || typeof what === 'function')
|
||||||
|
&& (what !== null)
|
||||||
|
&& 'constructor' in what
|
||||||
|
&& typeof what.constructor === 'function'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type that identifies a value as a static class, even if it is not instantiable.
|
||||||
|
*/
|
||||||
|
export type StaticClass<T, T2> = Function & {prototype: T} & T2 // eslint-disable-line @typescript-eslint/ban-types
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the parameter is a static class.
|
||||||
|
* @param something
|
||||||
|
*/
|
||||||
|
export function isStaticClass<T, T2>(something: unknown): something is StaticClass<T, T2> {
|
||||||
|
return typeof something === 'function' && typeof something.prototype !== 'undefined'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type used to represent a value that can identify a factory in the container.
|
||||||
|
*/
|
||||||
|
export type DependencyKey = Instantiable<any> | StaticClass<any, any> | string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to store dependency requirements by their place in the injectable
|
||||||
|
* target's parameters.
|
||||||
|
*/
|
||||||
|
export interface DependencyRequirement {
|
||||||
|
paramIndex: number,
|
||||||
|
key: DependencyKey,
|
||||||
|
overridden: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to store dependency requirements by the class property they should
|
||||||
|
* be injected into.
|
||||||
|
*/
|
||||||
|
export interface PropertyDependency {
|
||||||
|
key: DependencyKey,
|
||||||
|
property: string | symbol,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to keep track of singleton factory values, by their dependency key.
|
||||||
|
*/
|
||||||
|
export interface InstanceRef {
|
||||||
|
key: DependencyKey,
|
||||||
|
value: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to keep track of the injection type of a class.
|
||||||
|
*/
|
||||||
|
export interface InjectionType {
|
||||||
|
type: 'named' | 'singleton',
|
||||||
|
name?: string,
|
||||||
|
}
|
||||||
0
src/forms/.gitkeep
Normal file
62
src/forms/FormRequest.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {Container, Injectable, InjectParam} from '../di'
|
||||||
|
import {Request} from '../http/lifecycle/Request'
|
||||||
|
import {Valid, ValidationRules} from './rules/types'
|
||||||
|
import {Validator} from './Validator'
|
||||||
|
import {AppClass} from '../lifecycle/AppClass'
|
||||||
|
import {DataContainer} from '../http/lifecycle/Request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for defining reusable validators for request routes.
|
||||||
|
* If instantiated with a container, it must be a request-level container,
|
||||||
|
* but the type interface allows any data-container to be used when creating
|
||||||
|
* manually.
|
||||||
|
*
|
||||||
|
* You should mark implementations of this class as singleton to avoid
|
||||||
|
* re-validating the input data every time it is accessed.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Instantiate with the request:
|
||||||
|
* const data = <MyFormRequest> request.make(MyFormRequest)
|
||||||
|
*
|
||||||
|
* // Instantiate with some container:
|
||||||
|
* const data = new MyFormRequest(someDataContainer)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export abstract class FormRequest<T> extends AppClass {
|
||||||
|
/** The cached validation result. */
|
||||||
|
protected cachedResult?: Valid<T>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectParam(Request)
|
||||||
|
protected readonly data: DataContainer,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected container(): Container {
|
||||||
|
return (this.data as unknown) as Container
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validation rules that should be applied to the request to guarantee
|
||||||
|
* that it contains the given data type.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected abstract getRules(): ValidationRules | Promise<ValidationRules>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and get the request input. Throws a validation error on fail.
|
||||||
|
* Internally, caches the result after the first validation. So, singleton
|
||||||
|
* validators will avoid re-processing their rules every time.
|
||||||
|
*/
|
||||||
|
public async get(): Promise<Valid<T>> {
|
||||||
|
if ( !this.cachedResult ) {
|
||||||
|
const validator = <Validator<T>> this.make(Validator, await this.getRules())
|
||||||
|
this.cachedResult = await validator.validate(this.data.input())
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cachedResult
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/forms/Validator.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from './rules/types'
|
||||||
|
import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from '../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error thrown thrown when an object fails its validation.
|
||||||
|
*/
|
||||||
|
export class ValidationError<T> extends ErrorWithContext {
|
||||||
|
constructor(
|
||||||
|
/** The original input data. */
|
||||||
|
public readonly data: unknown,
|
||||||
|
|
||||||
|
/** The validator instance used. */
|
||||||
|
public readonly validator: Validator<T>,
|
||||||
|
|
||||||
|
/** Validation error messages, by field. */
|
||||||
|
public readonly errors: Messages,
|
||||||
|
) {
|
||||||
|
super('One or more fields were invalid.', { data,
|
||||||
|
messages: errors.all() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to validate arbitrary data using functional rules.
|
||||||
|
*/
|
||||||
|
export class Validator<T> {
|
||||||
|
constructor(
|
||||||
|
/** The rules used to validate input objects. */
|
||||||
|
protected readonly rules: ValidationRules,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to validate the input data.
|
||||||
|
* If it is valid, it is type aliased as Valid<T>.
|
||||||
|
* If it is invalid, a ValidationError is thrown.
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
public async validate(data: unknown): Promise<Valid<T>> {
|
||||||
|
const messages = await this.validateAndGetErrors(data)
|
||||||
|
if ( messages.any() ) {
|
||||||
|
throw new ValidationError<T>(data, this, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as Valid<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given data is valid and type aliases it as Valid<T>.
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
public async isValid(data: unknown): Promise<boolean> {
|
||||||
|
return !(await this.validateAndGetErrors(data)).any()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the validation rules to the data object and return any error messages.
|
||||||
|
* @param data
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async validateAndGetErrors(data: unknown): Promise<Messages> {
|
||||||
|
const messages = new Messages()
|
||||||
|
const params: ValidatorFunctionParams = { data }
|
||||||
|
|
||||||
|
for ( const key in this.rules ) {
|
||||||
|
if ( !Object.prototype.hasOwnProperty.call(this.rules, key) ) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// This walks over all of the values in the data structure using the nested
|
||||||
|
// key notation. It's not type-safe, but neither is the original input object
|
||||||
|
// yet, so it's useful here.
|
||||||
|
for ( const walkEntry of dataWalkUnsafe<any>(data as any, key) ) {
|
||||||
|
let [entry, dataKey] = walkEntry // eslint-disable-line prefer-const
|
||||||
|
const rules = (Array.isArray(this.rules[key]) ? this.rules[key] : [this.rules[key]]) as ValidatorFunction[]
|
||||||
|
|
||||||
|
for ( const rule of rules ) {
|
||||||
|
const result: ValidationResult = await rule(dataKey, entry, params)
|
||||||
|
|
||||||
|
if ( !result.valid ) {
|
||||||
|
let errors = ['is invalid']
|
||||||
|
|
||||||
|
if ( Array.isArray(result.message) && result.message.length ) {
|
||||||
|
errors = result.message
|
||||||
|
} else if ( !Array.isArray(result.message) && result.message ) {
|
||||||
|
errors = [result.message]
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const error of errors ) {
|
||||||
|
if ( !messages.has(dataKey, error) ) {
|
||||||
|
messages.put(dataKey, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( result.valid && result.castValue ) {
|
||||||
|
entry = result.castValue
|
||||||
|
data = dataSetUnsafe(dataKey, entry, data as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( result.stopValidation ) {
|
||||||
|
break // move on to the next field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/forms/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export * from './rules/types'
|
||||||
|
export * as Rule from './rules/rules'
|
||||||
|
|
||||||
|
export * from './unit/Forms'
|
||||||
|
|
||||||
|
export * from './Validator'
|
||||||
|
export * from './FormRequest'
|
||||||
|
|
||||||
|
export * from './middleware'
|
||||||
34
src/forms/middleware.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {Instantiable} from '../di'
|
||||||
|
import {FormRequest} from './FormRequest'
|
||||||
|
import {ValidationError} from './Validator'
|
||||||
|
import {ResponseObject, RouteHandler} from '../http/routing/Route'
|
||||||
|
import {Request} from '../http/lifecycle/Request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a middleware function that validates a request's input against
|
||||||
|
* the given form request class and registers the FormRequest class into
|
||||||
|
* the request container.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* Route.group(...).pre(formRequest(MyFormRequestClass))
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param formRequestClass
|
||||||
|
*/
|
||||||
|
export function formRequest<T>(formRequestClass: Instantiable<FormRequest<T>>): RouteHandler {
|
||||||
|
return async function formRequestRouteHandler(request: Request): Promise<ResponseObject> {
|
||||||
|
const formRequestInstance = <FormRequest<T>> request.make(formRequestClass)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRequestInstance.get()
|
||||||
|
request.registerSingletonInstance<FormRequest<T>>(formRequestClass, formRequestInstance)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if ( e instanceof ValidationError ) {
|
||||||
|
return e.errors.toJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src/forms/rules/arrays.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import {ValidationResult, ValidatorFunction} from './types'
|
||||||
|
|
||||||
|
/** Requires the input value to be an array. */
|
||||||
|
function is(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( Array.isArray(inputValue) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be an array',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the values in the input value array to be distinct. */
|
||||||
|
function distinct(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
const arr = is(fieldName, inputValue)
|
||||||
|
if ( !arr.valid ) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray(inputValue) && (new Set(inputValue)).size === inputValue.length ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must not contain duplicate values',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input array to contain the given value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function includes(value: unknown): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
const arr = is(fieldName, inputValue)
|
||||||
|
if ( !arr.valid ) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray(inputValue) && inputValue.includes(value) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must include ${value}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input array NOT to contain the given value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function excludes(value: unknown): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
const arr = is(fieldName, inputValue)
|
||||||
|
if ( !arr.valid ) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray(inputValue) && !inputValue.includes(value) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must not include ${value}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input array to have exactly `len` many entries.
|
||||||
|
* @param len
|
||||||
|
*/
|
||||||
|
function length(len: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
const arr = is(fieldName, inputValue)
|
||||||
|
if ( !arr.valid ) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray(inputValue) && inputValue.length === len ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be exactly of length ${len}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input array to have at least `len` many entries.
|
||||||
|
* @param len
|
||||||
|
*/
|
||||||
|
function lengthMin(len: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
const arr = is(fieldName, inputValue)
|
||||||
|
if ( !arr.valid ) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray(inputValue) && inputValue.length >= len ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be at least length ${len}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input array to have at most `len` many entries.
|
||||||
|
* @param len
|
||||||
|
*/
|
||||||
|
function lengthMax(len: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
const arr = is(fieldName, inputValue)
|
||||||
|
if ( !arr.valid ) {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray(inputValue) && inputValue.length <= len ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be at most length ${len}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Arr = {
|
||||||
|
is,
|
||||||
|
distinct,
|
||||||
|
includes,
|
||||||
|
excludes,
|
||||||
|
length,
|
||||||
|
lengthMin,
|
||||||
|
lengthMax,
|
||||||
|
}
|
||||||
80
src/forms/rules/inference.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {infer as inferUtil} from '../../util'
|
||||||
|
import {ValidationResult} from './types'
|
||||||
|
|
||||||
|
/** Attempt to infer the native type of a string value. */
|
||||||
|
function infer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Casts the input value to a boolean.
|
||||||
|
* Note that this assumes the value may be boolish. The strings "true", "True",
|
||||||
|
* "TRUE", and "1" evaluate to `true`, while "false", "False", "FALSE", and "0"
|
||||||
|
* evaluate to `false`.
|
||||||
|
* @param fieldName
|
||||||
|
* @param inputValue
|
||||||
|
*/
|
||||||
|
function boolean(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
let castValue = Boolean(inputValue)
|
||||||
|
|
||||||
|
if ( ['true', 'True', 'TRUE', '1'].includes(String(inputValue)) ) {
|
||||||
|
castValue = true
|
||||||
|
}
|
||||||
|
if ( ['false', 'False', 'FALSE', '0'].includes(String(inputValue)) ) {
|
||||||
|
castValue = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
castValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Casts the input value to a string. */
|
||||||
|
function string(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
castValue: String(inputValue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Casts the input value to a number, if it is numerical. Fails otherwise. */
|
||||||
|
function numeric(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( !isNaN(parseFloat(String(inputValue))) ) {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
castValue: parseFloat(String(inputValue)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be numeric',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Casts the input value to an integer. Fails otherwise. */
|
||||||
|
function integer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( !isNaN(parseInt(String(inputValue), 10)) ) {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
castValue: parseInt(String(inputValue), 10),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be an integer',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Cast = {
|
||||||
|
infer,
|
||||||
|
boolean,
|
||||||
|
string,
|
||||||
|
numeric,
|
||||||
|
integer,
|
||||||
|
}
|
||||||
210
src/forms/rules/numeric.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import {ValidationResult, ValidatorFunction} from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to be greater than some value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function greaterThan(value: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( Number(inputValue) > value ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be greater than ${value}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to be at least some value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function atLeast(value: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( Number(inputValue) >= value ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be at least ${value}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to be less than some value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function lessThan(value: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( Number(inputValue) < value ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be less than ${value}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to be at most some value.
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function atMost(value: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( Number(inputValue) <= value ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be at most ${value}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to have exactly `num` many digits.
|
||||||
|
* @param num
|
||||||
|
*/
|
||||||
|
function digits(num: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).replace('.', '').length === num ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must have exactly ${num} digits`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to have at least `num` many digits.
|
||||||
|
* @param num
|
||||||
|
*/
|
||||||
|
function digitsMin(num: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).replace('.', '').length >= num ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must have at least ${num} digits`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to have at most `num` many digits.
|
||||||
|
* @param num
|
||||||
|
*/
|
||||||
|
function digitsMax(num: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).replace('.', '').length <= num ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must have at most ${num} digits`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to end with the given number sequence.
|
||||||
|
* @param num
|
||||||
|
*/
|
||||||
|
function ends(num: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).endsWith(String(num)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must end with "${num}"`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to begin with the given number sequence.
|
||||||
|
* @param num
|
||||||
|
*/
|
||||||
|
function begins(num: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).startsWith(String(num)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must begin with "${num}"`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to be a multiple of the given number.
|
||||||
|
* @param num
|
||||||
|
*/
|
||||||
|
function multipleOf(num: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( parseFloat(String(inputValue)) % num === 0 ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be a multiple of ${num}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be even. */
|
||||||
|
function even(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( parseFloat(String(inputValue)) % 2 === 0 ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be even',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be odd. */
|
||||||
|
function odd(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( parseFloat(String(inputValue)) % 2 === 0 ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be odd',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Num = {
|
||||||
|
greaterThan,
|
||||||
|
atLeast,
|
||||||
|
lessThan,
|
||||||
|
atMost,
|
||||||
|
digits,
|
||||||
|
digitsMin,
|
||||||
|
digitsMax,
|
||||||
|
ends,
|
||||||
|
begins,
|
||||||
|
multipleOf,
|
||||||
|
even,
|
||||||
|
odd,
|
||||||
|
}
|
||||||
191
src/forms/rules/presence.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import {ValidationResult, ValidatorFunction} from './types'
|
||||||
|
import {UniversalPath} from '../../util'
|
||||||
|
|
||||||
|
/** Requires the given input value to be some form of affirmative boolean. */
|
||||||
|
function accepted(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(String(inputValue)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be accepted',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the given input value to be some form of boolean. */
|
||||||
|
function boolean(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0]
|
||||||
|
if ( boolish.includes(String(inputValue)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be true or false',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be of type string. */
|
||||||
|
function string(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( typeof inputValue === 'string' ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be a string',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the given input value to be present and non-nullish. */
|
||||||
|
function required(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'is required',
|
||||||
|
stopValidation: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias of required(). */
|
||||||
|
function present(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return required(fieldName, inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias of required(). */
|
||||||
|
function filled(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return required(fieldName, inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the given input value to be absent or nullish. */
|
||||||
|
function prohibited(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'is not allowed',
|
||||||
|
stopValidation: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias of prohibited(). */
|
||||||
|
function absent(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return prohibited(fieldName, inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias of prohibited(). */
|
||||||
|
function empty(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return prohibited(fieldName, inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the given input to be found in an array of values.
|
||||||
|
* @param values
|
||||||
|
*/
|
||||||
|
function foundIn(values: any[]): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( values.includes(inputValue) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be one of: ${values.join(', ')}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the given input NOT to be found in an array of values.
|
||||||
|
* @param values
|
||||||
|
*/
|
||||||
|
function notFoundIn(values: any[]): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( values.includes(inputValue) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be one of: ${values.join(', ')}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be number-like. */
|
||||||
|
function numeric(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( !isNaN(parseFloat(String(inputValue))) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be numeric',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the given input value to be integer-like. */
|
||||||
|
function integer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( !isNaN(parseInt(String(inputValue), 10)) && parseInt(String(inputValue), 10) === parseFloat(String(inputValue)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be an integer',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the given input value to be a UniversalPath. */
|
||||||
|
function file(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( inputValue instanceof UniversalPath ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be a file',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A special validator function that marks a field as optional.
|
||||||
|
* If the value of the field is nullish, no further validation rules will be applied.
|
||||||
|
* If it is non-nullish, validation will continue.
|
||||||
|
* @param fieldName
|
||||||
|
* @param inputValue
|
||||||
|
*/
|
||||||
|
function optional(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( inputValue ?? true ) {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
stopValidation: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Is = {
|
||||||
|
accepted,
|
||||||
|
boolean,
|
||||||
|
string,
|
||||||
|
required,
|
||||||
|
present,
|
||||||
|
filled,
|
||||||
|
prohibited,
|
||||||
|
absent,
|
||||||
|
empty,
|
||||||
|
foundIn,
|
||||||
|
notFoundIn,
|
||||||
|
numeric,
|
||||||
|
integer,
|
||||||
|
file,
|
||||||
|
optional,
|
||||||
|
}
|
||||||
31
src/forms/rules/provided/DateValidator.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
import {Injectable} from '@extollo/di'
|
||||||
|
import {Validator} from '../Validator'
|
||||||
|
import {ValidationResult} from "../types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DateValidator extends Validator {
|
||||||
|
protected names: string[] = [
|
||||||
|
'date',
|
||||||
|
'date.after',
|
||||||
|
'date.at_least',
|
||||||
|
'date.before',
|
||||||
|
'date.at_most',
|
||||||
|
'date.equals',
|
||||||
|
'date.format',
|
||||||
|
]
|
||||||
|
|
||||||
|
public matchName(name: string): boolean {
|
||||||
|
return this.names.includes(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(fieldName: string, inputValue: any, params: { name: string; params: any }): ValidationResult {
|
||||||
|
switch ( params.name ) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return { valid: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
5
src/forms/rules/rules.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { Arr } from './arrays'
|
||||||
|
export { Cast } from './inference'
|
||||||
|
export { Num } from './numeric'
|
||||||
|
export { Is } from './presence'
|
||||||
|
export { Str } from './strings'
|
||||||
245
src/forms/rules/strings.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import {ValidationResult, ValidatorFunction} from './types'
|
||||||
|
import {isJSON} from '../../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String-related validation rules.
|
||||||
|
*/
|
||||||
|
const regexes: {[key: string]: RegExp} = {
|
||||||
|
'string.is.alpha': /[a-zA-Z]*/,
|
||||||
|
'string.is.alpha_num': /[a-zA-Z0-9]*/,
|
||||||
|
'string.is.alpha_dash': /[a-zA-Z-]*/,
|
||||||
|
'string.is.alpha_score': /[a-zA-Z_]*/,
|
||||||
|
'string.is.alpha_num_dash_score': /[a-zA-Z\-_0-9]*/,
|
||||||
|
'string.is.email': /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])/, // eslint-disable-line no-control-regex
|
||||||
|
'string.is.ip': /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/,
|
||||||
|
'string.is.ip.v4': /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
|
||||||
|
'string.is.ip.v6': /(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))/,
|
||||||
|
'string.is.mime': /^(?=[-a-z]{1,127}\/[-.a-z0-9]{1,127}$)[a-z]+(-[a-z]+)*\/[a-z0-9]+([-.][a-z0-9]+)*$/,
|
||||||
|
'string.is.url': /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w\-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\\w]*))?)/,
|
||||||
|
'string.is.uuid': /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRex(key: string, inputValue: unknown, message: string): ValidationResult {
|
||||||
|
if ( regexes[key].test(String(inputValue)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be alphabetical characters only. */
|
||||||
|
function alpha(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.alpha', inputValue, 'must be alphabetical only')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be alphanumeric characters only. */
|
||||||
|
function alphaNum(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be alphabetical characters or the "-" character only. */
|
||||||
|
function alphaDash(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.alpha_dash', inputValue, 'must be alphabetical and dashes only')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be alphabetical characters or the "_" character only. */
|
||||||
|
function alphaScore(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.alpha_score', inputValue, 'must be alphabetical and underscores only')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be alphabetical characters, numeric characters, "-", or "_" only. */
|
||||||
|
function alphaNumDashScore(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.alpha_num_dash_score', inputValue, 'must be alphanumeric, dashes, and underscores only')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid RFC email address format. */
|
||||||
|
function email(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.email', inputValue, 'must be an email address')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid IPv4 or IPv6 address. */
|
||||||
|
function ip(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.ip', inputValue, 'must be a valid IP address')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid IPv4 address. */
|
||||||
|
function ipv4(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.ip.v4', inputValue, 'must be a valid IP version 4 address')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid IPv6 address. */
|
||||||
|
function ipv6(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.ip.v6', inputValue, 'must be a valid IP version 6 address')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid file MIME type. */
|
||||||
|
function mime(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid RFC URL format. */
|
||||||
|
function url(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.url', inputValue, 'must be a valid URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid RFC UUID format. */
|
||||||
|
function uuid(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
return validateRex('string.is.uuid', inputValue, 'must be a valid UUID')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validation function that requires the input value to match the given regex.
|
||||||
|
* @param rex
|
||||||
|
*/
|
||||||
|
function regex(rex: RegExp): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( rex.test(String(inputValue)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'is not valid',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validation function that requires the input to NOT match the given regex.
|
||||||
|
* @param rex
|
||||||
|
*/
|
||||||
|
function notRegex(rex: RegExp): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( !rex.test(String(inputValue)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'is not valid',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validation function that requires the given input to end with the substring.
|
||||||
|
* @param substr
|
||||||
|
*/
|
||||||
|
function ends(substr: string): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).endsWith(substr) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must end with "${substr}"`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validation function that requires the given input to begin with the substring.
|
||||||
|
* @param substr
|
||||||
|
*/
|
||||||
|
function begins(substr: string): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).startsWith(substr) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must begin with "${substr}"`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requires the input value to be a valid JSON string. */
|
||||||
|
function json(fieldName: string, inputValue: unknown): ValidationResult {
|
||||||
|
if ( isJSON(String(inputValue)) ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'must be valid JSON',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to have exactly len many characters.
|
||||||
|
* @param len
|
||||||
|
*/
|
||||||
|
function length(len: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).length === len ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be exactly of length ${len}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to have at least len many characters.
|
||||||
|
* @param len
|
||||||
|
*/
|
||||||
|
function lengthMin(len: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).length >= len ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be at least length ${len}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a validator function that requires the input value to have at most len many characters.
|
||||||
|
* @param len
|
||||||
|
*/
|
||||||
|
function lengthMax(len: number): ValidatorFunction {
|
||||||
|
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||||
|
if ( String(inputValue).length <= len ) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `must be at most length ${len}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Str = {
|
||||||
|
alpha,
|
||||||
|
alphaNum,
|
||||||
|
alphaDash,
|
||||||
|
alphaScore,
|
||||||
|
alphaNumDashScore,
|
||||||
|
email,
|
||||||
|
ip,
|
||||||
|
ipv4,
|
||||||
|
ipv6,
|
||||||
|
mime,
|
||||||
|
url,
|
||||||
|
uuid,
|
||||||
|
regex,
|
||||||
|
notRegex,
|
||||||
|
ends,
|
||||||
|
begins,
|
||||||
|
json,
|
||||||
|
length,
|
||||||
|
lengthMin,
|
||||||
|
lengthMax,
|
||||||
|
}
|
||||||
85
src/forms/rules/types.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Additional parameters passed to complex validation functions.
|
||||||
|
*/
|
||||||
|
export interface ValidatorFunctionParams {
|
||||||
|
/** The entire original input data. */
|
||||||
|
data: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface representing the result of an attempted validation that failed.
|
||||||
|
*/
|
||||||
|
export interface ValidationErrorResult {
|
||||||
|
/** Whether or not the validation succeeded. */
|
||||||
|
valid: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The human-readable error message(s) describing the issue.
|
||||||
|
*/
|
||||||
|
message?: string | string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, validation of subsequent fields will stop.
|
||||||
|
*/
|
||||||
|
stopValidation?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface representing the result of an attempted validation that succeeded.
|
||||||
|
*/
|
||||||
|
export interface ValidationSuccessResult {
|
||||||
|
/** Whether or not the validation succeeded. */
|
||||||
|
valid: true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the value was cast to a different type, or inferred, as a result of this validation,
|
||||||
|
* provide it here. It will replace the input string as the value of the field in the form.
|
||||||
|
*/
|
||||||
|
castValue?: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, validation of subsequent fields will stop.
|
||||||
|
*/
|
||||||
|
stopValidation?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All possible results of an attempted validation. */
|
||||||
|
export type ValidationResult = ValidationErrorResult | ValidationSuccessResult
|
||||||
|
|
||||||
|
/** A validator function that takes only the field key and the object value. */
|
||||||
|
export type SimpleValidatorFunction = (fieldName: string, inputValue: any) => ValidationResult | Promise<ValidationResult>
|
||||||
|
|
||||||
|
/** A validator function that takes the field key, the object value, and an object of contextual params. */
|
||||||
|
export type ComplexValidatorFunction = (fieldName: string, inputValue: any, params: ValidatorFunctionParams) => ValidationResult | Promise<ValidationResult>
|
||||||
|
|
||||||
|
/** Useful type alias for all allowed validator function signatures. */
|
||||||
|
export type ValidatorFunction = SimpleValidatorFunction | ComplexValidatorFunction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of validation rules that are applied to input objects on validators.
|
||||||
|
*
|
||||||
|
* The keys of this object are deep-nested keys and can be used to validate
|
||||||
|
* nested properties.
|
||||||
|
*
|
||||||
|
* For example, the key "user.links.*.url" refers to the "url" property of all
|
||||||
|
* objects in the "links" array on the "user" object on:
|
||||||
|
*
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* "user": {
|
||||||
|
* "links": [
|
||||||
|
* {
|
||||||
|
* "url": "..."
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "url": "..."
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export type ValidationRules = {[key: string]: ValidatorFunction | ValidatorFunction[]}
|
||||||
|
|
||||||
|
/** A type alias denoting that a particular type has been validated. */
|
||||||
|
export type Valid<T> = T
|
||||||
44
src/forms/templates/form.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {Template} from '../../cli'
|
||||||
|
|
||||||
|
const templateForm: Template = {
|
||||||
|
name: 'form',
|
||||||
|
fileSuffix: '.form.ts',
|
||||||
|
description: 'Create a new form request validator',
|
||||||
|
baseAppPath: ['http', 'forms'],
|
||||||
|
render(name: string) {
|
||||||
|
return `import {Injectable, FormRequest, ValidationRules, Rule} from '@extollo/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${name} object
|
||||||
|
* ----------------------------
|
||||||
|
* This is the object interface that is guaranteed when
|
||||||
|
* all of the validation rules below pass.
|
||||||
|
*/
|
||||||
|
export interface ${name}Form {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${name}Request validator
|
||||||
|
* ----------------------------
|
||||||
|
* Request validator that defines the rules needed to guarantee
|
||||||
|
* that a request's input conforms to the interface defined above.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ${name}FormRequest extends FormRequest<${name}Form> {
|
||||||
|
/**
|
||||||
|
* The validation rules that should be applied to the various
|
||||||
|
* request input fields.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected getRules(): ValidationRules {
|
||||||
|
return {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export { templateForm }
|
||||||
18
src/forms/unit/Forms.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {Singleton, Inject} from '../../di'
|
||||||
|
import {CommandLine} from '../../cli'
|
||||||
|
import {templateForm} from '../templates/form'
|
||||||
|
import {Unit} from '../../lifecycle/Unit'
|
||||||
|
import {Logging} from '../../service/Logging'
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class Forms extends Unit {
|
||||||
|
@Inject()
|
||||||
|
protected readonly cli!: CommandLine
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
public async up(): Promise<void> {
|
||||||
|
this.cli.registerTemplate(templateForm)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ErrorWithContext, HTTPStatus, HTTPMessage} from "@extollo/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 "@extollo/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]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from '../lifecycle/Request'
|
||||||
import {uninfer, infer, uuid_v4} from "@extollo/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('='))
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {Inject, Instantiable, Singleton} from "@extollo/di"
|
import {Inject, Instantiable, Singleton} from '../../di'
|
||||||
import {Collection, HTTPStatus} from "@extollo/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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Injectable} from "@extollo/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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 "@extollo/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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||||
import {Injectable} from "@extollo/di"
|
import {Injectable} from '../../../di'
|
||||||
import {ErrorWithContext} from "@extollo/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)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {Injectable, Inject} from "@extollo/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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 "@extollo/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 "@extollo/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())
|
.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')
|
||||||
@@ -49,6 +52,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the request body as JSON.
|
||||||
|
* @param request
|
||||||
|
*/
|
||||||
public async applyJSON(request: Request): Promise<Request> {
|
public async applyJSON(request: Request): Promise<Request> {
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
let data = ''
|
let data = ''
|
||||||
@@ -61,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()
|
||||||
@@ -74,6 +84,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the request body using Busboy. This assumes the request contents are multipart.
|
||||||
|
* @param request
|
||||||
|
*/
|
||||||
public async applyBusboy(request: Request): Promise<Request> {
|
public async applyBusboy(request: Request): Promise<Request> {
|
||||||
const config = this.config.get('server.uploads', {})
|
const config = this.config.get('server.uploads', {})
|
||||||
|
|
||||||
@@ -86,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.`)
|
||||||
@@ -114,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
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||||
import {Injectable} from "@extollo/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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 "@extollo/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'))
|
||||||
}
|
}
|
||||||
|
|||||||