diff --git a/.gitignore b/.gitignore index 2a9711974328c5489ada510619dd37e601b87851..c5270de7fdc27533332330e0fc16c767c7176a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules /coverage -/dist \ No newline at end of file +/dist +/public \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fabc22d601d6649adc6436c0411dccbf213ee939..de49dbb82537fe0b1ce6bb4511884e9a660f4f19 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ image: node stages: - build - test -# - deploy # deployment is disabled for now + - deploy # deployment is enabled only for pages cache: - &global_cache_node_modules @@ -35,6 +35,7 @@ install-dependencies: build: # This job runs in the build stage, which runs first. stage: build + environment: production artifacts: untracked: false when: on_success @@ -61,10 +62,35 @@ lint-test-job: # This job also runs in the test stage. stage: test # It can run at the same time as unit-test-job (in parallel). script: - npx prettier . --check + +pages-test: + stage: test + script: + - npx typedoc src/index.ts --out test-pages + artifacts: + when: on_success + access: all + expire_in: 30 days + paths: + - test-pages + rules: + - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH # this job will be executed only on seconday branches + +pages: + stage: deploy + script: + - npx typedoc src/index.ts --out public + artifacts: + paths: + - public + when: on_success + rules: + - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH # This job is executed only on the main branch + #deploy-job: # This job runs in the deploy stage. # stage: deploy # It only runs when *both* jobs in the test stage complete successfully. # environment: production -# when: on-success +# when: on_success # - job: build # artifacts: true # script: diff --git a/README.md b/README.md index 94e0d6bac7b040b0f5226927e716df7ec416ee70..fbd3e457c12d334945b4303521e782e61e2f76f2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +![GitLab License](https://img.shields.io/gitlab/license/worklog1%2Fcream) +![Gitlab Pipeline Status](https://img.shields.io/gitlab/pipeline-status/worklog1%2Fcream?branch=master) +![NPM Downloads](https://img.shields.io/npm/dy/%40creamapi%2Fcream) +![GitLab Issues](https://img.shields.io/gitlab/issues/open/worklog1%2Fcream) +![GitLab Contributors](https://img.shields.io/gitlab/contributors/worklog1%2Fcream) +![NPM Version](https://img.shields.io/npm/v/%40creamapi%2Fcream) + +Buy Me A Coffee + # Cream - A Library For Semi-Declarative REST API Creation Cream stands for Concise REST API Maker and it is a ExpressJS extension mainly targeting TypeScript builds. @@ -7,13 +16,13 @@ It wasn't tested on plain JS. If express is not installed: -```npm +```bash npm install express @types/express @creamapi/cream ``` If you've already installed expreess: -```npm +```bash npm install @creamapi/cream ``` @@ -84,7 +93,7 @@ class MyCustomApplication extends ExpressApplication { public constructor() { let expressApp = express(); /* - here you can use any express middleware like cors, json, bodyParser + here you can use any express middleware like cors, json, bodyParser, morgan, etc. */ expressApp.use(express.json()); @@ -108,7 +117,7 @@ Now if we go to https://localhost:4040/hello-world we will see Sending a string to the browser is cool, but REST APIs are more complex than this. They can receive data as a request and give a complex response, like a JSON text. -## Handling data coming from the client +### Handling data coming from the client Let's reuse the last example, but this time we want to get a string from the client and write it on the screen. For this example, to keep it simple, we will use a Get request again, but this time we will use a UrlParameter to retrive the data. What does it mean? It means that when the user makes a request to http://localhost:4040/hello-world/\ we want to get the value of \ and write it back to the user. @@ -131,7 +140,7 @@ export class HelloController extends ExpressModule { Now if we try to go to http://localhost:4040/hello-world/my%20hello we will see `my hello` written in our browser! -## Returning complex objects +### Returning complex objects Now we want to return a json object containing both our string and its length. To do so we must create a custom class that contains such data and tell cream that we want to serialize it to JSON. We can do it like this: @@ -152,13 +161,16 @@ import { @Serializable(CreamSerializers.JSON) class HelloView { @AutoMap - stringLength: number; + get stringLength(): number { + return this.stringData.length; + } @MapTo('userData') - stringData: string; + public stringData: string; + + otherData: number; constructor(userString) { - this.stringLength = stringData.length; this.stringData = stringData; } } @@ -168,8 +180,14 @@ Here we can see that we tell cream that HelloView is serializable by a JSON seri We also see AutoMap and MapTo, these two decorators are used to declare which fields are serialized. +> Non-decorated fields, like otherData, are not serialized by default. +> This behaviov is helpful to prevent unwanted dataleaks. With a serialize +> all by default behavior a secure field can be leaked, for example the user's password. + > The difference between MapTo and AutoMap is that MapTo allows us to specify the name of the field whilst AutoMap will take the name of the decorated attribute. +We can see that we can also serialize getters. This allows us to compute dynamically stuff when the object is serializable. Also, `this` correctly points to the correct object. + Now we want to use our custom data. As before let's reuse the last example as a base: ```ts @@ -190,14 +208,14 @@ Now if we go again to http://localhost:4040/hello-world/my%20hello we will not s ```json { - "userData": "my hello", - "stringLength": 8 + "stringLength": 8, + "userData": "my hello" } ``` ## Continuing -To expand our REST API we also need to receive more complex data from the user, but this topic, how to handle different HTTP requests, is covered in the [User Guide](). +To expand our REST API we also need to receive more complex data from the user, but this topic, how to handle different HTTP requests, is covered in the [User Guide](public/index.html). # Comparing it with Express diff --git a/package-lock.json b/package-lock.json index 09246b83d60dcc03fcf103c7d5d11a0f97008d49..f97f846c67c0bb588f6e03d150ed8d9b7fb0326d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "prettier": "3.3.3", "supertest": "^7.0.0", "ts-jest": "^29.2.5", + "typedoc": "^0.26.11", "typescript": "^5.6.3" } }, @@ -946,6 +947,62 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@shikijs/core": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.23.0.tgz", + "integrity": "sha512-J4Fo22oBlfRHAXec+1AEzcowv+Qdf4ZQkuP/X/UHYH9+KA9LvyFXSXyS+HxuBRFfon+u7bsmKdRBjoZlbDVRkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.23.0", + "@shikijs/engine-oniguruma": "1.23.0", + "@shikijs/types": "1.23.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.23.0.tgz", + "integrity": "sha512-CcrppseWShG+8Efp1iil9divltuXVdCaU4iu+CKvzTGZO5RmXyAiSx668M7VbX8+s/vt1ZKu75Vn/jWi8O3G/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.23.0", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-es": "0.1.2" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.23.0.tgz", + "integrity": "sha512-gS8bZLqVvmZXX+E5JUMJICsBp+kx6gj79MH/UEpKHKIqnUzppgbmEn6zLa6mB5D+sHse2gFei3YYJxQe1EzZXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.23.0", + "@shikijs/vscode-textmate": "^9.3.0" + } + }, + "node_modules/@shikijs/types": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.23.0.tgz", + "integrity": "sha512-HiwzsihRao+IbPk7FER/EQT/D0dEEK3n5LAtHDzL5iRT+JMblA7y9uitUnjEnHeLkKigNM+ZplrP7MuEyyc5kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1082,6 +1139,16 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -1127,6 +1194,16 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1219,6 +1296,13 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -1236,6 +1320,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1650,6 +1741,17 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1677,6 +1779,28 @@ "node": ">=10" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1766,6 +1890,17 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -1947,6 +2082,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -1968,6 +2113,20 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -2039,6 +2198,13 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2049,6 +2215,19 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2588,6 +2767,44 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hexoid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", @@ -2605,6 +2822,17 @@ "dev": true, "license": "MIT" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3582,6 +3810,16 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3612,6 +3850,13 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -3658,6 +3903,60 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3695,6 +3994,100 @@ "node": ">= 0.6" } }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", + "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3881,6 +4274,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-to-es": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.1.2.tgz", + "integrity": "sha512-sBYKVJlIMB0WPO+tSu/NNB1ytSFeHyyJZ3Ayxfx3f/QUuXu0lvZk0VB4K7npmdlHSC0ldqanzh/sUSlAbgCTfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^4.4.0", + "regex-recursion": "^4.1.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4110,6 +4515,17 @@ "node": ">= 6" } }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4124,6 +4540,16 @@ "node": ">= 0.10" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -4196,6 +4622,30 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "license": "Apache-2.0" }, + "node_modules/regex": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.4.0.tgz", + "integrity": "sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regex-recursion": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-4.2.1.tgz", + "integrity": "sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4401,6 +4851,21 @@ "node": ">=8" } }, + "node_modules/shiki": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.23.0.tgz", + "integrity": "sha512-xfdu9DqPkIpExH29cmiTlgo0/jBki5la1Tkfhsv+Wu5TT3APLNHslR1acxuKJOCWqVdSc+pIbs/2ozjVRGppdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.23.0", + "@shikijs/engine-javascript": "1.23.0", + "@shikijs/engine-oniguruma": "1.23.0", + "@shikijs/types": "1.23.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -4465,6 +4930,17 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4524,6 +5000,21 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4714,6 +5205,17 @@ "node": ">=0.6" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-jest": { "version": "29.2.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", @@ -4813,6 +5315,55 @@ "node": ">= 0.6" } }, + "node_modules/typedoc": { + "version": "0.26.11", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.11.tgz", + "integrity": "sha512-sFEgRRtrcDl2FxVP58Ze++ZK2UQAEvtvvH8rRlig1Ja3o7dDaMHmaBfvJmdGnNEFaLTpQsN8dpvZaTqJSu/Ugw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.16.2", + "yaml": "^2.5.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", @@ -4827,6 +5378,13 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -4834,6 +5392,79 @@ "dev": true, "license": "MIT" }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4910,6 +5541,36 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -4992,6 +5653,19 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -5033,6 +5707,17 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 15bc7b7b8194782c950de545c26638b132ca6538..b955584f91d0ec5602013a520fc9668d55282196 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,13 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "directories": { - "doc": "docs" + "doc": "public" }, "scripts": { "build": "tsc", - "test": "jest" + "docs": "typedoc src/index.ts --out public", + "test": "jest", + "deploy": "npm run build && npm publish" }, "repository": { "type": "git", @@ -26,11 +28,11 @@ "typescript" ], "author": "Raul Radu", - "license": "SEE LICENSE IN LICENSE", + "license": "Apache-2.0", "bugs": { - "url": "https://gitlab.com/worklog1/express-utils/issues" + "url": "https://gitlab.com/worklog1/cream/issues" }, - "homepage": "https://gitlab.com/worklog1/express-utils#readme", + "homepage": "https://gitlab.com/worklog1/cream/public", "devDependencies": { "@types/express": "^5.0.0", "@types/jest": "^29.5.14", @@ -41,6 +43,7 @@ "prettier": "3.3.3", "supertest": "^7.0.0", "ts-jest": "^29.2.5", + "typedoc": "^0.26.11", "typescript": "^5.6.3" }, "dependencies": { diff --git a/src/ExchangeUtils/Message.ts b/src/ExchangeUtils/Message.ts index 35390770ae5957d1ef10aa69fe67764afc987d66..accea52733838626e91a8230d71f266fb132f361 100644 --- a/src/ExchangeUtils/Message.ts +++ b/src/ExchangeUtils/Message.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +/** + * This is a collection of mime types and should be updgraded + * or better deduced from another existing type + */ export type MessageType = | 'application/json' | 'image/jpeg' diff --git a/src/ExpressAdapter/ExpressAdapters.ts b/src/ExpressAdapter/ExpressAdapters.ts index b2865e80ec891e64022c0d65a008189f6ec6d583..0280582cbe4d0f172e428ff78072bb0baec1a6c5 100644 --- a/src/ExpressAdapter/ExpressAdapters.ts +++ b/src/ExpressAdapter/ExpressAdapters.ts @@ -40,6 +40,39 @@ export const PARAMS_METADATA_KEY = Symbol('express:paramAssoc'); export const HEADERS_METADATA_KEY = Symbol('express:headersAssoc'); export const MIDDLEWARE_METADATA_KEY = Symbol('express:middlewareAssoc'); +/** + * This method is used to declare a method of a class (that must extend {@link ExpressModule}) + * to be an API endpoint. This endpoint is bound to the router representing the class basepoint. + * The basepoint (or zone) is defined by using {@link ExpressController} decorator.\ + * The decorated method is not altered and that means it can be used as a normal method. + * + * @remarks + * It is suggested to use {@link Get}, {@link Post}, {@link Put} and {@link Delete}\ + * Methods are bound to the router in a topdown approach, this means that from Express's point of + * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù + * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. + * @example + * ```ts + * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; + * + * \@ExpressController("/my-base-route") + * export class MyController extends ExpressModule { + * \@ExpressCall("/hello-world", HttpMethod.GET) + * myMethod(): string{ + * return "hello, world"; + * } + * + * // It also works with asynchronous methods + * \@ExpressCall("/hello-world-async", HttpMethod.GET) + * async myMethodAsync(): Promise { + * return "hello, async world!"; + * } + * } + * ``` + * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition + * @param httpMethod The HTTP Method that must be used for the path. See {@link HttpMethod} for available methods + * @returns returns the descriptor of the method. + */ export function ExpressCall( relativePath: string, httpMethod: HttpMethod @@ -110,7 +143,7 @@ export function ExpressCall( collections.get(param.collection); if (collection) { - if (param.name == '$') { + if (param.name == '*') { args[param.index] = collection; } else { args[param.index] = (collection as any)[ @@ -174,22 +207,198 @@ export function ExpressCall( }; } +/** + * This method is used to declare a method of a class (that must extend {@link ExpressModule}) + * to be an API endpoint as a GET http method. + * + * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter + * To understand its behaviour please see {@link ExpressCall}. + * + * @remarks Methods are bound to the router in a topdown approach, this means that from Express's point of + * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù + * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. + * @example + * ```ts + * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; + * + * \@ExpressController("/my-base-route") + * export class MyController extends ExpressModule { + * \@Get("/hello-world") + * myMethod(): string{ + * return "hello, world"; + * } + * + * // It also works with asynchronous methods + * \@Get("/hello-world-async") + * async myMethodAsync(): Promise { + * return "hello, async world!"; + * } + * } + * ``` + * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition + * @returns returns the descriptor of the method. + */ export function Get(relativePath: string) { return ExpressCall(relativePath, HttpMethod.GET); } +/** + * This method is used to declare a method of a class (that must extend {@link ExpressModule}) + * to be an API endpoint as a POST http method. + * + * To retrieve the body passed as an argument just use {@link Body} or {@link BodyField}. + * + * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter + * To understand its behaviour please see {@link ExpressCall}. + * + * @remarks + * Methods are bound to the router in a topdown approach, this means that from Express's point of + * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù + * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. + * + * @example + * ```ts + * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; + * + * \@ExpressController("/my-base-route") + * export class MyController extends ExpressModule { + * \@Post("/hello-world") + * myMethod(@Body body: any): string{ + * return "hello, world"; + * } + * + * // It also works with asynchronous methods + * \@Post("/hello-world-async") + * async myMethodAsync(@BodyField("myField") myField: string): Promise { + * return myField; + * } + * } + * ``` + * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition + * @returns returns the descriptor of the method. + */ export function Post(relativePath: string) { return ExpressCall(relativePath, HttpMethod.POST); } +/** + * This method is used to declare a method of a class (that must extend {@link ExpressModule}) + * to be an API endpoint as a PUT http method. + * + * To retrieve the body passed as an argument just use {@link Body} or {@link BodyField}. + * + * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter + * To understand its behaviour please see {@link ExpressCall}. + * + * @remarks Methods are bound to the router in a topdown approach, this means that from Express's point of + * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù + * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. + * @example + * ```ts + * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; + * + * \@ExpressController("/my-base-route") + * export class MyController extends ExpressModule { + * \@Put("/hello-world") + * myMethod(@Body body: any): string{ + * return "hello, world"; + * } + * + * // It also works with asynchronous methods + * \@Put("/hello-world-async") + * async myMethodAsync(@BodyField("myField") myField: string): Promise { + * return myField; + * } + * } + * ``` + * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition + * @returns returns the descriptor of the method. + */ export function Put(relativePath: string) { return ExpressCall(relativePath, HttpMethod.PUT); } +/** + * This method is used to declare a method of a class (that must extend {@link ExpressModule}) + * to be an API endpoint as a DELETE http method. + * + * To retrieve the body passed as an argument just use {@link Body} or {@link BodyField}. + * + * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter + * To understand its behaviour please see {@link ExpressCall}. + * + * @remarks + * Methods are bound to the router in a topdown approach, this means that from Express's point of + * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù + * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. + * + * @example + * ```ts + * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; + * + * \@ExpressController("/my-base-route") + * export class MyController extends ExpressModule { + * \@Delete("/hello-world") + * myMethod(@Body body: any): string{ + * return "hello, world"; + * } + * + * // It also works with asynchronous methods + * \@Delete("/hello-world-async") + * async myMethodAsync(@BodyField("myField") myField: string): Promise { + * return myField; + * } + * } + * ``` + * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition + * @returns returns the descriptor of the method. + */ export function Delete(relativePath: string) { return ExpressCall(relativePath, HttpMethod.DELETE); } +/** + * This decorator is used to make a class to be a controller that handles HTTP requests. + * The class decorated as a controller must inherit from {@link ExpressModule}. + * In practice this will bound any {@link ExpressCall}-decorated method to an express router. + * The router is also bound to the baseRoute parameter.\ + * The call tree will look something like this: + * ``` + * / <- this is the basepoint (parameter baseRoute) a controller is bound to + * |- router1 <- This is one controller + * | |- GET + * | | |- /path1 + * | | | |- method1-1 <- this is a endpoint + * | | | |- method1-2 <- multiple methods can be bound to the same route (aka they collide) + * | | |- /path2 + * | | | |- method1-3 + * | | |- / <- this will look like it is bound to the base path + * | | |- method1-4 + * | |- POST + * | |- PUT + * | |- DELETE + * |- router2 <- multiple controller can be bound to the same basepoint + * | |- GET + * | | |- /path1 + * | | |- method2-1 <- this method is bound to the same path as method1-1 + * | |- POST + * | |- PUT + * | |- DELETE + * |- /new-endpoint <- this is another basepoint. Any method bound to this method will be bound to the base path /new-endpoint + * | |- router1 + * | |- GET + * | |- POST + * | |- PUT + * | |- DELETE + * ``` + * @remarks + * For whom want to work on low lever prototyping this decorator will alter the prototype of the + * decorated class by adding functionalities without altering its behavior, including the constructor. + * @param baseRoute the URL to which the controller is bound to + * @returns a new class that extends the base decorated class that implements a few functions + * that will bound routes to the router. Albeit it is a brand new class its usage is completely + * transparent for the users. + */ export function ExpressController< T extends { new (...args: any[]): ExpressModule }, >(baseRoute: string) { @@ -246,6 +455,15 @@ export function ExpressController< }; } +/** + * This parameter decorator will decorate a method parameter by associating it with a field in the body. + * This permits the autofill of the parameter with the corresponding field (named fieldName) in the body. + * @remarks + * If the field is undefined the field will be filled as undefined.\ + * If no body is provided to the request then all parameters decorated with BodyField will be undefined + * @param fieldName the field name in the body + * @returns the decorator function + */ export function BodyField(fieldName: string) { return function ( target: Object, @@ -267,10 +485,37 @@ export function BodyField(fieldName: string) { }; } +/** + * This parameter decorator will decorate a method parameter by associating it with the whole body. + * If no body is provided to the request then all attributes decorated with Body will be undefined + * @returns the decorator function + */ export function Body() { return BodyField(':body'); } +/** + * This parameter decorator will decorate a method parameter by associating it with a field in the URL. + * This field must be defined in the url like normally done in express. + * @example + * ```ts + * // we are in a controller class + * + * \@Get("/concat-space/:myParam1/:myParam2") + * concatWithSpace( + * \@UrlParameter("myParam1") param1: string, + * \@UrlParameter("myParam2") param2: string + * ): string { + * return param1 + " " + param2; + * } + * //... + * ``` + * @remarks If the field is undefined in the URL request the field will be filled as undefined.\ + * In general parameters are non-null because missing one parameter when making the request + * will result to a different HTTP call and by extension a different controller method. + * @param fieldName the field name in the body + * @returns the decorator function + */ export function UrlParameter(fieldName: string) { return function ( target: Object, @@ -292,6 +537,12 @@ export function UrlParameter(fieldName: string) { }; } +/** + * This parameter decorator will decorate a method parameter by associating it with a Request Header (eg. content-type). + * @remarks If the header is undefined the field will be filled as undefined. + * @param headerName the http header name + * @returns the decorator function + */ export function Header(headerName: string) { return function ( target: Object, diff --git a/src/ExpressAdapter/ExpressModule.ts b/src/ExpressAdapter/ExpressModule.ts index d802db231187ab35bde5559c9af9ef082b132a65..becfd411b08c32b789e035a45d9d1fa7ef2a8798 100644 --- a/src/ExpressAdapter/ExpressModule.ts +++ b/src/ExpressAdapter/ExpressModule.ts @@ -18,12 +18,44 @@ import { Router } from 'express'; import { BaseMiddlewares } from '../ExpressMiddleware/ExpressMiddleware'; import { ExpressApplication } from '../ExpressApplication'; +/** + * This class is just a way to allow to explicitly declare + * that the class inheriting ExpressModule is a controller. + * This is because {@link ExpressController} decorates only + * **ExpressModules** and defines some fields that are used by + * the ExpressApplication to handle requests and define endpoints. + * Also it gives an interface for accessing the express application + * handling the class easily (by using ExpressModule.app) + */ export class ExpressModule { + /** + * This allows to access the express router that will handle + * the requests. This router will be registered to the + * {@link ExpressApplication} that will be defined in {@link ExpressModule.app} + */ public accessor router: Router; + + /** + * This is the basepoint to which the controller is bound to + */ public accessor baseUrl: string; + + /** + * This is used to keep the information about the class + * that will inherit the ExpressModule and that will be + * decorated by {@link ExpressController} + */ public accessor className: string; + + /** + * The {@link ExpressApplication} the controller is registered to + */ private _app!: ExpressApplication; + /** + * @param middlewareList The list of controller-wise middlewares associated + * with the controller. The method-associated middlewares will not appear in this list + */ constructor(public middlewareList: BaseMiddlewares = []) { this.router = Router(); this.baseUrl = '/'; diff --git a/src/ExpressAdapter/ParameterProp.ts b/src/ExpressAdapter/ParameterProp.ts index efb7b0053549f2f18d10b744c68557e34092a5ab..41c65832729ae6f79b523579624ddcdc87061b3c 100644 --- a/src/ExpressAdapter/ParameterProp.ts +++ b/src/ExpressAdapter/ParameterProp.ts @@ -14,7 +14,18 @@ * limitations under the License. */ +/** + * This class is used to defined meta information about + * method parameters. + * It is used to define bindings between the target method + * and the data source (for example headers, body, ecc.) + */ export class ParameterProp { + /** + * + * @param index the index of the parameter in the function call + * @param name the name of the parameter in the resource data structure + */ constructor( public index: number, public name: string diff --git a/src/ExpressApplication.ts b/src/ExpressApplication.ts index 7a1eb4278dc00f76ce4563a9749c53cfdd67df85..3594fc80586fe63ae99e0a51228ea321359512fd 100644 --- a/src/ExpressApplication.ts +++ b/src/ExpressApplication.ts @@ -25,15 +25,66 @@ import { import { Server } from 'http'; +/** + * This type is just for expressivity to identify + * the purpose of any variable that will handle controllers + */ type ControllerMap = Map; + +/** + * This type is just for expressivity to identify + * the purpose of any variable that will handle services + */ type ServiceMap = ControllerMap; +/** + * This class is the main class for your Cream-based REST API + * It will handle controllers, services and will communicate with + * express for you. + * + * @example To use it you can either extend from it or create a new object + * ```ts + * import express from "express"; + * import { ExpressApplication } from "@creamapi/cream"; + * + * let expressApp = express(); + * expressApp.use(express.json()); + * let app = new ExpressApplication(expressApp, 4040); + * app.addControllers([]); + * app.start(); + * ``` + */ export class ExpressApplication { + /** + * The map of active and registered controllers. + * The key will be the name of the controller. + * By this I mean the literal class name. + * Only objects that extend ExpressModule can be + * used as a controller + */ private controllers: ControllerMap; + + /** + * The map of active and registered services. + * The key will be the id given to the service when describing it. + */ private services: ServiceMap; + + /** + * The port to which the server will be bounded to. + */ private port: number; + + /** + * The server instance given by the express API + */ private server?: Server; + /** + * @param app is the express application that will handle the requests. + * @param port is the port that the server will be bound to + * @param _errorHandler is you custom implementation of the error handler that extends ExpressErrorHandler + */ constructor( private app: Express, port: number, @@ -44,6 +95,9 @@ export class ExpressApplication { this.services = new Map(); } + /** + * This attribute setter allows for setting a new custom error handler + */ set errorHandler(v: ExpressErrorHandler) { this._errorHandler = v; } @@ -191,7 +245,12 @@ export class ExpressApplication { }); } - public stop(): Promise { + /** + * This function is used to stop the server on purpose + * @returns void + * @throws any generated error by Server.close + */ + public async stop(): Promise { return new Promise((resolve, reject) => { this.server!.close(async (err) => { if (err) { @@ -202,10 +261,22 @@ export class ExpressApplication { }); } + /** + * @returns the active express application + */ public getExpressApp(): Express { return this.app; } + /** + * This method is used when we want to retreive a shared service + * within a controller. This is useful for example when we want to share + * user data among the services but we don't want to access the database + * everytime so a runtime service that is synced with the DB but caches data + * locally can be used. + * @param serviceId the service identifier that is given with IdentifiedBy decorator + * @returns the requested service or undefined if the service was not found + */ public getService(serviceId: string) { return this.services.get(serviceId) as T; } diff --git a/src/ExpressErrorHandler/ExpressErrorHandler.ts b/src/ExpressErrorHandler/ExpressErrorHandler.ts index 237570b6ce20f1e433ce0c111621b90bd9890a6e..5d4d5f21229a7acf4e3953d0d3f1b71229e89582 100644 --- a/src/ExpressErrorHandler/ExpressErrorHandler.ts +++ b/src/ExpressErrorHandler/ExpressErrorHandler.ts @@ -45,7 +45,7 @@ export interface ErrorInfo { */ export interface ExpressErrorHandler { /** - * @description This method is called whenever an exception is thrown + * This method is called whenever an exception is thrown * */ handle(err: Error, req: Request, res: Response): void; diff --git a/src/ExpressMiddleware/ExpressMiddleware.ts b/src/ExpressMiddleware/ExpressMiddleware.ts index 41b224bf70d4d788e8ba50b923074cfef47f32a1..397e162e7efd5a37e6f515fb6624e66bbae73f97 100644 --- a/src/ExpressMiddleware/ExpressMiddleware.ts +++ b/src/ExpressMiddleware/ExpressMiddleware.ts @@ -17,15 +17,51 @@ import { NextFunction, Request, Response } from 'express'; import { RestError } from '../ExpressErrorHandler/ExpressErrorHandler'; +/** + * @internal + * this is just a placeholder type for {} | undefined + */ export type MiddlewareDataCollection = {} | undefined; +/** + * @internal + * this type is for representing middelware data in requests + */ export type MiddlewareDataCollections = Map; +/** + * This interface is used to extend express Request interface with + * middleware data + */ export interface ExtendedRequest extends Request { + /** + * The collections that are used by middlewares to communicate data + * to the endpoint + */ middlewareDataCollections?: MiddlewareDataCollections; } +/** + * @internal + * + * common interface for middlewares. + * this common interface is used to define the duality of handle. + * This duality is due to the fact that handle can be either async + * or not sync. + * + * Having everything async will not give any performance boost, + * it will only create more confusion than there already is + */ export interface BaseMiddleware { + /** + * @internal + * This method is for handling requests coming from the user. + * It is in the correct format for express calls. + * @param req the request coming from the user + * @param res the response to the user + * @param next the next function to be called when finished + * working on the request and no response should be given + */ handle( req: Request, res: Response, @@ -33,12 +69,46 @@ export interface BaseMiddleware { ): void | Promise; } +/** + * This abstract class implements the base middleware handle method for handling + * ASYNChronous middlewares. + * @remarks The API for middlewares is still in its early stage and will need some refactoring + * to make it simpler for users. + */ export abstract class AsyncExpressMiddleware implements BaseMiddleware { + /** + * This is the method that your custom middleware should implement. + * This method will communicate to the user by returning the collection for + * this specific middleware. + * @remarks If the collection already exists then all its data will be + * overwritten by the return of this function. This sucks I know, any suggestion + * on how to change this is welcome. + * + * If an error should be thrown, like a 404 file not found error, {@link RestError} + * should be used to communicate with the user. + * Any error that does not extend RestError + * will return to the user with a 500 internal server error + * + * @param req the request coming from the client, with additional data included + * by previous middleware coming from the middleware stack + * @param res a response to the user, useful for example to send partial data. + * + * @returns the collection of this middleware, containing information useful for + * the following execution stack + */ public abstract behaviour( req: ExtendedRequest, res: Response ): Promise>; + /** + * @internal + * this implementation of handle is going to set the collection in the + * collection map with the new content. + * If behavior throws it will check if the error extends RestError + * and then uses the rest error statusCode to send information to the user. + * @remarks This might not be necessary since the error handler already does this + */ async handle(req: ExtendedRequest, res: Response, next: NextFunction) { try { let data: MiddlewareReturnData = await this.behaviour(req, res); @@ -61,12 +131,46 @@ export abstract class AsyncExpressMiddleware implements BaseMiddleware { } } +/** + * This abstract class implements the base middleware handle method for handling + * SYNChronous middlewares. + * @remarks The API for middlewares is still in its early stage and will need some refactoring + * to make it simpler for users. + */ export abstract class ExpressMiddleware implements BaseMiddleware { + /** + * This is the method that your custom middleware should implement. + * This method will communicate to the user by returning the collection for + * this specific middleware. + * @remarks If the collection already exists then all its data will be + * overwritten by the return of this function. This sucks I know, any suggestion + * on how to change this is welcome. + * + * If an error should be thrown, like a 404 file not found error, {@link RestError} + * should be used to communicate with the user. + * Any error that does not extend RestError + * will return to the user with a 500 internal server error + * + * @param req the request coming from the client, with additional data included + * by previous middleware coming from the middleware stack + * @param res a response to the user, useful for example to send partial data. + * + * @returns the collection of this middleware, containing information useful for + * the following execution stack + */ public abstract behaviour( req: ExtendedRequest, res: Response ): MiddlewareReturnData; + /** + * @internal + * this implementation of handle is going to set the collection in the + * collection map with the new content. + * If behavior throws it will check if the error extends RestError + * and then uses the rest error statusCode to send information to the user. + * @remarks This might not be necessary since the error handler already does this + */ handle(req: ExtendedRequest, res: Response, next: NextFunction) { try { let data: MiddlewareReturnData = this.behaviour(req, res); @@ -87,9 +191,21 @@ export abstract class ExpressMiddleware implements BaseMiddleware { } } +/** + * @internal + * this type is just used to declare an array of BaseMiddlewares + */ export type BaseMiddlewares = BaseMiddleware[]; +/** + * This method is used to define a collection in the collection mapping + * To access this data in a method use {@link MiddlewareData} + */ export class MiddlewareReturnData { + /** + * @param collectionName the collection identifier that should be used when saving the data in the map + * @param content the content of the collection + */ constructor( public readonly collectionName = 'default', public readonly content?: T diff --git a/src/ExpressMiddleware/MiddlewareData.ts b/src/ExpressMiddleware/MiddlewareData.ts index 292a59fb73218914a0b6801c34d26759f8335534..7b60cbd8f68e7280420840a689623424fa458b8d 100644 --- a/src/ExpressMiddleware/MiddlewareData.ts +++ b/src/ExpressMiddleware/MiddlewareData.ts @@ -17,7 +17,19 @@ import { ParameterProp } from '../ExpressAdapter/ParameterProp'; import { MIDDLEWARE_METADATA_KEY } from '../ExpressAdapter/ExpressAdapters'; +/** + * @internal + * This class is used to define parameter prop for middleware data + * This is used to map parameters to the data in a middleware collection + */ export class MiddlewareParameterProp extends ParameterProp { + /** + * @param index the index of the parameter in the parameter call array + * @param name The name of the parameter in the data collection + * @param collection The collection that the data should be retrieved from + * + * @remarks The data is accessed like collection[name] + */ constructor( index: number, name: string, @@ -27,11 +39,23 @@ export class MiddlewareParameterProp extends ParameterProp { } } +/** + * @internal + * a type for easily defining arrays + */ export type MiddlewareParameterProps = MiddlewareParameterProp[]; +/** + * This decorator factory is used to declare that a parameter of a method should be filled
+ * from the collection `collectionName`. The parameter will be filled either with the data or
+ * undefined if the data is not found in the collection or the collection is not found in the map + * @param collectionName The collection the data should be retrieved from. The default collection name is 'default' + * @param dataName the name of the field in the collection. To retrieve the entire collection the string "*" is used. + * @returns the decorator that will effectively decorate the method + */ export function MiddlewareData( collectionName: string = 'default', - dataName: string = '$' + dataName: string = '*' ) { return function ( target: Object, diff --git a/src/ExpressMiddleware/UseMiddleware.ts b/src/ExpressMiddleware/UseMiddleware.ts index 377ffdf93e0868b60473d34d8a35b81afce9767e..55516075533fb16c1113be6d1e13ddd42ac6898d 100644 --- a/src/ExpressMiddleware/UseMiddleware.ts +++ b/src/ExpressMiddleware/UseMiddleware.ts @@ -20,6 +20,12 @@ import { Route, Routes, ROUTES_METADATA_KEY } from '../HttpUtils/Route'; import { BaseMiddleware, BaseMiddlewares } from './ExpressMiddleware'; import { ExpressModule } from '../ExpressAdapter/ExpressModule'; +/** + * This decorator is used to create a stack of middlewares for a controller.
+ * This stack of middlewares is applied to all ExpressCalls in the controller + * @param middlewares the middlewares that should be called before calling the endpoint + * @returns a class that extends the target class and will initialize the middlewares + */ export function UseMiddlewaresForController< T extends { new (...args: any[]): ExpressModule }, >(middlewares: BaseMiddlewares) { @@ -32,6 +38,13 @@ export function UseMiddlewaresForController< }; } +/** + * This decorator is used to push to the middleware stack a new middleware for the
+ * decorated ExpressCall. Middlewares pushed with this decorator will be executed with
+ * a top-down approach (like a stack) + * @param middleware the middleware that should be pushed to the stack + * @returns the actual decorator + */ export function UseMiddleware(middleware: T) { return function ( target: ExpressModule, diff --git a/src/ExpressService/ExpressService.ts b/src/ExpressService/ExpressService.ts index 5e022ffe5916a0e3cdf9434494a071681340dbd1..978ab633bab96a6cfa4b51b9aae0547c3e97bd8f 100644 --- a/src/ExpressService/ExpressService.ts +++ b/src/ExpressService/ExpressService.ts @@ -16,24 +16,60 @@ import { ExpressApplication } from '../ExpressApplication'; +/** + * This abstract class is used to declare a service for the app.
+ * This service is active all time during the lifetime of the owner app
+ * A service is mainly used for exchanging information between controllers
+ * for example daisy chaining updates within the controllers.
+ * Another useful example is just to initialize and share a database connection
+ * between multiple controllers or just to run some code at startup. + */ export abstract class ExpressService { + /** + * the application owning the service + */ private _app!: ExpressApplication; + + /** + * The id of the service. This id is used to retrieve the service from within the app + */ private _id!: string; + /** + * This method must be implemented to bootstrap the service. + * If the service is successfully started then this method + * must return true. If there is an error with the initialization + * the method must return false + */ abstract init(): Promise; + /** + * This is the setter to set the owning application + */ public set app(v: ExpressApplication) { this._app = v; } + /** + * This method is use to get the owning application + */ public get app(): ExpressApplication { return this._app; } + /** + * This method is used to get the current identifier of the service + */ public get id() { return this._id; } + /** + * This decorator is used to declare the identifier of the service + * @remarks It is mandatory + * @param id The identifier that uniquely identifies the service. Having multiple services with the same ID will give conflicts + * @returns the decorator that will create a new class based from the service and will also set the identifier + */ public static IdentifiedBy( id: string ) { diff --git a/src/HttpUtils/ContentType.ts b/src/HttpUtils/ContentType.ts index 4038a370905cd411be4d2584a4439f9e45cf4128..b39d45528dcb759b25f6a180977d2f6571448204 100644 --- a/src/HttpUtils/ContentType.ts +++ b/src/HttpUtils/ContentType.ts @@ -19,6 +19,12 @@ import { Constructable } from '../Utils/Constructable'; export const HTTP_CONTENT_TYPE_METADATA_KEY = Symbol('cream:http:content-type'); +/** + * This decorator is used to decorate a class to add information about + * the content type that should be set in the Content-Type header + * @param contentType the content type that should be set in the header + * @returns the decorator of the function + */ export function ContentType(contentType: MessageType) { return function (target: T): T { Reflect.defineMetadata( diff --git a/src/HttpUtils/HttpMethod.ts b/src/HttpUtils/HttpMethod.ts index f6075f76a3ee46a0b7f6efa742810a3cf940527e..006aac33124fdc70c6148b0ae9de70cd7e084f6a 100644 --- a/src/HttpUtils/HttpMethod.ts +++ b/src/HttpUtils/HttpMethod.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +/** + * A simple enumerator of supported HTTP methods + */ export enum HttpMethod { GET = 'GET', POST = 'POST', diff --git a/src/HttpUtils/HttpReturnCode.ts b/src/HttpUtils/HttpReturnCode.ts index 9414205bf7cbb11081d8382e1cf32f0194422e34..930965e9ad915bd84b09a7fe045f2756a3dd097d 100644 --- a/src/HttpUtils/HttpReturnCode.ts +++ b/src/HttpUtils/HttpReturnCode.ts @@ -18,6 +18,13 @@ import { Constructable } from '../Utils/Constructable'; export const HTTP_CODE_METADATA_KEY = Symbol('cream:http:return-code'); +/** + * This decorator is used to decorate a class to add information about + * the return code that should be sent to the user when the target is serialized + * or in general returned from an ExpressCall + * @param code the code that should be returned to the user + * @returns the decorator of the function + */ export function HttpReturnCode(code: number) { return function (target: T): T { Reflect.defineMetadata(HTTP_CODE_METADATA_KEY, code, target.prototype); diff --git a/src/HttpUtils/Route.ts b/src/HttpUtils/Route.ts index 9ee72bf8ba8238241a47052aeafcd8b06f890ffd..2046c23c4d8de5103ec86466970a5b0dd7af3956 100644 --- a/src/HttpUtils/Route.ts +++ b/src/HttpUtils/Route.ts @@ -23,17 +23,40 @@ import { import { HttpMethod } from './HttpMethod'; import { ExpressModule } from '../ExpressAdapter/ExpressModule'; +/** + * @internal + * a type to define express-like functions + */ export type ExpressFunction = ( req: ExtendedRequest, res: Response, next: NextFunction ) => Promise; +/** + * @internal + * This type defines a method that creates + * a method of type ExpressFunction based on ExpressModule + */ export type ExpressFunctionFactory = ( thisArg: ExpressModule ) => ExpressFunction; +/** + * @internal + * This class represents the route associated with a ExpressCall. + * it is used to map endpoints to Express Functions that will + * map parameters to the target method and then call the method. + */ export class Route { + /** + * + * @param route The route of the ExpressCall decorated method. It is relative to the base path of the ExpressController + * @param method The function that will handle the request and then call the class method with the correct bounded "this" + * @param methodName the original method name + * @param httpMethod the http method associated with the ExpressCall decorated module + * @param middlewares the stack of middleware that should be called before param method + */ constructor( public route: string, public method: ExpressFunctionFactory, diff --git a/src/Serializer/CommonSerializers.ts b/src/Serializer/CommonSerializers.ts index 2b611d1957a63ca4a91cbd7a5cbbadacd7168250..6065e41c4e3904a0bba593ca2a29f8cabf5754d0 100644 --- a/src/Serializer/CommonSerializers.ts +++ b/src/Serializer/CommonSerializers.ts @@ -17,8 +17,25 @@ import { Serializer, SerialBite, SerializerCommon } from './ExpressSerializer'; import { SerializerMetaInfo } from './SerializerMetaInfo'; +/** + * In this namespace you can find common serializers defined by + * Cream + */ export namespace CreamSerializers { + /** + * This serializer serialize objects to JSON notation + * It does not serialize methods, only attributes + * @remarks circular dependencies are not checked yet and this can + * cause a serious issue with your code. + */ export class JSON extends Serializer { + /** + * This method takes a number as input and returns a string corresponding to that number + * it just uses {@link Number.toString} under the hood + * @param _dataLabel **ignored** + * @param num The number to be serialized + * @returns a string representing the number num in json format + */ async serializeNumber( _dataLabel: string, num: number @@ -26,6 +43,12 @@ export namespace CreamSerializers { return Number(num).toString(); } + /** + * This method just adds one quota at the beginning and one at the end of the string + * @param _dataLabel **ignored** + * @param data The string to be serialized + * @returns a string representing the string data in json format + */ async serializeString( _dataLabel: string, data: string @@ -33,6 +56,12 @@ export namespace CreamSerializers { return '"' + data + '"'; } + /** + * This method returns the string equivalent of the boolean values true/false in the JSON format + * @param _dataLabel **ignored** + * @param data the boolean data to be serialized + * @returns the string equivalent in JSON format of data + */ async serializeBoolean( _dataLabel: string, data: boolean @@ -40,10 +69,31 @@ export namespace CreamSerializers { return data ? 'true' : 'false'; } + /** + * This method is called when a null valued attribute is found. + * @remarks Beware that this doesn't mean that the value was undefined, but rather that + * the attribute exists but it is null, with not a value.\ + * For further information about the difference between undefined and null + * you should look in the JavaScript documentation. + * @param _dataLabel **ignored** + * @returns 'null' + */ async serializeNull(_dataLabel: string): Promise { return 'null'; } + /** + * This method will do the job of serializing the input + * object that is automatically converted to a stream.\ + * @remarks The stream order is top-down from class notation, + * so the first attribute found in the class will be the first + * to be inserted in the serialStream + * @param _serializedObjectName - **ignored** + * @param serialStream - the input stream the object to be serialized was sliced to by the {@link Serializer.serialize} method + * @param metaInfo - additional meta information about how to handle the object. These attributes were defined + * by {@link Meta} or by the {@link Serializer.serialize} method + * @returns a string representing the serialized version of the stream in JSON format + */ async handleSerializationStream( _serializedObjectName: string, serialStream: SerialBite[], @@ -56,20 +106,37 @@ export namespace CreamSerializers { return this.serializeObject(serialStream); } + /** + * This method serializes array-like objects previously identified by + * the {@link Serializer.serialize} method. + * @param serialStream - the serial stream of the array. each object is one object of the array + * SerialBite.dataLabel will be the index of the object + * @returns the string corresponding to the array. If the array is empty it will return a '[]' + * representing the empty array in JSON + */ async serializeArray(serialStream: SerialBite[]): Promise { + if (serialStream.length == 0) return '[]'; + let outStream = '['; for (let elem of serialStream) { outStream += (await this.serialize(elem.dataLabel, elem.data)) + ','; } - if (outStream.endsWith(',')) { - return outStream.slice(0, outStream.length - 1) + ']'; - } else { - return outStream + ']'; - } + + return outStream.slice(0, outStream.length - 1) + ']'; } + /** + * This method is used to serialize a stream that contains object-like data + * {@link SerialBite.dataLabel} will contain either the attribute name or the alternative + * given by the {@link MapTo} decorator. This is not an issue because a reference to + * the actual field is held in {@link SerialBite.data} + * @param serialStream - the array of SerialBites representing the object + * @returns the string representing the serialStream in JSON format + */ async serializeObject(serialStream: SerialBite[]): Promise { + if (serialStream.length == 0) return '{}'; + let outStream = '{'; for (let elem of serialStream) { if (elem.data !== undefined) { @@ -82,14 +149,16 @@ export namespace CreamSerializers { } } - if (outStream.endsWith(',')) { - return outStream.slice(0, outStream.length - 1) + '}'; - } - - return outStream + '}'; + return outStream.slice(0, outStream.length - 1) + '}'; } } + /** + * @experimental + * This class will act as a serializer to XML format. + * This is still experimental and it is not yet suggested for + * extensive use. Any help on its enhancement is welcome! + */ export class XML extends Serializer { public static Attribute: string = 'xml:attribute'; public static Text: string = 'xml:text'; diff --git a/src/Serializer/ExpressSerializer.ts b/src/Serializer/ExpressSerializer.ts index 2262caad9f1fd93594dbea1c8e9250130516bc6d..864a0bee3612f18fa1b27c719f2d864cb3b7d4e6 100644 --- a/src/Serializer/ExpressSerializer.ts +++ b/src/Serializer/ExpressSerializer.ts @@ -26,21 +26,62 @@ import { SerializerMetaInfo, } from './SerializerMetaInfo'; +/** + * @internal + * Defines the base types that should be serialized + */ type BaseSerializable = {} | string | number | boolean; +/** + * @internal + * This type defines the unit of data that + * should be serialized + */ export type SerialBite = { + /** + * the data that should be serialized + */ data: BaseSerializable; + + /** + * The label that should be used when serializing + * aka the output name + */ dataLabel: string; + + /** + * Additional meta information that can be + * used by serializers when serializing objects + */ metaInfo: SerializerMetaInfo | undefined; }; +/** + * This namespace defines Common information + * for serializers + */ export namespace SerializerCommon { + /** + * This namespace defines the common attributes to all serializers + */ export namespace Attributes { + /** + * This attribute is used to declare an object as an array + */ export const Array: string = 'common:array'; + + /** + * This attribute is used to automatically serialize everything in the object + */ export const AutoSerialize: string = 'common:autoserialize'; } } +/** + * This base class that implements the complex logic for handling + * serialization of objects. It gives a framework to easily implement + * complex serializers. + */ export abstract class Serializer { private targetName: string; private contextStack: object[] = []; @@ -67,6 +108,12 @@ export abstract class Serializer { abstract serializeNull(dataLabel: string): Promise; + /** + * This method is used to serialize a piece of data + * @param dataLabel the label of the data + * @param data the actual data + * @returns a string representing the serialized object + */ public async serialize(dataLabel: string, data: any): Promise { if (typeof data === 'number') { return this.serializeNumber(dataLabel, data); @@ -85,6 +132,13 @@ export abstract class Serializer { return this.serializeAnyObject(dataLabel, data); } + /** + * This method is used to serialize anything that is not + * a base type + * @param dataLabel the data label that should be used when rendering + * @param data the data that should be serialized + * @returns the string representing the serialized object + */ private async serializeAnyObject( dataLabel: string, data: Object @@ -135,19 +189,37 @@ export abstract class Serializer { return outStream; } + /** + * Gets the current context + * @returns the current context + */ private getContext(): Object { if (this.contextStack.length == 0) return {}; return this.contextStack[this.contextStack.length - 1]; } + /** + * removes the current context from the stack + */ private popContext() { this.contextStack.pop(); } + /** + * This is used to push a context to the stack. + * This context is used to infer data ownership + * @param context the context that should be pushed + */ private pushContext(context: Object) { this.contextStack.push(context); } + /** + * This static method makes any object serializable by iterating through the object + * this can break when recursive references are taken in place + * @param data the data to be serialized + * @returns the decorated data + */ public static makeSerializable(data: any) { for (let key in data) { AutoMap(data, key); @@ -156,6 +228,13 @@ export abstract class Serializer { return data; } + /** + * This method is called before handling a custom object + * @param dataLabel the label of the object + * @param data the object + * @param metaInfo any information useful for serialization + * @returns a string that should be appended before the serialization of the object + */ public async preObject( dataLabel: string, data: SerialBite[], @@ -164,6 +243,13 @@ export abstract class Serializer { return ''; } + /** + * This method is called after handling a custom object + * @param dataLabel the label of the object + * @param data the object + * @param metaInfo any information useful for serialization + * @returns a string that should be appended after the serialization of the object + */ public async postObject( dataLabel: string, data: SerialBite[], @@ -172,10 +258,21 @@ export abstract class Serializer { return ''; } + /** + * This method checks if the provided data is an array + * @param data the data to be checked + * @returns true if data is array, false otherwise + */ private dataIsArray(data: Object): boolean { return data.hasOwnProperty('length'); } + /** + * This method is used to create a serialization of + * the array in data by putting the index as the + * field name + * @param data the array that should be mapped + */ private autoMapArray(data: any[]): void { let serialMap: SerialMap[] = []; @@ -189,6 +286,11 @@ export abstract class Serializer { Reflect.defineMetadata(SERIAL_MAP_METADATA_KEY, serialMap, data); } + /** + * This method is used to streamify the data from the object given as input + * @param data the data that should be streamified + * @returns the stream of SerialBites that will be later handled by the serializer + */ private streamify(data: Object): SerialBite[] { let streamBuffer: SerialBite[] = []; @@ -216,6 +318,12 @@ export abstract class Serializer { return streamBuffer; } + /** + * This method will return meta information for the current context based on the + * dataLabel + * @param dataLabel the name of the field that the metadata should be retrieved from + * @returns the metadata associated with the dataLabel + */ private fetchMetaInfoForObject(dataLabel: string): SerializerMetaInfo { let serialMap: SerialMap[] | undefined = Reflect.getMetadata( SERIAL_MAP_METADATA_KEY, @@ -237,7 +345,10 @@ export abstract class Serializer { } /** + * @internal * This class is only used to bootstrap serialization + * It will also handle base types when no complex object is returned + * The serialization of those types is language agnostic */ export class BootstrapSerializer extends Serializer { async serializeNull(_dataLabel: string): Promise { diff --git a/src/Serializer/Serializable.ts b/src/Serializer/Serializable.ts index 7f36c1246b217da8aac7fbce5851f3c0af432f32..c20b4dd4b59dd69ccb59b1e64e42934ecf6828be 100644 --- a/src/Serializer/Serializable.ts +++ b/src/Serializer/Serializable.ts @@ -25,6 +25,12 @@ export type SerialMap = { outName: string; }; +/** + * Declares a class to be serialized and also declares the serializer that + * should be used to serialize the decorated class + * @param serializer the Serializer that should be used + * @returns the decorator that will decorate the class + */ export function Serializable( serializer: Constructable ) { @@ -38,10 +44,24 @@ export function Serializable( }; } +/** + * This method is used to declare a field serializable + * and it will also get the name of the field automatically + * @param target the target class attribute + * @param propertyName the name of the class attribute + * @returns the decorator that will handle the mapping for serialization + */ export function AutoMap(target: any, propertyName: string) { return MapTo(propertyName)(target, propertyName); } +/** + * This method is used to declare a field serializable + * and it will also get the name of the field automatically but it will + * map to a different name declared by the user + * @param name the new name of the field in the serialized object + * @returns the decorator that will handle the mapping for serialization + */ export function MapTo(name: string) { return function (target: any, propertyName: string) { let serialMap: SerialMap[] = diff --git a/src/Serializer/SerializableDataStructures.ts b/src/Serializer/SerializableDataStructures.ts index e0d275b8fdbfafe35dd988e62bb1afd048d37452..cce85e5502f86cce3a70babd9f8ae5b16f6c3355 100644 --- a/src/Serializer/SerializableDataStructures.ts +++ b/src/Serializer/SerializableDataStructures.ts @@ -17,6 +17,9 @@ import { Serializable } from './Serializable'; import { CreamSerializers } from './CommonSerializers'; +/** + * A common definition for a top level serializable aray + */ @Serializable(CreamSerializers.JSON) export class JSONSerializableArray extends Array { constructor(array: T[]) { diff --git a/src/Serializer/SerializerMetaInfo.ts b/src/Serializer/SerializerMetaInfo.ts index 4db9daafc37b9c7e0222118f9ff157e221d85bf8..a1c54d3abda4f57ef0c3f969c07a26778da38ef2 100644 --- a/src/Serializer/SerializerMetaInfo.ts +++ b/src/Serializer/SerializerMetaInfo.ts @@ -18,6 +18,12 @@ export const SERIALIZER_META_INFO_METADATA_KEY = Symbol( 'cream:data-serializer' ); +/** + * @internal + * This class is used to store meta information for + * the serializers that will be used to make decisions + * when serializing objects + */ export class SerializerMetaInfo { private attributes: string[] = []; @@ -30,6 +36,14 @@ export class SerializerMetaInfo { } } +/** + * Defines a metadata for the attribute that can be used by + * serializers to make decisions on serialization. + * @example the XML serializer uses the attribute AutoIndex to automatically + * add the index of the element as the attribute index in the xml tag + * @param attribute the attribute that should be defined + * @returns the decorator that will decorate the attribute + */ export function Meta(attribute: string) { return function (target: any, propertyName: string) { let metaInfo: SerializerMetaInfo = diff --git a/src/Utils/Constructable.ts b/src/Utils/Constructable.ts index b4a063dc19c1f8202ff0d279568f0e53ce7ba495..6a1e01ea5ab6c04b38546dd3840c1640734d86c9 100644 --- a/src/Utils/Constructable.ts +++ b/src/Utils/Constructable.ts @@ -14,6 +14,15 @@ * limitations under the License. */ +/** + * @internal + * This type is used for identifying objects that implement the new function + * in their prototype. This is useful to identify constructable objects + */ export type Constructable = { new (...args: any): T }; +/** + * @internal + * It is just an array of type Constructable + */ export type Constructables = Constructable[];