├── templates ├── fragment-spacer.ejs ├── fragment-error.ejs ├── fragment-header.ejs ├── fragment-welcome.ejs ├── fragment-footer.ejs ├── fragment-head.ejs ├── layout.ejs ├── simple.ejs ├── fragment-template.ejs ├── fragment-index.ejs ├── fragment-sample-routes.ejs ├── fragment-navigation.ejs ├── index.ejs └── fragment-plugin-routes.ejs ├── public ├── img │ ├── logo.png │ ├── favicon.ico │ ├── GitHub-Mark-32px.png │ └── GitHub-Mark-Light-32px.png ├── index.html └── css │ └── style.css ├── .editorconfig ├── docs ├── localhost 8000.URL ├── Getting Started - Fastify.URL ├── fastify_Getting-Started.md at master ˙ fastifyfastify ˙ GitHub.URL ├── launch.json ├── _env ├── Dockerfile-usage.md └── Docker-publish.md ├── .taprc ├── .jsdoc.json ├── .dockerignore ├── .npmignore ├── Dockerfile.distroless ├── Dockerfile.alpine ├── .gitignore ├── Dockerfile ├── test ├── sample.test.js └── app.test.js ├── src ├── server-simple.js ├── pubsub.js ├── constants.js ├── app.js ├── server.js ├── route.js ├── route-info.js ├── features.js └── utils.js ├── .snyk ├── README.md ├── package.json ├── LICENSE └── CHANGELOG.md /templates/fragment-spacer.ejs: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartiniOnGitHub/fastify-example/HEAD/public/img/logo.png -------------------------------------------------------------------------------- /public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartiniOnGitHub/fastify-example/HEAD/public/img/favicon.ico -------------------------------------------------------------------------------- /templates/fragment-error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /public/img/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartiniOnGitHub/fastify-example/HEAD/public/img/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /public/img/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartiniOnGitHub/fastify-example/HEAD/public/img/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /templates/fragment-header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | <%- include('fragment-navigation') -%> 5 |
6 | -------------------------------------------------------------------------------- /templates/fragment-welcome.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= welcome %> 5 |

6 |
7 |
8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /docs/localhost 8000.URL: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=http://localhost:8000/ 3 | IDList= 4 | HotKey=0 5 | IconFile=C:\Users\martinis\AppData\Local\Mozilla\Firefox\Profiles\1nc1d3r.default\shortcutCache\y0++al9srsHxcVuF_s0tfw==.ico 6 | IconIndex=0 7 | -------------------------------------------------------------------------------- /templates/fragment-footer.ejs: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /docs/Getting Started - Fastify.URL: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://www.fastify.io/docs/latest/Getting-Started/ 3 | IDList= 4 | HotKey=0 5 | IconFile=C:\Users\martinis\AppData\Local\Mozilla\Firefox\Profiles\1nc1d3r.default\shortcutCache\ZmhlVemnBvuCnEyJb3bTww==.ico 6 | IconIndex=0 7 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | # tap config file 2 | # color: false 3 | check-coverage: false 4 | comments: true 5 | # coverage: false 6 | # jsx: true 7 | node-arg: --allow-natives-syntax 8 | # snapshot: true 9 | strict: true 10 | # timeout: 60 11 | # ts: true 12 | 13 | files: 14 | # - 'test/**/*.test.js' 15 | - 'test/*.test.js' 16 | -------------------------------------------------------------------------------- /templates/fragment-head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> - <%= projectName %> 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/fastify_Getting-Started.md at master ˙ fastifyfastify ˙ GitHub.URL: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://github.com/fastify/fastify/blob/master/docs/Getting-Started.md 3 | IDList= 4 | HotKey=0 5 | IconFile=C:\Users\martinis\AppData\Local\Mozilla\Firefox\Profiles\1nc1d3r.default\shortcutCache\3WIKukybVrhPldB+3XiTqw==.ico 6 | IconIndex=0 7 | -------------------------------------------------------------------------------- /templates/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include('fragment-head') -%> 6 | 7 | 8 | <%- include('fragment-header') -%> 9 | 10 |
11 | <%- body %> 12 |
13 | 14 | <%- include('fragment-footer') -%> 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/simple.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= title %> - <%= projectName %> 7 | 8 | 9 | 10 |
11 | Welcome to simple template for <%= projectName %>. 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/fragment-template.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hello from a template fragment.

4 |
5 |

Note that a global layout has been set for all pages.

6 |
7 | 8 |
9 |

10 | <%= welcome %> 11 |

12 |
13 |
14 | -------------------------------------------------------------------------------- /templates/fragment-index.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hello from Index fragment page.

4 |
5 |

Note that a global layout has been set for all pages.

6 |
7 | 8 | <%- include('fragment-welcome') -%> 9 | 10 | <%- include('fragment-sample-routes') -%> 11 | <%# - include('fragment-spacer') -%> 12 | <%- include('fragment-plugin-routes') -%> 13 |
14 | -------------------------------------------------------------------------------- /templates/fragment-sample-routes.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sample pages and routes to call

4 |
5 | 6 | <% if (sampleRoutes !== null) { %> 7 | 12 | <% } else { %> 13 |

no data

14 | <% } %> 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /templates/fragment-navigation.ejs: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /templates/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%- include('fragment-head') -%> 7 | 8 | 9 | 10 | <%- include('fragment-header') -%> 11 | 12 |
13 | <%- include('fragment-welcome') -%> 14 | 15 | <%- include('fragment-sample-routes') -%> 16 | <%# - include('fragment-spacer') -%> 17 | <%- include('fragment-plugin-routes') -%> 18 |
19 | 20 | <%- include('fragment-footer') -%> 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /templates/fragment-plugin-routes.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Some routes published by loaded plugins

4 |
5 | 6 | <% if (pluginRoutes !== null) { %> 7 | 12 | <% } else { %> 13 |

no data

14 | 15 |
16 | nested div 17 |
18 | <% } %> 19 |
20 |
21 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["./src"], 4 | "exclude": ["./example", "./test", "./lib"], 5 | "includePattern": ".+\\.js(doc|x)?$", 6 | "excludePattern": "(^|\\/|\\\\)_" 7 | }, 8 | "opts": { 9 | "template": "templates/default", 10 | "encoding": "utf8", 11 | "destination": "./out/", 12 | "recurse": true 13 | }, 14 | "plugins": [ 15 | "plugins/summarize" 16 | ], 17 | "tags": { 18 | "allowUnknownTags": false, 19 | "dictionaries": ["jsdoc"] 20 | }, 21 | "templates": { 22 | "default": { 23 | "includeDate": false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "fastify-example - attach", 11 | "port": 9229 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "fastify-example - server", 17 | "program": "${workspaceFolder}/src/server" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .git 4 | .gitignore 5 | 6 | .settings 7 | .classpath 8 | .project 9 | .cache* 10 | .metadata 11 | .worksheet 12 | RemoteSystemsTempFiles/ 13 | Servers/ 14 | 15 | .gradle 16 | bin/ 17 | build/ 18 | /*/build/ 19 | # dist/ 20 | 21 | *.log 22 | *.log* 23 | *~* 24 | *.bak 25 | *.tmp 26 | *.temp 27 | Thumbs.db 28 | 29 | _old 30 | /dist 31 | # /*.min.js* 32 | 33 | /.vscode 34 | 35 | out/ 36 | tmp/ 37 | temp/ 38 | /target/ 39 | /target-eclipse/ 40 | 41 | .env 42 | .envrc 43 | local.properties 44 | 45 | # *.class 46 | 47 | node_modules 48 | 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | 53 | .nyc_output 54 | /coverage 55 | 56 | .dockerignore 57 | Dockerfile 58 | Dockerfile*.* 59 | Jenkinsfile 60 | Jenkinsfile*.* 61 | /jenkins 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .vscode 4 | .cache 5 | .metadata 6 | .settings 7 | .idea 8 | 9 | _old/ 10 | build/ 11 | docs/ 12 | lib/ 13 | logs/ 14 | out/ 15 | private/ 16 | tmp/ 17 | temp/ 18 | 19 | coverage/ 20 | .nyc_output 21 | 22 | scripts/ 23 | src/ 24 | test/ 25 | # example/ 26 | 27 | *.zip 28 | *.tar 29 | *.tar.gz 30 | *.tgz 31 | 32 | .env 33 | .envrc 34 | .babelrc 35 | .clinic 36 | .dependabot 37 | .eslintignore 38 | .eslintrc* 39 | .eslintcache 40 | .esdoc.json 41 | .yarn-integrity 42 | .taprc 43 | 44 | .editorconfig 45 | .gitattributes 46 | .travis.yml 47 | 48 | gulpfile.* 49 | jsdoc-conf.* 50 | .jsdoc.json 51 | 52 | jenkins.sh 53 | tslint.json 54 | 55 | *.log 56 | 57 | TODO.* 58 | BUILD.md 59 | 60 | .dockerignore 61 | # Dockerfile 62 | # Dockerfile*.* 63 | Jenkinsfile 64 | Jenkinsfile*.* 65 | /jenkins 66 | -------------------------------------------------------------------------------- /Dockerfile.distroless: -------------------------------------------------------------------------------- 1 | FROM node:14 as builder 2 | 3 | LABEL version="3.0.0" 4 | LABEL description="Example Fastify (Node.js) webapp Docker Image" 5 | LABEL maintainer="Sandro Martini " 6 | 7 | WORKDIR /app 8 | 9 | # set default node env 10 | ENV NODE_ENV=production 11 | ENV NPM_CONFIG_LOGLEVEL=warn 12 | 13 | # copy project definition/dependencies files, for better reuse of layers 14 | COPY package*.json ./ 15 | 16 | # copy stuff required by prepublish (postinstall) 17 | COPY .snyk ./ 18 | 19 | # install dependencies here, for better reuse of layers 20 | RUN npm install && npm audit fix 21 | 22 | # copy all sources in the container (exclusions in .dockerignore file) 23 | COPY . . 24 | 25 | 26 | # release layer (the only one in the final image) 27 | FROM gcr.io/distroless/nodejs:14 AS release 28 | COPY --from=builder /app /app 29 | WORKDIR /app 30 | 31 | EXPOSE 8000 32 | 33 | CMD [ "./src/server" ] 34 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Static pages - Index 6 | 7 | 8 | 9 | 10 | 11 |
12 | 17 |
18 | 19 |
20 |
21 |
22 |

23 | Welcome to Static pages 24 |

25 |
26 |
27 | 28 |
29 |
30 |

Some text...

31 |
32 |

no text

33 | 34 |
35 | nested div 36 |
37 |
38 |
39 |
40 | 41 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/_env: -------------------------------------------------------------------------------- 1 | # configuration variables for the current environment 2 | ENVIRONMENT=dev-local 3 | 4 | # FASTIFY_OPTIONS={ "logger": { "level": "debug" } } 5 | # FASTIFY_OPTIONS={ } 6 | 7 | # HTTP_PORT=3000 8 | # HTTP_ADDRESS=0.0.0.0 9 | 10 | # NATS_SERVER_URL=nats://demo.nats.io:4222 11 | # NATS_SERVER_URL=nats://localhost:4222 12 | 13 | # WEBHOOK_SECRET_KEY=my example Secret Key 14 | 15 | # feature flags, uncomment to disable related feature 16 | # FEATURE_PLATFORM_INFO_DISABLE=true 17 | # FEATURE_CHECK_RUNTIME_ENV_DISABLE=true 18 | # FEATURE_FAVICON_DISABLE=true 19 | # FEATURE_WEBHOOK_DISABLE=true 20 | # FEATURE_HEALTHCHECK_DISABLE=true 21 | # FEATURE_CLOUDEVENTS_DISABLE=true 22 | # FEATURE_NATS_DISABLE=true 23 | # FEATURE_CLOUDEVENTS_STRICT_DISABLE=true 24 | # FEATURE_CLOUDEVENTS_LOG_CONSOLE_DISABLE=true 25 | # FEATURE_CLOUDEVENTS_LOG_FILE_DISABLE=true 26 | 27 | # some tests for those values 28 | # FEATURE_CHECK_RUNTIME_ENV_DISABLE=true 29 | # FEATURE_FAVICON_DISABLE=false 30 | FEATURE_NATS_DISABLE=true 31 | FEATURE_CLOUDEVENTS_STRICT_DISABLE=false 32 | FEATURE_CLOUDEVENTS_LOG_CONSOLE_DISABLE=true 33 | FEATURE_CLOUDEVENTS_LOG_FILE_DISABLE=false 34 | 35 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as builder 2 | 3 | LABEL version="3.0.0" 4 | LABEL description="Example Fastify (Node.js) webapp Docker Image" 5 | LABEL maintainer="Sandro Martini " 6 | 7 | # update packages, to reduce risk of vulnerabilities 8 | RUN apk update && apk upgrade 9 | # RUN apk cache clean 10 | 11 | # set a non privileged user to use when running this image 12 | RUN addgroup -S nodejs && adduser -S nodejs -G nodejs 13 | USER nodejs 14 | # set right (secure) folder permissions 15 | RUN mkdir -p /home/nodejs/app/node_modules && chown -R nodejs:nodejs /home/nodejs/app 16 | 17 | WORKDIR /home/nodejs/app 18 | 19 | # set default node env 20 | # to be able to run tests (for example in CI), do not set production as environment 21 | ENV NODE_ENV=production 22 | 23 | ENV NPM_CONFIG_LOGLEVEL=warn 24 | 25 | # copy project definition/dependencies files, for better reuse of layers 26 | COPY --chown=nodejs:nodejs package*.json ./ 27 | 28 | # copy stuff required by prepublish (postinstall) 29 | COPY .snyk ./ 30 | 31 | # install dependencies here, for better reuse of layers 32 | RUN npm install && npm audit fix && npm cache clean --force 33 | 34 | # copy all sources in the container (exclusions in .dockerignore file) 35 | COPY --chown=nodejs:nodejs . . 36 | 37 | EXPOSE 8000 38 | 39 | # add an healthcheck, useful 40 | # healthcheck by calling the additional script exposed by the plugin 41 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s CMD npm run healthcheck-manual 42 | 43 | # ENTRYPOINT [ "npm" ] 44 | # CMD [ "npm", "start" ] 45 | CMD [ "node", "./src/server" ] 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .DS_Store 2 | .git 3 | .cache 4 | .metadata 5 | .settings 6 | .vscode 7 | # *.log 8 | *.iws 9 | .idea/ 10 | *.iml 11 | *.md5 12 | *.sha* 13 | *~* 14 | *.bak 15 | Thumbs.db 16 | *.db 17 | *.zip 18 | *.tar 19 | *.tar.gz 20 | _old/ 21 | # bin/ 22 | build/ 23 | dist/ 24 | out/ 25 | private/ 26 | # server/ 27 | # target/ 28 | temp/ 29 | tmp/ 30 | 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Logs 37 | logs 38 | *.log* 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | 43 | # Runtime data 44 | pids 45 | *.pid 46 | *.seed 47 | *.pid.lock 48 | 49 | # Directory for instrumented libs generated by jscoverage/JSCover 50 | lib-cov 51 | 52 | # Coverage directory used by tools like istanbul 53 | coverage 54 | 55 | # nyc test coverage 56 | .nyc_output 57 | 58 | # Clinic analysis/results 59 | .clinic 60 | 61 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 62 | .grunt 63 | 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | 67 | # node-waf configuration 68 | .lock-wscript 69 | 70 | # Compiled binary addons (http://nodejs.org/api/addons.html) 71 | build/Release 72 | 73 | # Typescript v1 declaration files 74 | typings/ 75 | 76 | # Optional npm cache directory 77 | .npm 78 | 79 | # npm 5 dependencies lock file (in general should be committed) 80 | package-lock.json 81 | 82 | # Optional eslint cache 83 | .eslintcache 84 | 85 | # Optional REPL history 86 | .node_repl_history 87 | 88 | # Output of 'npm pack' 89 | *.tgz 90 | 91 | # Yarn Integrity file 92 | .yarn-integrity 93 | 94 | # env/dotenv variables file 95 | .env 96 | .envrc 97 | 98 | # Others 99 | lib/ 100 | .DS_Store 101 | ._.DS_Store 102 | 103 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-slim as builder 2 | 3 | LABEL version="3.0.0" 4 | LABEL description="Example Fastify (Node.js) webapp Docker Image" 5 | LABEL maintainer="Sandro Martini " 6 | 7 | # update packages, to reduce risk of vulnerabilities 8 | RUN apt-get update && apt-get upgrade -y && apt-get autoclean -y && apt-get autoremove -y 9 | 10 | # set a non privileged user to use when running this image 11 | RUN groupadd -r nodejs && useradd -g nodejs -s /bin/bash -d /home/nodejs -m nodejs 12 | USER nodejs 13 | # set right (secure) folder permissions 14 | RUN mkdir -p /home/nodejs/app/node_modules && chown -R nodejs:nodejs /home/nodejs/app 15 | 16 | WORKDIR /home/nodejs/app 17 | 18 | # set default node env 19 | ARG NODE_ENV=development 20 | # ARG NODE_ENV=production 21 | # to be able to run tests (for example in CI), do not set production as environment 22 | ENV NODE_ENV=${NODE_ENV} 23 | 24 | ENV NPM_CONFIG_LOGLEVEL=warn 25 | 26 | # copy project definition/dependencies files, for better reuse of layers 27 | COPY --chown=nodejs:nodejs package*.json ./ 28 | 29 | # copy stuff required by prepublish (postinstall) 30 | COPY .snyk ./ 31 | 32 | # install dependencies here, for better reuse of layers 33 | RUN npm install && npm audit fix && npm cache clean --force 34 | 35 | # copy all sources in the container (exclusions in .dockerignore file) 36 | COPY --chown=nodejs:nodejs . . 37 | 38 | # build/pack binaries from sources 39 | 40 | # This results in a single layer image 41 | # FROM node:lts-alpine AS release 42 | # COPY --from=builder /dist /dist 43 | 44 | # exposed port/s 45 | EXPOSE 8000 46 | 47 | # add an healthcheck, useful 48 | # healthcheck with curl, but not recommended 49 | # HEALTHCHECK CMD curl --fail http://localhost:8000/health || exit 1 50 | # healthcheck by calling the additional script exposed by the plugin 51 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s CMD npm run healthcheck-manual 52 | 53 | # ENTRYPOINT [ "node" ] 54 | # CMD [ "npm", "start" ] 55 | CMD [ "node", "./src/server" ] 56 | 57 | # end. 58 | -------------------------------------------------------------------------------- /test/sample.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | 23 | const assert = require('assert').strict 24 | const test = require('tap').test 25 | const tap = require('tap') 26 | // const test = tap.test 27 | // const sget = require('simple-get').concat 28 | // const fs = require('fs') 29 | // const path = require('path') 30 | // const resolve = require('path').resolve 31 | 32 | // test zero, just to ensure that test framework works 33 | assert(tap !== null) 34 | tap.pass('this is an empty test, but test frameworks works') 35 | 36 | // note that this is not neccessarily the main/entry point file, unless specified/called directly ... 37 | tap.comment('Sample JavaScript Test file using TAP ...') 38 | 39 | // load the module/s to test 40 | const utilModule = require('../src/utils') 41 | 42 | // first tests, on a utility module 43 | tap.equal(utilModule.isStringEmpty('not empty'), false) 44 | tap.equal(utilModule.isStringEmpty(''), true) 45 | 46 | // other tests, using a different (better) syntax 47 | test('util, string empty or not', (t) => { 48 | // t.plan(2) 49 | // const util = require('../src/utils.js') // forbidden by my ESLint rules (at the moment), and best practice 50 | const util = utilModule 51 | 52 | t.equal(utilModule.isStringEmpty('not empty'), false) 53 | t.equal(utilModule.isStringEmpty(''), true) 54 | 55 | t.end() 56 | }) 57 | 58 | // etc ... 59 | -------------------------------------------------------------------------------- /docs/Dockerfile-usage.md: -------------------------------------------------------------------------------- 1 | # Webapp and Docker 2 | 3 | ## Usage 4 | 5 | To use Docker, all is already configured via code in its 'Dockerfile', so you only need to tell Docker to use it. 6 | 7 | For simplicity, the current webapp name here is 'test-webapp'. 8 | 9 | Note that by default no user is able to run Docker commands without 'sudo', unless it is in the 'docker' group. 10 | 11 | 12 | ### Build 13 | 14 | - to build the image: `docker build -t test-webapp .` 15 | - to inspect image (for check contents, etc): `docker exec -it test-webapp /bin/bash` 16 | - to install dependencies (if not already installed during the build): `docker run -t test-webapp npm install` 17 | - to install dependencies (if not already installed during the build) for production environment: `docker run -t test-webapp npm install --only=production` 18 | 19 | ### Run 20 | 21 | - to run the image (normal): `docker run -p 8080:8000 test-webapp` 22 | - to run the image (detached): `docker run -d -p 8080:8000 test-webapp` 23 | - to run the image (detached and a name for the instance 1 and related port, example): `docker run -d -p 8001:8000 test-webapp --name test-webapp-1 test-webapp` 24 | - and then get container ID: `docker ps` 25 | - and get its logs: `docker logs ` 26 | - to run the image (normal) via npm command start (in this case the process will stay active, so I need to stop later): `docker run -p 8080:8000 -t test-webapp bash -c "npm start"` 27 | - to run the image (normal) via npm command: `docker run -p 8080:8000 -t test-webapp bash -c "npm test"` 28 | - to run the image (detached) via npm command start (in this case the process will stay active in background, so I need to stop later): `docker run -d -p 8080:8000 -t test-webapp bash -c "npm start"` 29 | - and then I could get its logs with (requires its id): `docker logs ` 30 | - or to display all or only latest n lines, for example: `docker logs --tail="all"` 31 | 32 | ### Inspect/Checks 33 | 34 | - to enter in the container (for check contents, etc, but attention to entry point if set): `docker run -it test-webapp bash` 35 | - to enter in the container (for check contents, etc, but attention to entry point if set): `docker exec -it /bin/bash` 36 | - to check the container works, for example: `curl -i http://localhost:8080` 37 | 38 | ### Stop/Cleanup 39 | 40 | - to stop the container, for example: `docker stop ` 41 | - to cleanup all images, containers, etc: `docker system prune --all` 42 | 43 | ---- 44 | -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | 23 | const assert = require('assert').strict 24 | const test = require('tap').test 25 | const tap = require('tap') 26 | 27 | // load environment specific variables from '.env' file (if any) into process.env ... 28 | const dotenv = require('dotenv') 29 | dotenv.config() 30 | 31 | const Fastify = require('fastify') 32 | const App = require('../src/app') 33 | 34 | // load/init webapp constants and general utilities 35 | const k = require('../src/constants') 36 | // const utils = require('../src/utils') 37 | 38 | const fastifyOptions = JSON.parse(k.fastifyOptionsString) 39 | 40 | // some basic test, but using the async/await syntax 41 | test('Basic', async t => { 42 | const fastify = Fastify() 43 | await fastify.register(App) 44 | t.teardown(() => { fastify.close() }) 45 | 46 | const response = await fastify.inject({ 47 | method: 'GET', 48 | path: '/health' 49 | }) 50 | 51 | t.equal(response.statusCode, 200) 52 | const r = response.json() 53 | t.equal(r.statusCode, 200) 54 | t.equal(r.status, 'ok') 55 | // other fields in it like uptime, so no other assertions (it would be too complex to compare) 56 | 57 | t.end() 58 | }) 59 | 60 | // some basic test, but using the async/await syntax 61 | test('Basic with options', async t => { 62 | const fastify = Fastify(fastifyOptions) 63 | await fastify.register(App, fastifyOptions) 64 | t.teardown(() => { fastify.close() }) 65 | 66 | const response = await fastify.inject({ 67 | method: 'GET', 68 | path: '/health' 69 | }) 70 | 71 | t.equal(response.statusCode, 200) 72 | const r = response.json() 73 | t.equal(r.statusCode, 200) 74 | t.equal(r.status, 'ok') 75 | // other fields in it like uptime, so no other assertions (it would be too complex to compare) 76 | 77 | t.end() 78 | }) 79 | 80 | // etc ... 81 | -------------------------------------------------------------------------------- /src/server-simple.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | 23 | const fastify = require('fastify')() 24 | 25 | const fs = require('fs') 26 | const path = require('path') 27 | 28 | const templateEngine = require('ejs') 29 | const resolve = require('path').resolve 30 | const templatesFolder = 'templates' 31 | const pubFolder = '../public' 32 | 33 | fastify.register(require('@fastify/view'), { 34 | engine: { 35 | ejs: templateEngine 36 | }, 37 | includeViewExtension: true, 38 | templates: templatesFolder, 39 | options: { 40 | filename: resolve(templatesFolder), 41 | views: [__dirname, pubFolder] 42 | } 43 | }) 44 | 45 | fastify.register(require('@fastify/static'), { 46 | root: path.join(__dirname, pubFolder), 47 | prefix: '/public/' // optional: default '/' 48 | }) 49 | 50 | fastify.get('/', (request, reply) => { 51 | reply.view('index', { 52 | projectName: 'Simple Server', 53 | projectVersion: '1.0.0', 54 | environment: 'development', 55 | assets: '/public/', 56 | title: 'Home', 57 | welcome: 'Welcome to the Home Page', 58 | sampleRoutes: null, 59 | pluginRoutes: null 60 | }) 61 | }) 62 | 63 | fastify.get('/favicon.ico', (request, reply) => { 64 | reply.type('image/x-icon').sendFile('img' + '/favicon.ico') 65 | }) 66 | 67 | // note that to make it work (be exposed) when deployed in a container (Docker, etc) we need to listen not only to localhost but for example to all interfaces ... 68 | fastify.listen({ port: 8000, host: '0.0.0.0' }, (err, address) => { 69 | if (err) { 70 | app.log.error(err) 71 | process.exit(1) 72 | // throw err 73 | } 74 | const msg = `Server listening on '${address}' ...` 75 | console.log(msg) 76 | fastify.log.info(msg) 77 | }) 78 | 79 | // log server startup, but note that by default logs are disabled in Fastify (even errors) ... 80 | fastify.log.info('Server Startup script successfully executed') 81 | 82 | // module.exports = fastify 83 | -------------------------------------------------------------------------------- /src/pubsub.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | 23 | const assert = require('assert').strict 24 | 25 | // TODO: later try to use fastify.NATS instead (published by my plugin) ... wip 26 | const NATS = require('nats') 27 | 28 | const utils = require('./utils') 29 | // const hostname = require('os').hostname() 30 | 31 | function logReceivedMessage (err, msg) { 32 | utils.logToConsole(`Received message: (${err}, ${msg})`) 33 | if (err) { 34 | console.error(err.message) 35 | } else { 36 | // later find a good way to inject here NATS StringCodec, 37 | // or use it directly from an external variable ... 38 | const sc = NATS.StringCodec() // codec for a string message 39 | utils.logToConsole(`Message received, decoded: '${sc.decode(msg.data)}'`) 40 | } 41 | } 42 | 43 | // sample subscriber function for the NATS queue specified in constants 44 | async function subscribe (nats, queueName, disabled = false, cb = logReceivedMessage, dec = NATS.StringCodec()) { 45 | if (!nats || disabled === true) { 46 | return 47 | } 48 | utils.logToConsole(`Subscribe to messages from the queue '${queueName}'`) 49 | 50 | if (utils.isDefinedAndNotNull(cb)) { 51 | // simple subscriber with a callback 52 | nats.subscribe(queueName, { callback: cb }, { 53 | // max: 1 // after 1 message, auto-unsubscribe from the subject 54 | }) 55 | } else { 56 | // use the recommended way, async iterator 57 | // example iterator subscription 58 | const sub = nats.subscribe(queueName, { 59 | // max: 1 // after 1 message, auto-unsubscribe from the subject 60 | }) 61 | for await (const m of sub) { 62 | const decoded = dec.decode(m.data) 63 | utils.logToConsole(`Message received from async iterator, decoded: '${decoded}'`) 64 | } 65 | } 66 | } 67 | 68 | // sample publish function for the NATS queue specified in constants 69 | async function publish (nats, queueName, disabled = false, msg = '', enc = NATS.StringCodec()) { 70 | if (!nats || disabled === true) { 71 | return 72 | } 73 | utils.logToConsole(`Publish message in the queue '${queueName}'`) 74 | 75 | // simple publisher 76 | nats.publish(queueName, enc.encode(msg)) 77 | } 78 | 79 | module.exports = { 80 | publish, 81 | subscribe 82 | } 83 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | 23 | // const assert = require('assert').strict 24 | const path = require('path') 25 | const hostname = require('os').hostname() 26 | 27 | const isDocker = require('is-docker') 28 | 29 | const k = { 30 | packageName: require('../package.json').name, 31 | packageVersion: require('../package.json').version, 32 | fastifyVersion: require('fastify/package.json').version, 33 | projectFolderFromScript: path.normalize(path.join(__dirname, path.sep, '..', path.sep)), 34 | fastifyOptionsString: process.env.FASTIFY_OPTIONS || '{ "logger": { "level": "info" } }', 35 | hostname, 36 | protocol: 'http', 37 | address: process.env.HTTP_ADDRESS || '127.0.0.1', // safer default 38 | port: process.env.HTTP_PORT || 8000, 39 | folders: { 40 | templatesFolderName: 'templates', 41 | publicAssetsFolderName: 'public', 42 | cssAssetsFolderName: 'css', 43 | imagesAssetsFolderName: 'img', 44 | jsAssetsFolderName: 'js' 45 | }, 46 | mappings: { 47 | rootMapping: '/', 48 | staticAssetsMapping: '/static/', 49 | webhookMapping: '/custom-webhook' 50 | }, 51 | serverUrlMode: 'pluginAndRequestSimplified', // same behavior as default value, but in this way set in CloudEvent extension object 52 | // baseNamespace: 'com.github.smartiniOnGitHub.fastify-example', 53 | cloudEventOptions: { 54 | strict: true // enable strict mode in generated CloudEvents, optional 55 | }, 56 | natsQueueOptions: { 57 | servers: process.env.NATS_SERVER_URL // use from env var, or use NATS demo if plugin option is enabled 58 | }, 59 | queueDisabled: false // always try to send messages to the queue, as a sample 60 | } 61 | k.imagesFolderFromScript = path.normalize(path.join(k.projectFolderFromScript, path.sep, k.folders.publicAssetsFolderName, path.sep, k.folders.imagesAssetsFolderName, path.sep)) 62 | // to make it work (be exposed) when deployed in a container (Docker, etc) we need to listen not only to localhost but for example to all interfaces ... 63 | if (!process.env.HTTP_ADDRESS) { 64 | k.address = (isDocker() === true) ? '0.0.0.0' : '127.0.0.1' 65 | } 66 | k.baseNamespace = `com.github.smartiniOnGitHub.${k.packageName}-v${k.packageVersion}` 67 | k.serverUrl = `${k.protocol}://${k.address}:${k.port}` 68 | k.source = k.serverUrl 69 | k.queueName = `${k.packageName}-${k.packageVersion}` 70 | k.message = `Hello World, from a Fastify web application just started at '${k.hostname}' and available at '${k.serverUrl}'!` 71 | 72 | module.exports = k 73 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - standard > eslint > lodash: 8 | patched: '2020-05-02T02:26:19.778Z' 9 | - standard > eslint > inquirer > lodash: 10 | patched: '2020-05-02T02:26:19.778Z' 11 | - standard > eslint > table > lodash: 12 | patched: '2020-05-02T02:26:19.778Z' 13 | - tap > import-jsx > @babel/core > lodash: 14 | patched: '2020-05-02T02:26:19.778Z' 15 | - tap > import-jsx > @babel/core > @babel/helper-module-transforms > lodash: 16 | patched: '2020-05-02T02:26:19.778Z' 17 | - tap > nyc > istanbul-lib-instrument > @babel/traverse > lodash: 18 | patched: '2020-05-02T02:26:19.778Z' 19 | - tap > treport > import-jsx > @babel/core > lodash: 20 | patched: '2020-05-02T02:26:19.778Z' 21 | - tap > import-jsx > @babel/core > @babel/helpers > @babel/traverse > lodash: 22 | patched: '2020-05-02T02:26:19.778Z' 23 | - tap > nyc > istanbul-lib-instrument > @babel/traverse > @babel/generator > lodash: 24 | patched: '2020-05-02T02:26:19.778Z' 25 | - tap > treport > import-jsx > @babel/core > @babel/helper-module-transforms > lodash: 26 | patched: '2020-05-02T02:26:19.778Z' 27 | - tap > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > lodash: 28 | patched: '2020-05-02T02:26:19.778Z' 29 | - tap > nyc > istanbul-lib-instrument > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash: 30 | patched: '2020-05-02T02:26:19.778Z' 31 | - tap > treport > import-jsx > @babel/core > @babel/helpers > @babel/traverse > lodash: 32 | patched: '2020-05-02T02:26:19.778Z' 33 | - tap > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > @babel/generator > lodash: 34 | patched: '2020-05-02T02:26:19.778Z' 35 | - tap > treport > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > lodash: 36 | patched: '2020-05-02T02:26:19.778Z' 37 | - tap > nyc > istanbul-lib-instrument > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash: 38 | patched: '2020-05-02T02:26:19.778Z' 39 | - tap > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash: 40 | patched: '2020-05-02T02:26:19.778Z' 41 | - tap > treport > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > @babel/generator > lodash: 42 | patched: '2020-05-02T02:26:19.778Z' 43 | - tap > treport > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash: 44 | patched: '2020-05-02T02:26:19.778Z' 45 | - tap > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash: 46 | patched: '2020-05-02T02:26:19.778Z' 47 | - tap > treport > import-jsx > @babel/core > @babel/helper-module-transforms > @babel/helper-replace-supers > @babel/traverse > @babel/helper-function-name > @babel/template > @babel/types > lodash: 48 | patched: '2020-05-02T02:26:19.778Z' 49 | -------------------------------------------------------------------------------- /docs/Docker-publish.md: -------------------------------------------------------------------------------- 1 | # How to publish Webapp Docker images to DockerHub 2 | 3 | ## General info 4 | 5 | Main purpose of this page is to describe/show how to publish Docker images 6 | of the current webapp to Docker images Repository [DockerHub](https://hub.docker.com). 7 | 8 | Note that in this webapp I have two Docker files: 9 | - `Dockerfile`, the default one, with all project Developer dependencies etc 10 | - `Dockerfile.alpine`, a variant but much smaller, no Developer dependencies, 11 | and tailored for running the image in Production 12 | 13 | Docker tags by default are for the default Docker image, 14 | and there are other tags (with suffix '-alpine') 15 | that refers to the Alpine based Docker image. 16 | 17 | Webapp Docker images are all based on standard Node.js (official) images. 18 | 19 | Note that the repository at DockerHub was [smartiniatdocker09/fastify-example](https://hub.docker.com/r/smartiniatdocker09/fastify-example/), 20 | with automatic build rules so that any push in the git 'master' branch 21 | will trigger a build of both images. 22 | A push for a git tag should build an image with the same tag (and maybe a suffix). 23 | So manual publish shouldn't be needed, anyway it's here just for reference. 24 | 25 | Image tags were at [tags - smartiniatdocker09/fastify-example](https://hub.docker.com/r/smartiniatdocker09/fastify-example/tags), and you can find tags like: 26 | 'latest' / 'latest-alpine' and even '2.2.0' / '2.2.0-alpine', etc ... 27 | 28 | Note: since some time all this is **no more available** without a Pro subscription; 29 | so to avoid stale/outdated images (with security vulnerabilities inside, etc) 30 | I **deleted** that repo and all related images and tags. 31 | 32 | 33 | ### Manual Publish 34 | 35 | To manually publish images of the current webapp, do the following steps. 36 | Note that when using default file ('Dockerfile') no need to specify it 37 | in Docker commands, otherwise you need to specify the file (as seen in following examples) with the option '-f file_to_use'. 38 | 39 | - DockerHub username, represented here as `$dockerhub_username` 40 | - login to DockerHub with `docker login`, 41 | - build the image, with the username as first part of the name: `docker build -t $dockerhub_username/fastify-example -f Dockerfile.alpine .` 42 | - inspect current images, with: `docker images`, and from the list 43 | get related image ID (called here 'imageID'), 44 | opz. pipe with grep to filter images only with that name, 45 | by appending: ` | grep fastify-example` 46 | - create the tag, for example with: `docker tag imageID $dockerhub_username/fastify-example:2.2.0-alpine`; by default tag is 'latest' 47 | - push that tag, with: `docker push $dockerhub_username/fastify-example:2.2.0-alpine` 48 | - remove generated images, with `docker rmi imageID` (add the flag ‘-f’ if needed) 49 | - repeat previous steps for other tags, for example for 'latest' 50 | if it's the same of current tag 51 | - optional, logout from DockerHub with `docker logout` 52 | 53 | Note that images for 'latest' can be done/repeated even at any commit/push of code, 54 | but for milestones it's better to build/tag just after a git tag has been created 55 | (so it's aligned with source code). 56 | 57 | 58 | ### Verify published images/tags 59 | 60 | To test, run it: 61 | `docker run -d -p 8080:8000 -t $dockerhub_username/fastify-example:latest-alpine` 62 | and browse to [localhost:8080](http://localhost:8080), 63 | because container port has been redirected to host port 8080. 64 | Otherwise change the public (host) port from 8080 to 8000, and browse to: 65 | [localhost:8000](http://localhost:8000). 66 | 67 | Note that in some cases if NATS public server is not reachable 68 | (for example by Corporate IF firewalls), you need to pass to Docker 69 | the env var to disable that feature, with `-e “FEATURE_NATS_DISABLE=true”` 70 | before the image name in the command-line. 71 | 72 | Then stop that image but getting the list of running containers with: 73 | `docker container ls`, and stop with `docker kill containerID`. 74 | 75 | Last, to cleanup related images, use usual Docker commands, like: 76 | `docker rmi imageID`, maybe with the '-f' flag to force their deletion. 77 | 78 | ---- 79 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | /* eslint no-inner-declarations: "off" */ 23 | 24 | // application wrap, configure and export Fastify definitions for the current application 25 | // loaded as a Fastify plugin can't return value (it's discarded by the caller, even if assigned as return value), 26 | // so it seems better to load it in a normal way, but async now 27 | 28 | // load/init webapp constants and general utilities 29 | const k = require('./constants') 30 | const utils = require('./utils') 31 | 32 | /* 33 | // load environment specific variables from '.env' file (if any) into process.env ... 34 | const dotenv = require('dotenv') 35 | dotenv.config() 36 | 37 | utils.logToConsole(`Server starting for ${k.packageName}-v${k.packageVersion} ...`) 38 | 39 | // startup configuration constants 40 | const fastifyOptions = JSON.parse(k.fastifyOptionsString) 41 | const fastify = require('fastify')(fastifyOptions) 42 | */ 43 | 44 | // const assert = require('assert').strict 45 | const path = require('path') 46 | const resolve = path.resolve 47 | 48 | const publicFolderFromScript = path.normalize(path.join(k.projectFolderFromScript, 'public', path.sep)) 49 | 50 | // function that wraps the web application and related content 51 | async function app (fastify, options = {}) { 52 | if (!fastify) { 53 | throw new Error('Fastify instance must have a value') 54 | } 55 | 56 | // define an object to return, it could contain useful data/references, depending on needs 57 | let app = {} 58 | 59 | fastify.register(require('@fastify/view'), { 60 | engine: { 61 | ejs: require('ejs') 62 | }, 63 | includeViewExtension: true, 64 | templates: k.folders.templatesFolderName, 65 | layout: 'layout', // global layout, used in all ejs pages 66 | viewExt: 'ejs', 67 | options: { 68 | // async: true, // check later if useful here, and how to use ... 69 | filename: resolve(k.folders.templatesFolderName), 70 | views: [publicFolderFromScript] 71 | } 72 | }) 73 | 74 | fastify.register(require('@fastify/static'), { 75 | root: publicFolderFromScript, 76 | prefix: k.mappings.staticAssetsMapping // optional: default '/' 77 | }) 78 | 79 | // load all webapp features that are enabled: need to pass fastify instance and maybe some options 80 | // const features = require('./features')(fastify, null) 81 | // load it as a plugin 82 | // await fastify.register(require('./features')) 83 | // new, load as normal function, to be able to get its return value (discarded otherwise) 84 | const features = await require('./features')(fastify, null) 85 | // note that JSON conversion will discard some object types (functions, etc) 86 | fastify.log.debug(`Return values from features: ${JSON.stringify(features)}`) 87 | 88 | // load some publish/subscribe utility functions 89 | // const { publish, subscribe } = require('./pubsub') 90 | 91 | // define some routes 92 | // const routes = require('./route')(fastify, null) 93 | // new, load it as a plugin 94 | await fastify.register(require('./route')) 95 | 96 | // define some callback logic, called when the application has successfully initialized 97 | // moved in main server source, more useful than in app (no needed in tests, etc) 98 | // fastify.ready(() => { ... }) 99 | 100 | // return some values 101 | app = { 102 | results: true, 103 | features 104 | } 105 | return app 106 | } 107 | 108 | module.exports = app 109 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | /* eslint no-inner-declarations: "off" */ 23 | 24 | // main entry point for instancing and starting the server 25 | 26 | // const assert = require('assert').strict 27 | 28 | // TODO: later try to use fastify.NATS instead (published by my plugin) ... wip 29 | const NATS = require('nats') 30 | 31 | // load environment specific variables from '.env' file (if any) into process.env ... 32 | const dotenv = require('dotenv') 33 | dotenv.config() 34 | 35 | // load/init webapp constants and general utilities 36 | const k = require('./constants') 37 | const utils = require('./utils') 38 | 39 | utils.logToConsole(`Server starting for ${k.packageName}-v${k.packageVersion} ...`) 40 | 41 | // startup configuration constants 42 | const Fastify = require('fastify') 43 | const fastifyOptions = JSON.parse(k.fastifyOptionsString) 44 | const fastify = Fastify(fastifyOptions) 45 | 46 | // get the web application and instance/register as a plugin 47 | const App = require('./app') 48 | // fastify.register(App, fastifyOptions) 49 | // new, load as normal function, to be able to get its return value (discarded otherwise) 50 | // later call directly with await (requires top-level await) 51 | const app = App(fastify, fastifyOptions) 52 | .then((res) => { 53 | fastify.log.debug(`Return values from app: ${JSON.stringify(res)}`) 54 | }) 55 | 56 | // start the web application 57 | // note that to make it work (be exposed) when deployed in a container (Docker, etc) we need to listen not only to localhost but for example to all interfaces ... 58 | fastify.listen({ port: k.port, host: k.address }, (err, address) => { 59 | if (err) { 60 | fastify.log.error(err) 61 | process.exit(1) 62 | // throw err 63 | } 64 | utils.logToConsole(`Server listening on '${address}' ...`) 65 | // fastify.log.info(`Server listening on '${address}' ...`) 66 | }) 67 | 68 | // load some publish/subscribe utility functions 69 | const { publish, subscribe } = require('./pubsub') 70 | 71 | // define some callback logic, called when the application has successfully initialized 72 | // moved in main server source, more useful than in app (no needed in tests, etc) 73 | fastify.ready((err) => { 74 | if (err) throw err 75 | const msgPrintRoutesStart = `Printing Routes (env '${utils.currentEnv()}')` 76 | if (utils.isEnvProduction()) { 77 | fastify.log.info(`${msgPrintRoutesStart}, disabled in a production environment`) 78 | } else { 79 | const routes = fastify.printRoutes({ commonPrefix: false }) 80 | const msgPrintRoutesFull = `${msgPrintRoutesStart}, only for non production environments\n${routes}` 81 | fastify.log.info(msgPrintRoutesFull) 82 | // note that in this case it would be better to log to console (for a better formatting), for example with: 83 | utils.logToConsole(msgPrintRoutesFull) 84 | } 85 | 86 | // subscribe and publish a message to the queue, as a sample 87 | // assert(utils.isDefinedAndNotNull(fastify.NATS)) 88 | // assert(utils.isDefinedAndNotNull(fastify.nc)) 89 | const natsStringCodec = NATS.StringCodec() 90 | // const natsStringCodec = fastify.NATS.StringCodec() 91 | subscribe(fastify.nc, k.queueName, k.queueDisabled, null, natsStringCodec) 92 | publish(fastify.nc, k.queueName, k.queueDisabled, k.message, natsStringCodec) 93 | // later find a better way to reuse StringCodec as default, without having to pass as argument ... 94 | }) 95 | 96 | // log server startup, but note that by default logs are disabled in Fastify (even errors) ... 97 | fastify.log.info('Server Startup script successfully executed') 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-example 2 | 3 | [![Code Style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) 4 | [![Known Vulnerabilities](https://snyk.io//test/github/smartiniOnGitHub/fastify-example/badge.svg)](https://snyk.io//test/github/smartiniOnGitHub/fastify-example?targetFile=package.json) 5 | [![Apache 2.0 License](https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat)](./LICENSE) 6 | 7 | Example webapp with Fastify 8 | 9 | 10 | ## Setup and run 11 | 12 | To complete project setup, do: 13 | ``` 14 | npm install 15 | ``` 16 | 17 | and run with 18 | ``` 19 | npm start 20 | ``` 21 | 22 | then point your browser to [localhost:8000](http://localhost:8000) 23 | 24 | 25 | ## Setup and run with Docker 26 | 27 | Note that it's possible to let Docker do all inside a container, because 28 | all is described in a Dockerfile; see [Dockerfile-usage](./docs/Dockerfile-usage.md) 29 | for related commands. 30 | 31 | Of course you need a local installation of Docker (recent, if possible latest), 32 | but nothing other. 33 | 34 | 35 | ## Others 36 | 37 | To run a development server (with hot reload enabled) instead execute this: 38 | ``` 39 | npm run start:dev 40 | ``` 41 | 42 | For other custom commands look the 'scripts' section inside 'package.json'. 43 | 44 | 45 | ## Requirements 46 | 47 | Fastify ^4.5.2, Node.js 14 LTS (14.15.0) or later. 48 | 49 | 50 | ## Security 51 | 52 | Nothing known. 53 | 54 | 55 | ## Sources 56 | 57 | Source code is all inside main repo: 58 | [fastify-example](https://github.com/smartiniOnGitHub/fastify-example). 59 | 60 | Documentation generated from source code (API): 61 | [here](https://smartiniongithub.github.io/fastify-example/). 62 | 63 | 64 | ## Note 65 | 66 | Some features can be configured via environment variables; 67 | if a `.env` file is found in project root, all its contents 68 | will be loaded into environmental variables. 69 | Supported variables: 70 | - `FASTIFY_OPTIONS`, set Fastify main options at startup, as a JSON string; 71 | to change logging level for example use something like: `{ "logger": { "level": "debug" } }' 72 | and to remove logging for example use: '{ }' 73 | - `HTTP_PORT`, set default HTTP port for the server 74 | - `HTTP_ADDRESS`, set default HTTP address for the server 75 | - `NATS_SERVER_URL`, set the URL for the NATS server (if enabled), or plugin default 76 | - `WEBHOOK_SECRET_KEY`, set the secret key to require by exposed webhook 77 | - `FEATURE_PLATFORM_INFO_DISABLE`, to disable the logging of some info 78 | related to the current runtime platform like: 79 | Node.js version, OS name, Fastify version, Webapp version, etc ... 80 | - `FEATURE_CHECK_RUNTIME_ENV_DISABLE`, to disable (not load) related plugin; 81 | if enabled and current Node.js environment is not compatible with the one 82 | set in 'package.json' by default it will throw an exception, 83 | so the webapp will crash (to avoid running the webapp in a not compliant env) 84 | - `FEATURE_FAVICON_DISABLE`, to disable (not load) related plugin 85 | - `FEATURE_WEBHOOK_DISABLE`, to disable (not load) related plugin 86 | - `FEATURE_HEALTHCHECK_DISABLE`, to disable (not load) related plugin 87 | - `FEATURE_CLOUDEVENTS_DISABLE`, to disable (not load) related plugin; 88 | note that this by default uses a public NATS server so disable it 89 | if not needed or if that server is not reachable for corporate firewall policies 90 | - `FEATURE_CLOUDEVENTS_STRICT_DISABLE`, to disable strict mode in generated CloudEvents 91 | (if/when related plugin is enabled) 92 | - `FEATURE_CLOUDEVENTS_LOG_CONSOLE_DISABLE`, to disable CloudEvent serialization to console 93 | - `FEATURE_CLOUDEVENTS_LOG_FILE_DISABLE`, to disable CloudEvent serialization to console, 94 | by default true (so disabled); 95 | note that when enabled a new log file will be created at any run of the server, 96 | but previous (if present) will be overwritten, 97 | so any new run is fresh and old logs will be discarded 98 | - `FEATURE_NATS_DISABLE`, to disable (not load) related plugin 99 | if not specified default behavior will be applied. 100 | 101 | As a sample, by default (unless disabled) some messages will be sent to a NATS queue, 102 | when the web application has started and when a client ask for a page. 103 | NATS server by default (in related plugin) is a public one, 104 | [demo.nats.io](nats://demo.nats.io:4222). 105 | Pay attention in corporate networks, where that NATS server could NOT be reachable. 106 | 107 | 108 | ## License 109 | 110 | Licensed under [Apache-2.0](./LICENSE). 111 | 112 | ---- 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-example", 3 | "version": "4.0.0", 4 | "description": "Example web application with Fastify", 5 | "main": "src/server", 6 | "scripts": { 7 | "audit:log": "npm audit > ./temp/audit.log", 8 | "cache:cleanup": "npm cache clean --force", 9 | "cache:verify": "npm cache verify", 10 | "clean:install": "rm -rf ./package-lock.json ./node_modules/", 11 | "debug:dev": "nodemon --debug --watch src ./src/server", 12 | "debug": "node --inspect ./src/server", 13 | "dependency:log": "npm list > ./temp/dependencies.log", 14 | "docker:build:alpine": "docker build -t fastify-example -f Dockerfile.alpine .", 15 | "docker:build:distroless": "docker build -t fastify-example -f Dockerfile.distroless .", 16 | "docker:build": "docker build -t fastify-example .", 17 | "docker:clean": "docker rmi fastify-example", 18 | "docker:healthcheck-manual": "docker exec -it fastify-example node node_modules/fastify-healthcheck/src/healthcheck http://localhost:8000/health", 19 | "docker:images": "docker images \"fastify-example*\"", 20 | "docker:inspect:alpine": "docker exec -it fastify-example ash", 21 | "docker:inspect": "docker exec -it fastify-example bash", 22 | "docker:log": "docker logs --follow --tail=1000 fastify-example", 23 | "docker:process": "docker ps --filter name=fastify-example", 24 | "docker:run": "docker run --init --rm --name fastify-example -d -p 8000:8000 -t fastify-example", 25 | "docker:start": "npm run docker:run", 26 | "docker:status": "docker inspect --format '{{ json .State.Health }}' fastify-example", 27 | "docker:stop": "docker kill fastify-example", 28 | "docs:clean": "rm -rf ./out/*", 29 | "docs:generate": "npx jsdoc -c .jsdoc.json -R README.md", 30 | "docs": "npm run docs:clean && npm run docs:generate", 31 | "healthcheck-manual": "node node_modules/fastify-healthcheck/src/healthcheck http://localhost:8000/health", 32 | "license-check:log": "npx legally | tee ./temp/license-check.log", 33 | "license-check": "npx legally", 34 | "license-checker-check-allowed:log": "npm run license-checker-check-allowed | tee ./temp/license-checker-check-allowed.log", 35 | "license-checker-check-allowed": "npx license-checker --production --onlyAllow='Apache-2.0;BSD-2-Clause;BSD-3-Clause;MIT;ISC;Artistic-2.0'", 36 | "license-checker:log": "npm run license-checker | tee ./temp/license-checker.log", 37 | "license-checker": "npx license-checker --production", 38 | "lint:fix": "standard --fix", 39 | "lint:log": "npm run lint > ./temp/lint-standard.log", 40 | "lint:standard": "standard --verbose", 41 | "lint": "npm run lint:standard", 42 | "postinstall": "", 43 | "prepublish": "", 44 | "prestart": "", 45 | "pretest": "", 46 | "start:debug": "node --inspect-brk ./src/server", 47 | "start:dev": "nodemon --watch src ./src/server", 48 | "start:simple": "node ./src/server-simple", 49 | "start": "node ./src/server", 50 | "test:clean": "rm -rf .nyc_output/* ./coverage/*", 51 | "test:coverage:all": "npm run test:unit -- --cov", 52 | "test:coverage": "npm run test:unit -- --cov --coverage-report=html", 53 | "test:dev": "npm run test:clean && npm run test:unit:dev", 54 | "test:unit:debug": "tap -T --node-arg=--inspect-brk", 55 | "test:unit:dev": "tap --watch", 56 | "test:unit:with-snapshot": "cross-env TAP_SNAPSHOT=1 npm run test:unit", 57 | "test:unit": "tap", 58 | "test": "npm run lint && npm run test:unit" 59 | }, 60 | "dependencies": { 61 | "@fastify/formbody": "^7.0.1", 62 | "@fastify/jwt": "^7.0.0", 63 | "@fastify/static": "^7.0.0", 64 | "@fastify/view": "^7.1.0", 65 | "dotenv": "^16.0.1", 66 | "ejs": "^3.1.8", 67 | "fast-json-stringify": "^5.1.0", 68 | "fastify-check-runtime-env": "^4.0.1", 69 | "fastify-cloudevents": "^4.0.0", 70 | "fastify-favicon": "^4.1.0", 71 | "fastify-healthcheck": "^4.1.0", 72 | "fastify-nats-client": "^4.1.0", 73 | "fastify-webhook": "^4.0.1", 74 | "fastify": "^4.5.2", 75 | "is-docker": "^2.2.1", 76 | "make-promises-safe": "^5.1.0" 77 | }, 78 | "devDependencies": { 79 | "cross-env": "^7.0.3", 80 | "jsdoc": "^4.0.3", 81 | "nodemon": "^3.0.0", 82 | "simple-get": "^4.0.1", 83 | "standard": "^17.0.0", 84 | "tap": "^18.0.0" 85 | }, 86 | "peerDependencies": {}, 87 | "engines": { 88 | "node": ">=14.15.0" 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/smartiniOnGitHub/fastify-example.git" 93 | }, 94 | "keywords": [ 95 | "node", 96 | "example", 97 | "fastify", 98 | "ejs", 99 | "es10", 100 | "es2019" 101 | ], 102 | "author": "Sandro Martini ", 103 | "license": "Apache-2.0" 104 | } 105 | -------------------------------------------------------------------------------- /src/route.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | 23 | const k = require('./constants') 24 | const utils = require('./utils') 25 | 26 | // sample data for pages and responses 27 | const commonPageData = { 28 | projectName: k.packageName, 29 | projectVersion: k.packageVersion, 30 | environment: utils.currentEnv(), 31 | assets: k.mappings.staticAssetsMapping, 32 | welcome: 'Hello from EJS Templates', 33 | sampleRoutes: null, 34 | pluginRoutes: null 35 | } 36 | 37 | // load some publish/subscribe utility functions 38 | const { publish, subscribe } = require('./pubsub') 39 | 40 | // manually define the list of some exposed routes, with some description 41 | const sampleRoutes = [ 42 | { link: '/', url: '/', description: 'Home page (async)' }, 43 | { link: 'time', url: '/time', description: 'Sample API call that returns the current server time, as timestamp and in ISO format (async)' }, 44 | { link: 'error', url: '/error', description: 'Sample route that always returns an error (async)' }, 45 | { link: 'simple', url: '/simple', description: 'Sample EJS simple template page (async)' }, 46 | { link: 'template', url: '/template', description: 'Sample EJS template page using the same template of root page (async)' } 47 | // ].sort((a, b) => a.link.localeCompare(b.link)) 48 | ].sort(utils.compareProperties('link')) // opt. add sort order, 'asc' (by default) or 'desc' 49 | // manually define the list of some routes exposed by some loaded plugins 50 | const pluginRoutes = [ 51 | { link: 'static', url: '/static/', description: "Serve static resources, sample (by default 'index.html' will be served, by 'fastify-static' plugin" }, 52 | { link: 'favicon', url: '/favicon.ico', description: "Serve the favicon, by 'fastify-favicon' plugin" }, 53 | { link: 'custom-webhook', url: '/custom-webhook', description: "Expose a sample webhook (via HTTP POST), by 'fastify-webhook' plugin" }, 54 | { link: 'healthcheck', url: '/health', description: "Expose an healthcheck, by 'fastify-healthcheck' plugin" } 55 | ].sort(utils.compareProperties('link')) // opt. add sort order, 'asc' (by default) or 'desc' 56 | 57 | // define some routes 58 | // note that some routes here are normal (non-async) but others are async ... 59 | async function routes (fastify, options = {}) { 60 | if (!fastify) { 61 | throw new Error('Fastify instance must have a value') 62 | } 63 | 64 | // define the root route, async (but normal is good the same) 65 | fastify.get('/', async (request, reply) => { 66 | // utils.logRoute(request) 67 | // publish a message in the queue, as a sample 68 | await publish(fastify.nc, k.queueName, k.queueDisabled, 69 | `Hello World, from the root page of a Fastify web application at '${k.hostname}'!` 70 | ) 71 | // with a layout set it's better to use a partial template here, to avoid duplication of tags ... 72 | // return reply.view('index', { // full index page no more used at the moment, but keep for reference/example ... 73 | return reply.view('fragment-index', { 74 | ...commonPageData, 75 | title: 'Home', 76 | welcome: 'Work-In-Progress/Prototype webapp', 77 | sampleRoutes, 78 | pluginRoutes 79 | }) 80 | // ensure to return something at the end of an async function 81 | }) 82 | // example route to return current timestamp, in async way 83 | fastify.get('/time', async (request, reply) => { 84 | const now = new Date() 85 | const timestamp = now.getTime() 86 | // publish a message in the queue, as a sample 87 | await publish(fastify.nc, k.queueName, k.queueDisabled, 88 | `Ask for server time: timestamp is ${timestamp} at '${k.hostname}'` 89 | ) 90 | return { 91 | timestamp, 92 | time: now.toISOString() 93 | } 94 | }) 95 | // example route to return a page from a template, async (but normal is good the same) 96 | fastify.get('/simple', async (request, reply) => { 97 | // publish a message in the queue, as a sample 98 | await publish(fastify.nc, k.queueName, k.queueDisabled, 99 | `Ask for a template page at '${k.hostname}'` 100 | ) 101 | return reply.view('simple', { 102 | ...commonPageData, 103 | title: 'Simple but full Template' 104 | }) 105 | }) 106 | // example route to return a page from a template, async (but normal is good the same) 107 | // note that same this uses the same template of the root page, but with less template variables 108 | fastify.get('/template', async (request, reply) => { 109 | // publish a message in the queue, as a sample 110 | await publish(fastify.nc, k.queueName, k.queueDisabled, 111 | `Ask for a template page at '${k.hostname}'` 112 | ) 113 | // with a layout set it's better to use a partial template here, to avoid duplication of tags ... 114 | return reply.view('fragment-template', { 115 | ...commonPageData, 116 | title: 'Home page from a template' 117 | }) 118 | }) 119 | // example route, to always generate an error 120 | fastify.get('/error', async (req, reply) => { 121 | reply.code(500) 122 | const err = new Error() 123 | err.message = 'Error Message' 124 | err.statusCode = reply.code 125 | err.description = 'Verbose Error description...' 126 | // publish a message in the queue, as a sample 127 | await publish(fastify.nc, k.queueName, k.queueDisabled, 128 | `Ask for error page at '${k.hostname}'` 129 | ) 130 | return err 131 | }) 132 | 133 | // add other (related) routes from a dedicated source, as a sample 134 | // require('./route-info')(fastify, { routesList: sampleRoutes }) 135 | // new, load it as a plugin 136 | fastify.register(require('./route-info'), { routesList: sampleRoutes }) 137 | } 138 | 139 | module.exports = routes 140 | -------------------------------------------------------------------------------- /src/route-info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | 23 | const k = require('./constants') 24 | const utils = require('./utils') 25 | 26 | // define some routes for general information 27 | // note that some routes here are normal (non-async) but others are async ... 28 | async function routes (fastify, { routesList = [] } = {}) { 29 | // add some routes 30 | 31 | // example route to return some info on current package/version, in async way 32 | // this exposes even framework version, which is a sensitive information (for security), 33 | // so in a real app protect it or remove from here, or send when related config flag is not disabled 34 | // to guarantee that the async route always return a value, 35 | // the code called function inside the helper function is wrapped in a try/catch 36 | fastify.get('/info/app', async (request, reply) => { 37 | const app = infoAPP() || {} 38 | // return utils.getFromEither(app, { throwOnError: true, value: {} }) 39 | return utils.getFromEither(app) 40 | }) 41 | // example route to return some info on current scm (if available), in async way 42 | // this exposes sensitive information (for security), in a real app protect it 43 | fastify.get('/info/scm', async (request, reply) => { 44 | const scm = await infoSCM() || {} 45 | return utils.getFromEither(scm) 46 | }) 47 | // example route to return some info on the operating system (os), in async way 48 | // this exposes sensitive information (for security), in a real app protect it 49 | fastify.get('/info/os', async (request, reply) => { 50 | const os = infoOS() || {} 51 | return utils.getFromEither(os) 52 | }) 53 | // example route to return some info (gruping other info results), in async way 54 | // this exposes sensitive information (for security), in a real app protect it 55 | fastify.get('/info/all', async (request, reply) => { 56 | const all = {} 57 | try { 58 | all.app = infoAPP() 59 | all.scm = await infoSCM() 60 | all.os = infoOS() 61 | } catch (e) { 62 | fastify.log.error(e) 63 | } 64 | return all 65 | }) 66 | 67 | // add routes to a convenience list of routes, to generate content/links in the home page 68 | routesList.push( 69 | { link: 'info:app', url: '/info/app', description: 'Sample API to show some info about application package version etc (async)' }, 70 | { link: 'info:scm', url: '/info/scm', description: 'Sample API to show some info about source control system (if any): branch, commit hash, version, etc (async)' }, 71 | { link: 'info:os', url: '/info/os', description: 'Sample API to show some info about operating system (async)' }, 72 | { link: 'info:all', url: '/info/all', description: 'Sample API to show all info (async)' } 73 | ) 74 | routesList.sort() 75 | } 76 | 77 | // returns some info about the current application, and from its runtime process 78 | // returned value is an "either" object, containing err (error message) and data, but only one of them is not null 79 | function infoAPP (options = {}) { 80 | let err = null 81 | let data = {} 82 | try { 83 | data.app = { 84 | environment: utils.fromEnv('ENVIRONMENT'), 85 | frameworkVersion: k.fastifyVersion, 86 | name: k.packageName, 87 | version: k.packageVersion 88 | } 89 | data.runtime = { 90 | platform: utils.platformName(), 91 | nodejs: utils.runtimeVersion(), 92 | mode: utils.currentEnv(), 93 | pid: utils.pid(), 94 | uptime: utils.uptimeProcess(), 95 | memory_free: utils.osMemoryFree() 96 | } 97 | // throw new Error('Whoops!') // example, to test the right behavior of the either return value 98 | } catch (e) { 99 | err = e.message 100 | data = null 101 | } 102 | return { err, data } 103 | } 104 | 105 | // returns some info about the source control system (scm) on these sources, if available 106 | // returned value is an "either" object, containing err (error message) and data, but only one of them is not null 107 | // this is an async function 108 | async function infoSCM (options = {}) { 109 | // check git related data by executing it 110 | let err = null 111 | let data = {} 112 | try { 113 | // data.description = await utils.gitVersion().catch(e => err = e.message)), // sample catch for error in a single promise 114 | // simpler, catch errors from all promises via try/catch in callers ... 115 | data.description = await utils.gitVersion() 116 | data.branch = await utils.gitBranch() 117 | data.hashShort = await utils.gitHashShort() 118 | data.hashFull = await utils.gitHashFull() 119 | // throw new Error('Whoops!') // example, to test the right behavior of the either return value 120 | } catch (e) { 121 | err = e.message 122 | data = null 123 | } 124 | return { err, data } 125 | } 126 | 127 | // returns some info on the operating system (os) 128 | // returned value is an "either" object, containing err (error message) and data, but only one of them is not null 129 | function infoOS ({ includeEnv = false } = {}) { 130 | let err = null 131 | let data = {} 132 | try { 133 | data.platform = utils.osPlatform() 134 | data.version = utils.osVersion() 135 | data.arch = utils.osArch() 136 | data.cpu_cores = utils.osCPU().length 137 | data.memory = utils.osMemoryTotal() 138 | data.host = utils.osHost() 139 | data.uptime = utils.osUptime() 140 | if (includeEnv === true) { 141 | data.env = utils.envVars() // too many sensitive info here, enable only when needed 142 | } 143 | // throw new Error('Whoops!') // example, to test the right behavior of the either return value 144 | } catch (e) { 145 | err = e.message 146 | data = null 147 | } 148 | return { err, data } 149 | } 150 | 151 | // TODO: refactor to simplify the usage of a value as either: {err , data } and related creation and data extraction ... wip 152 | 153 | module.exports = routes 154 | -------------------------------------------------------------------------------- /src/features.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | /* eslint no-inner-declarations: "off" */ 23 | 24 | // features wrap: depending on enabled feature flags, load and configure each one 25 | // loaded as a Fastify plugin can't return value (it's discarded by the caller, even if assigned as return value), 26 | // so it seems better to load it in a normal way, but async now 27 | 28 | // const assert = require('assert').strict 29 | 30 | const k = require('./constants') 31 | const utils = require('./utils') 32 | 33 | // configuration for enabled/disabled features 34 | const featuresEnabled = { 35 | platformInfo: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_PLATFORM_INFO_DISABLE'), false), 36 | checkRuntimeEnv: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_CHECK_RUNTIME_ENV_DISABLE'), false), 37 | favicon: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_FAVICON_DISABLE'), false), 38 | webhook: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_WEBHOOK_DISABLE'), false), 39 | healthcheck: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_HEALTHCHECK_DISABLE'), false), 40 | cloudevents: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_CLOUDEVENTS_DISABLE'), false), 41 | cloudeventsStrictMode: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_CLOUDEVENTS_STRICT_DISABLE'), false), 42 | cloudeventsLogConsole: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_CLOUDEVENTS_LOG_CONSOLE_DISABLE'), false), 43 | cloudeventsLogFile: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_CLOUDEVENTS_LOG_FILE_DISABLE'), true), 44 | nats: utils.featureIsEnabled(true, utils.fromEnv('FEATURE_NATS_DISABLE'), false) 45 | } 46 | 47 | // load some publish/subscribe utility functions 48 | const { publish, subscribe } = require('./pubsub') 49 | 50 | // features is a function because I need to pass fastify instance, and some configuration options 51 | // otherwise implement as a class and pass those arguments in its constructor) 52 | async function features (fastify, options = {}) { 53 | if (!fastify) { 54 | throw new Error('Fastify instance must have a value') 55 | } 56 | 57 | // define an object to return, it could contain useful data/references, depending on features enabled 58 | const features = {} 59 | 60 | const featuresEnabledMsg = `Webapp features enabled: '${utils.dumpObject(featuresEnabled, { method: 'stringify' })}'` 61 | utils.logToConsole(featuresEnabledMsg) 62 | 63 | let ceLogFile = null // defined here because I need it visible in two unrelated code blocks 64 | 65 | if (featuresEnabled.platformInfo) { 66 | features.platformInfo = { // sample to return some feature data 67 | nodejs: utils.runtimeVersion(), 68 | os: utils.platformName(), 69 | webappName: `${k.packageName}-v${k.packageVersion}`, 70 | framework: `Fastify-v${k.fastifyVersion}` 71 | } 72 | // log some platform info 73 | fastify.log.info(`Node.js ${features.platformInfo.nodejs}, running on OS: ${features.platformInfo.os}`) 74 | // log some package and framework info 75 | fastify.log.info(`Webapp ${features.platformInfo.webappName}, running on ${features.platformInfo.framework}`) 76 | // log enabled features of the webapp 77 | fastify.log.info(featuresEnabledMsg) 78 | } 79 | 80 | if (featuresEnabled.checkRuntimeEnv) { 81 | // check if current Node.js runtime env is compatible 82 | // with requirements in 'package.json', or an exception will the thrown 83 | const engines = require('../package.json').engines 84 | fastify.register(require('fastify-check-runtime-env'), { 85 | nodeVersionCheckAtStartup: true, 86 | // nodeVersionExpected: '<=10.13.0 >=200.0.0', // sample, to raise error 87 | nodeVersionExpected: engines.node, 88 | // onCheckMismatch: 'warning' // log a message 89 | // onCheckMismatch: 'exception' // throw an exception // same as default 90 | onCheckMismatch: 'exit' // exit from current process with an error code 91 | }) 92 | // features.checkRuntimeEnv = {} // sample 93 | } 94 | 95 | if (featuresEnabled.favicon) { 96 | // fastify-favicon, example with null or empty options, using only plugin default options 97 | // features.favicon = {} // sample 98 | // fastify.register(require('fastify-favicon')) 99 | // example with custom path, usually relative to project root (without or with the final '/' char), but could be absolute 100 | fastify.register(require('fastify-favicon'), { 101 | path: k.imagesFolderFromScript 102 | }) 103 | } 104 | 105 | if (featuresEnabled.webhook) { 106 | // fastify-webhook, example with null or empty options, using only plugin default options 107 | // fastify.register(require('fastify-webhook')) 108 | // features.webhook = {} // sample 109 | const webhookHandlers = require('fastify-webhook/src/handlers') // get plugin handlers (optional) 110 | const webhookPlugin = require('fastify-webhook') 111 | fastify.register(webhookPlugin, { 112 | url: k.mappings.webhookMapping, 113 | handler: webhookHandlers.echo, 114 | // disableWebhook: false, // same as default 115 | enableGetPlaceholder: true, // as a sample 116 | secretKey: process.env.WEBHOOK_SECRET_KEY // optional: || '' , or || null 117 | }) 118 | fastify.log.info('Webhook registered with custom options') 119 | } 120 | 121 | if (featuresEnabled.healthcheck) { 122 | // fastify-healthcheck, example with null or empty options, using only plugin default options 123 | // features.healthcheck = {} // sample 124 | // enable only the option to expose even process uptime, as a sample 125 | fastify.register(require('fastify-healthcheck'), { 126 | exposeUptime: true 127 | }) 128 | } 129 | 130 | if (featuresEnabled.nats) { 131 | // example to connect to a nats queue using related plugin 132 | features.nats = {} // put all values of this feature inside a specific object 133 | await fastify.register(require('fastify-nats-client'), { 134 | enableDefaultNATSServer: true, // sample, to connect by default to public demo server 135 | drainOnClose: true, // sample, to drain last messages at plugin close 136 | natsOptions: k.natsQueueOptions 137 | }) 138 | // after, perform some checks 139 | // assert(utils.isDefinedAndNotNull(fastify.NATS)) 140 | // assert(utils.isDefinedAndNotNull(fastify.nc)) 141 | if (utils.isDefinedAndNotNull(fastify.nc) && utils.isNotNull(fastify.nc.currentServer)) { 142 | utils.logToConsole(`Connected to the queue server at: '${fastify.nc.getServer()}'`) 143 | } 144 | // check later if useful (once working) ... 145 | // features.nats.library = fastify.NATS 146 | // features.nats.connection = fastify.nc 147 | if (utils.isDefinedAndNotNull(fastify.nc)) { 148 | features.nats.currentServer = fastify.nc.getServer() 149 | } 150 | if (utils.isDefinedAndNotNull(fastify.NATS)) { 151 | features.nats.stringCodec = fastify.NATS.StringCodec() 152 | features.nats.jsonCodec = fastify.NATS.JSONCodec() 153 | } 154 | } 155 | 156 | if (featuresEnabled.cloudevents) { 157 | // example usage of fastify-cloudevents plugin 158 | // features.cloudevents = {} // put all values of this feature inside a specific object 159 | // define a sample id generator here 160 | // const pid = require('process').pid 161 | function * idCounterExample () { 162 | let counter = 0 163 | while (true) { 164 | yield `${counter++}` 165 | } 166 | } 167 | // instance the generator, to use everywhere here 168 | const gen = idCounterExample() 169 | // add a sample logging callback 170 | function loggingCallback (ce) { 171 | // const dump = fastify.CloudEvent.dumpObject(ce, 'ce') 172 | // utils.logToConsole(`CloudEvent dump, ${dump}`) 173 | // serialize the event, as a sample in all supported ways, but enable only one here 174 | // const ser = fastify.CloudEvent.serializeEvent(ce) 175 | // const ser = ce.serialize() 176 | const ser = fastify.cloudEventSerializeFast(ce) 177 | if (featuresEnabled.cloudeventsLogConsole) { 178 | utils.logToConsole(`CloudEvent serialized, ${ser}`) 179 | } 180 | if (featuresEnabled.cloudeventsLogFile) { 181 | ceLogFile.write(ser + '\n') 182 | } 183 | publish(fastify.nc, k.queueName, k.queueDisabled, ser) 184 | } 185 | // override cloudEventOptions strict mode with a feature flag, 186 | // so in this way I don't need to add a dependency from constants to utils 187 | k.cloudEventOptions.strict = utils.featureIsEnabled(true, utils.fromEnv('FEATURE_CLOUDEVENTS_STRICT_DISABLE'), false) 188 | // create the log file 189 | if (featuresEnabled.cloudeventsLogFile) { 190 | const fs = require('fs') 191 | ceLogFile = fs.createWriteStream(`./logs/${k.packageName}.json.log`) 192 | utils.logToConsole('CloudEvents log file Created') 193 | // handle log file close when the webapp will be closed 194 | // triggered when `fastify.close()` is invoked to stop the server, but not with C or webapp server stop 195 | fastify.addHook('onClose', (instance, done) => { 196 | ceLogFile.end() 197 | utils.logToConsole('CloudEvents log file Closed') 198 | done() 199 | }) 200 | } 201 | // fastify-cloudevents, example with only some most-common options 202 | fastify.register(require('fastify-cloudevents'), { 203 | serverUrl: k.serverUrl, 204 | serverUrlMode: k.serverUrlMode, 205 | baseNamespace: k.baseNamespace, 206 | // idGenerator: gen, 207 | onErrorCallback: loggingCallback, 208 | onRequestCallback: loggingCallback, 209 | onResponseCallback: loggingCallback, 210 | // onSendCallback: loggingCallback, 211 | onTimeoutCallback: loggingCallback, 212 | cloudEventOptions: k.cloudEventOptions 213 | }) 214 | } 215 | 216 | fastify.log.info('Webapp features loaded') 217 | 218 | // return some values 219 | return features 220 | } 221 | 222 | module.exports = features 223 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | /*#region Variables definitions */ 3 | :root { 4 | --black-ocean-color:#404040; 5 | --blue-ocean-color:#4A8FE2; 6 | --blue-ocean-light-color:#4C9BD5; 7 | --blueish-alternate-color:#297CA3; 8 | --blueish-color:#007ACC; 9 | --blueish-dark-pale-color:#3B3C5B; 10 | --blueish-light-alternate-color:#DCEDF5; 11 | --blueish-light-pale-color:#8283AC; 12 | --blueish-pale-color:#595A7B; 13 | --confirmed-color:lightgreen; 14 | --gray-dark-color:#252627; 15 | --gray-light-color:#F6F6F6; 16 | --gray-mid-color:#F6F6F6; 17 | --gray-ocean-color:#979797; 18 | --grayish-pale-color:#FBFAFB; 19 | --green-ocean-color:#58A700; 20 | --green-ocean-light-color:#8EBF63; 21 | --greenish-alternate-color:#56C25C; 22 | --greenish-color:#4D8618; 23 | --greenish-pale-color:#CAE8DE; 24 | --invalid-color:pink; 25 | --neutral-dark-color:#000000; 26 | --neutral-light-color:#ffffff; 27 | --orangish-color:#F15D2A; 28 | --purpleish-color:#3F0E40; 29 | --red-ocean-color:#C4000A; 30 | --red-ocean-light-color:#D95F5C; 31 | --reddish-alternate-color:#CD2553; 32 | --reddish-color:#FF0000; 33 | --sugar-paper-color:#56C2C2; 34 | --white-ocean-color:#FFFFFF; 35 | --yellow-ocean-color:#F59700; 36 | --yellow-ocean-light-color:#F6B44A; 37 | --yellowish-alternate-color:#C2BE55; 38 | --yellowish-color:#FFDE00; 39 | --yellowish-light-alternate-color:#F5F4DC; 40 | --yellowish-pale-color:#EFE3C4; 41 | 42 | --main-bg-color:var(--sugar-paper-color); 43 | --main-dark-bg-color:var(--gray-dark-color); 44 | --main-fg-color:var(--neutral-dark-color); 45 | --main-light-bg-color:var(--orangish-color); 46 | 47 | --test-color:var(--reddish-alternate-color); 48 | } 49 | 50 | /*#endregion*/ 51 | 52 | 53 | /*#region General styles */ 54 | html { 55 | -webkit-box-sizing:border-box; 56 | box-sizing:border-box; 57 | } 58 | *, *:before, *:after { 59 | -webkit-box-sizing:inherit; 60 | box-sizing:inherit; 61 | } 62 | * { 63 | border:0; 64 | margin:0; 65 | padding:0; 66 | } 67 | html, body { 68 | background-color:var(--neutral-light-color); 69 | color:var(--main-fg-color); 70 | font-family:"Open Sans", "Helvetica Neue", "Calibri Light", Roboto, Arial, sans-serif; 71 | font-weight:normal; 72 | letter-spacing:0.02em; 73 | margin:0; 74 | padding:0; 75 | } 76 | header, nav, footer, section { 77 | padding:0; 78 | } 79 | 80 | #app, container, .container-fluid { 81 | padding:0; 82 | } 83 | 84 | svg { 85 | fill:currentColor; 86 | } 87 | 88 | /*#endregion*/ 89 | 90 | 91 | /*#region Page layout */ 92 | 93 | /* 94 | #page-body, #page-content, header, nav, aside, footer, 95 | .header, .main, .navigation, .primary, .secondary, .content, .footer { 96 | } 97 | */ 98 | 99 | aside, .primary, .secondary { 100 | display:none; 101 | } 102 | 103 | header, .header { 104 | align-content:space-between; 105 | align-items:center; 106 | background-color:var(--blueish-color); 107 | display:flex; 108 | flex-direction:column; 109 | /* flex-wrap:wrap; */ 110 | font-size:1em; 111 | font-weight:400; 112 | justify-content:center; 113 | padding:0 0 0.4em 0; 114 | } 115 | 116 | footer, .footer { 117 | align-content:space-between; 118 | align-items:center; 119 | background-color:var(--orangish-color); 120 | color:var(--gray-dark-color); 121 | display:flex; 122 | flex-direction:row; 123 | /* flex-wrap:wrap; */ 124 | font-size:0.8em; 125 | font-weight:100; 126 | justify-content:center; 127 | padding:0.4em; 128 | text-transform:capitalize; 129 | } 130 | 131 | nav, .navigation, 132 | .navbar { 133 | flex-wrap:wrap; 134 | font-weight:400; 135 | left:0; 136 | margin:0 0 0.25em 0; 137 | padding:0 1em 0 1em; 138 | text-align:center; 139 | text-transform:capitalize; 140 | top:0; 141 | z-index:100000; 142 | } 143 | nav ul, .navigation ul, 144 | footer ul, .footer ul { 145 | list-style:none; 146 | margin-bottom:0; 147 | text-align:center; 148 | align-items:center; 149 | display:flex; 150 | flex-direction:row; 151 | } 152 | nav li, .navigation li { 153 | letter-spacing:normal; 154 | min-width:3em; 155 | vertical-align:middle; 156 | } 157 | nav a, nav a:link, nav a:visited, 158 | nav ul li a, nav ul li a:visited, 159 | .navigation a, .navigation a:link, .navigation a:visited { 160 | color:var(--neutral-light-color); 161 | opacity:0.8; 162 | padding:0.2em; 163 | text-decoration:none; 164 | } 165 | nav a, nav ul li a, 166 | footer a, footer ul li a, 167 | .footer a, .footer ul li a { 168 | color:var(--main-fg-color); 169 | } 170 | /* 171 | nav a:hover, nav ul li a:hover { 172 | opacity:1; 173 | } 174 | nav ul li:hover, nav ul li.active { 175 | } 176 | */ 177 | nav a:hover, nav ul li a:hover { 178 | /* background-color:var(--main-dark-bg-color); */ 179 | /* 180 | background-color:var(--orangish-color); 181 | color:var(--neutral-light-color); 182 | */ 183 | mix-blend-mode: difference; 184 | opacity:1; 185 | } 186 | 187 | footer ul li, .footer ul li { 188 | min-width:1em; 189 | padding:1px; 190 | } 191 | footer ul, .footer ul { 192 | list-style:none; 193 | padding:0; 194 | text-align:center; 195 | } 196 | footer a, footer a:link, footer ul li a:visited, 197 | .footer a, .footer a:link, .footer ul li a:visited { 198 | color:var(--main-fg-color); 199 | opacity:0.8; 200 | text-decoration:none; 201 | } 202 | footer a, .footer a, 203 | footer ul li, .footer ul li { 204 | text-transform:none; 205 | } 206 | footer a:hover, footer ul li a:hover { 207 | color:var(--neutral-light-color); 208 | /* mix-blend-mode: difference; */ 209 | opacity:1; 210 | } 211 | footer ul li:after, .footer ul li:after { 212 | content:"~"; 213 | padding-right:0.5em; 214 | } 215 | footer p, .footer p { 216 | text-align:center; 217 | } 218 | 219 | #logo img { 220 | display:inline-block; 221 | max-height:1.4em; 222 | min-height:1em; 223 | } 224 | 225 | .fixedTop, .fixedTop > header, .fixedTop > nav { 226 | overflow:hidden; 227 | position:fixed; 228 | top:0; 229 | width:100%; 230 | } 231 | 232 | /* get the value from a variable or if not defined (like here) use the given default value */ 233 | .fixedTopContent, .fixedTopContent > #content { 234 | margin-top:var(--fixedTop-height, 5em) 235 | } 236 | 237 | /* it requires even some JavaScript code to work properly */ 238 | .stickyTop { 239 | position:fixed; 240 | top:0; 241 | width:100%; 242 | } 243 | .stickyTop + .content { 244 | padding-top:0.2em; 245 | } 246 | 247 | .flexBoxContainer { 248 | -ms-flex-align:center; 249 | -ms-flex-flow:row wrap; 250 | -ms-flex-line-pack:center; 251 | -ms-flex-pack:justify; 252 | -webkit-box-align:center; 253 | -webkit-box-direction:normal; 254 | -webkit-box-orient:horizontal; 255 | -webkit-box-pack:justify; 256 | align-content:center; 257 | align-items:center; 258 | display:-ms-flexbox; 259 | display:-webkit-box; 260 | display:flex; 261 | flex-flow:row wrap; 262 | justify-content:space-between; 263 | margin:0; 264 | } 265 | .flexBoxContainer > div { 266 | padding:0.5em; 267 | } 268 | .flexBoxContainer > .flexBox { 269 | -ms-flex:1 1 auto; 270 | -webkit-box-flex:1; 271 | flex:1 1 auto; 272 | text-overflow:ellipsis; 273 | } 274 | 275 | .modal { 276 | z-index:1000000; 277 | } 278 | 279 | /*#endregion*/ 280 | 281 | 282 | /*#region Others */ 283 | h1, h2, h3, h4 { 284 | font-size:1em; 285 | text-align:center; 286 | } 287 | h1 { 288 | color:var(--main-fg-color); 289 | font-size:2em; 290 | font-weight:700; 291 | margin:0.8em 0 0.25em 0; 292 | text-transform:uppercase; 293 | } 294 | h2 { 295 | color:var(--main-fg-color); 296 | font-size:1.78em; 297 | font-weight:700; 298 | margin:0.6em 0 0.2em 0; 299 | text-transform:uppercase; 300 | } 301 | h3 { 302 | color:var(--main-fg-color); 303 | font-size:1.44em; 304 | font-weight:700; 305 | margin:0.4em 0 0.2em 0; 306 | } 307 | h4 { 308 | color:var(--main-fg-color); 309 | font-size:1em; 310 | font-weight:700; 311 | margin:0.2em 0 0.2em 0; 312 | } 313 | h5, h6 { 314 | font-size:0.56em; 315 | } 316 | 317 | hr { 318 | background:currentColor; 319 | height:2px; 320 | margin:0.2em 0 0.2em 0; 321 | } 322 | 323 | /* To avoid the 300 ms delay on mobile devices */ 324 | a[href], button { 325 | touch-action:manipulation; 326 | } 327 | 328 | /* input[type=text] { */ 329 | input:not([type=submit]):not([type=file]) { 330 | border:1px solid var(--gray-mid-color); 331 | } 332 | 333 | table { 334 | width:100%; 335 | } 336 | th { 337 | background-color:var(--gray-light-color); 338 | text-align:center; 339 | } 340 | table, tr, td, th { 341 | border:1px solid black; 342 | padding:0.25em; 343 | } 344 | tr:hover { 345 | background-color:var(--yellowish-light-alternate-color); 346 | } 347 | 348 | /*#endregion*/ 349 | 350 | 351 | /*#region Custom styles */ 352 | .container, .wrapper { 353 | padding-left:0.1em; 354 | padding-right:0.1em; 355 | } 356 | .row { 357 | margin:0; 358 | } 359 | 360 | .page-header, .page-footer { 361 | background-color:var(--main-bg-color); 362 | text-align:center; 363 | } 364 | 365 | tr.selected { 366 | background-color:var(--blueish-light-alternate-color); 367 | font-weight:700; 368 | } 369 | 370 | .my-panel { 371 | padding:1em; 372 | } 373 | .my-form, input.my-form { 374 | padding-left:1em; 375 | padding-right:1em; 376 | } 377 | 378 | /*#endregion*/ 379 | 380 | 381 | /*#region Some Angular-related styles overrides */ 382 | input.ng-invalid { 383 | background-color:var(--invalid-color) !important; 384 | } 385 | /* 386 | input.ng-valid { 387 | / * background-color:var(--confirmed-color) !important; * / 388 | } 389 | */ 390 | input.ng-dirty { 391 | border:var(--invalid-color) 1px solid !important; 392 | } 393 | 394 | /*#endregion*/ 395 | 396 | 397 | /*#region General */ 398 | .hidden { 399 | display:none; 400 | } 401 | .responsive_image { 402 | height:auto; 403 | max-width:100%; 404 | } 405 | .centered { 406 | text-align:center; 407 | } 408 | .right { 409 | text-align:right; 410 | } 411 | .unselectable { 412 | -moz-user-select:none; 413 | -ms-user-select:none; 414 | -webkit-user-select:none; 415 | user-select:none; 416 | } 417 | .debug { 418 | border:1px dashed var(--reddish-color) !important; 419 | display:none; 420 | font-family:'Courier New', Courier, monospace; 421 | font-size:0.8em; 422 | } 423 | .grow { 424 | -webkit-transition:all .2s ease-in-out; 425 | max-width:100%; 426 | transition:all .2s ease-in-out; 427 | } 428 | .grow:hover { 429 | -webkit-transform:scale(1.2); 430 | overflow:hidden; 431 | transform:scale(1.2); 432 | } 433 | .marker { 434 | border:1px dashed var(--reddish-color) !important; 435 | } 436 | .noBorder { 437 | border:0 !important; 438 | } 439 | .noDash ul, .noDash { 440 | list-style-type:none; 441 | } 442 | .noDash li, .noDash { 443 | word-break:break-all; 444 | } 445 | .noMargin { 446 | margin:0; 447 | } 448 | 449 | /*#endregion*/ 450 | 451 | 452 | /*#region Colors */ 453 | .red { 454 | background-color:var(--reddish-color); 455 | } 456 | .coloredBackground { 457 | background-color:var(--main-bg-color); 458 | } 459 | .coloredBackground_dark { 460 | background-color:var(--main-dark-bg-color); 461 | } 462 | .coloredBackground_light { 463 | background-color:var(--main-light-bg-color); 464 | } 465 | .coloredBackground_gray { 466 | background-color:var(--gray-light-color); 467 | } 468 | .coloredForeground { 469 | color:var(--main-bg-color); 470 | } 471 | 472 | .invalid { 473 | background-color:var(--invalid-color); 474 | } 475 | .confirmed { 476 | background-color:var(--confirmed-color); 477 | } 478 | .dirty { 479 | border:var(--invalid-color) 1px solid; 480 | } 481 | 482 | /*#endregion*/ 483 | 484 | 485 | /*#region Highlight controllers area */ 486 | .homeController, .summaryController, .menuController, .detailController, .errorController { 487 | border:1px solid black; 488 | } 489 | .debug.homeController { 490 | background-color:#EAFFEA; 491 | } 492 | .debug.summaryController { 493 | background-color:#5691C2; 494 | } 495 | .debug.menuController { 496 | background-color:#E6F5DC; 497 | } 498 | .debug.detailController { 499 | background-color:#DCEDF5; 500 | } 501 | .debug.errorController { 502 | background-color:#F5DCDC; 503 | } 504 | 505 | /*#endregion*/ 506 | 507 | 508 | /*#region Mobile platforms */ 509 | 510 | /*#region Extra Small Devices, Phones, but in Landscape mode */ 511 | @media screen and (min-width:640px) { 512 | * { 513 | padding:0; 514 | } 515 | aside, .primary, .secondary { 516 | display:block; 517 | font-size:0.6em; 518 | /* max-width:33%; */ 519 | min-width:25%; 520 | } 521 | #content, #page-content, .main, .content, .fixedTopContent { 522 | min-width:50%; 523 | } 524 | header, .header { 525 | font-size:1.2em; 526 | } 527 | hr { 528 | margin:1em 0 1em 0; 529 | padding:0; 530 | } 531 | } 532 | 533 | /*#endregion*/ 534 | 535 | /*#region Small Devices, Tablets */ 536 | @media screen and (min-width:768px) { 537 | * { 538 | padding:0.1em; 539 | } 540 | .homeController, .summaryController, .menuController, .detailController, .errorController { 541 | border:0; 542 | } 543 | .debug { 544 | display:block; 545 | } 546 | .out { 547 | display:none; 548 | } 549 | .in { 550 | display:inline-block; 551 | } 552 | } 553 | 554 | /*#endregion*/ 555 | 556 | /*#region Medium Devices, Desktops */ 557 | @media only screen and (min-width :992px) { 558 | * { 559 | padding:0.2em; 560 | } 561 | } 562 | 563 | /*#endregion*/ 564 | 565 | /*#endregion*/ 566 | 567 | 568 | /*#region Print styles */ 569 | @media print { 570 | body, header, footer { 571 | /* optimized for monochromatic print */ 572 | /* for color print, leave original background and color color, and comment header and footer here */ 573 | background:transparent none; 574 | color:black; 575 | /* general settings useful for print */ 576 | font-family:Georgia, "Times New Roman", Times, serif; 577 | font-size:12pt; 578 | font-weight:normal; 579 | margin:0; 580 | padding:0; 581 | width:100%; 582 | } 583 | nav, aside, 584 | #menu, .debug { 585 | display:none; 586 | } 587 | #wrapper, #content, #page-content, .content, .fixedTopContent { 588 | background:transparent none; 589 | border:0; 590 | color:black; 591 | float:none !important; 592 | margin:0 5%; 593 | padding:0; 594 | width:auto; 595 | } 596 | #content > .fixedTopContent , .fixedTopContent { 597 | margin-top:var(--fixedTop-height, 2em) 598 | } 599 | a { 600 | background:transparent; 601 | font-weight:bold; 602 | text-decoration:underline; 603 | } 604 | article { 605 | page-break-before:always; 606 | } 607 | h2, h3 { 608 | page-break-after:avoid; 609 | } 610 | img { 611 | max-width:100% !important; 612 | page-break-inside:avoid; 613 | } 614 | @page { 615 | margin:1cm; 616 | } 617 | } 618 | 619 | /*#endregion*/ 620 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [4.0.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/4.0.0) (unreleased) 4 | Summary Changelog: 5 | - General: update requirements to latest Fastify 4.x release ('^4.0.0' or later) 6 | and related plugins and Node.js 14 LTS (14.15.0 or later) 7 | - Feature: update and re-enable features dependent from my plugins, 8 | as soon as they are compatible with Fastify 4.x 9 | 10 | ## [3.0.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/3.0.0) (2022-04-21) 11 | Summary Changelog: 12 | - General: update requirements to latest Fastify 3.x release ('^3.3.0' or later) 13 | and related plugins and Node.js 12 LTS (min. Node.js 10 LTS for Fastify 3.x) 14 | - General: make EJS pages/emplates work with latest EJS ('^3.1.6' or later); 15 | note that a global layout has been set, so pages have been updated accordingly 16 | - General: update all dependencies; 17 | note that 'standard' has been update to latest 17.x (so ESLint 8.x) 18 | and 'ejs' has been updated to latest '3.1.7' to fix a vunlerability in a dependency 19 | - General: automated/manual builds at Docker Hub now are no more available, 20 | so removed references to it; sorry to have there only outdated images 21 | (up to 2021-04 approx., later I'll remove even them); 22 | I'll setup automated builds at GitHub and publish new images only there 23 | - General: removed outdated badges on dependencies 24 | because that site seems no longer available 25 | - General: removed outdated project/images at DockerHub 26 | - Feature: update and re-enable features dependent from my plugins, 27 | as soon as they are compatible with Fastify 3.x 28 | 29 | ## [2.8.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.8.0) (2022-02-11) 30 | Summary Changelog: 31 | - General: update all dependencies 32 | - General: update requirements to Node.js 12 LTS (12.13.0) 33 | because Node.js 10 now is in End-of-Life (EoL); 34 | then use ES2019/ES10 and some stuff even from ES2020/ES11 35 | (but not yet as syntax) 36 | - General: start to use some modern JavaScript features like async/await 37 | - Feature: split server (the general part) from the web application part 38 | (specific of this app), to better reuse it inside tests, etc 39 | 40 | ## [2.7.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.7.0) (2021-04-19) 41 | Summary Changelog: 42 | - General: update all dependencies 43 | - General: update my plugins to latest version 44 | - Feature: add sample route to show some Application info at '/info/app' 45 | - Feature: add sample route to show some Source Control Management system info 46 | (if any) at '/info/scm' 47 | - Feature: add sample route to show some Operating System info at '/info/os' 48 | - Feature: add sample route to group All info exposed by 3 previous routes at '/info/all', 49 | but in a little different way 50 | - Use the new release of 'fastify-cloudevents' aligned with v1.0.1 of the spec 51 | - Small other improvements 52 | 53 | ## [2.6.1](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.6.1) (2020-12-26) 54 | Summary Changelog: 55 | - General: update all dependencies 56 | - General: update requirements to latest Fastify 2.x release ('^2.15.3' or later) 57 | - Feature: add sample Dockerfile to use distroless images, 58 | but had to remove HEALTHCHECK directive inside the Dockerfile 59 | (container orchestrators do health checks without a specific script/executable inside images) 60 | - Feature: add some useful npm custom command in 'package.json' 61 | 62 | ## [2.6.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.6.0) (2020-10-02) 63 | Summary Changelog: 64 | - General: update all dependencies 65 | - General: update requirements to latest Fastify release ('^2.15.0' or later) 66 | - Feature: some css visual improvements 67 | - Use the new release of 'fastify-cloudevents' aligned with v1.0 of the spec, 68 | but no need to update code to use this new version 69 | - Small other improvements 70 | 71 | ## [2.5.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.5.0) (2020-05-29) 72 | Summary Changelog: 73 | - General: update all dependencies 74 | - General: update requirements to latest Fastify release ('^2.14.1' or later) 75 | - General: add plugins 'fastify-formbody', 'fastify-jwt' to manage in a simple way 76 | form url-encoded data and JWT tokens, for future use 77 | - General: update my plugin 'fastify-healthcheck' to latest version, 78 | and configure it to show even process uptime (as a sample, not really critical here) 79 | - Feature: update Dockerfile of both images 80 | to use latest Node.js LTS version (which in general is better), 81 | so at the moment is 12.x 82 | - Feature: log a warning if current Node.js release is lower than the expected one 83 | (for example that written in 'package.json'), with related feature flag to disable that check 84 | - Feature: many visual improvements in css styles (for a more modern layout, look and colors); 85 | and update template pages/fragments and even static page (as a sample) 86 | - Feature: add link to project source code at GitHub 87 | - Feature: add link (disabled for now) to a Login page (not present at the moment) 88 | - Fix: keep EJS templates to use latest 2.x release (so '^2.7.4' at the moment), 89 | because 3.x does not seem compatible 90 | - Improvement: update/cleanup some old code and some old styles 91 | - Security: let npm and snyk update dependencies, depending on auditing problems; 92 | this is visible for example in security auditing on generated Docker image 93 | (based on normal Node.js image) 94 | - Doc: added a lot of comments, suggestions, etc in TODO: 95 | some implemented, some not, some for the future, some never 96 | - etc 97 | 98 | 99 | ## [2.4.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.4.0) (2019-11-08) 100 | Summary Changelog: 101 | - Update all dependencies 102 | - Update requirements to latest Fastify release ('^2.10.0' or later) 103 | - Features: update to latest version of my plugin 'fastify-cloudevents' 104 | aligned with v0.3 of the spec 105 | - Features: update Dockerfile of both images 106 | so that installed packages will be updated, to reduce risk of vulnerabilities 107 | - Fix: update EJS templates to use its newer runtime inclusion, 108 | available since '^2.7.1' 109 | - Use Node.js assertions but in strict mode now 110 | - etc 111 | 112 | ## [2.3.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.3.0) (2019-06-17) 113 | Summary Changelog: 114 | - Update all dependencies 115 | - Update requirements to latest Fastify release ('^2.4.1' or later) 116 | - Features: add my plugin 'fastify-check-runtime-env' 117 | for checking some environmental properties at runtime 118 | (like Node.js version, and if not satisfied throw an exception), 119 | and add a feature flag to disable it 120 | - Features: add documentation for how to use published Docker images, 121 | and a reference to publish and tag them 122 | - Features: cleanup unnecessary badges in the README, 123 | and add a badge for for Docker images download 124 | - Features: remove dependency from 'semver', not really used directly 125 | - Publish (automatically) Docker images at DockerHub, for: 126 | 'latest', 'latest-alpine', and source code tags 127 | (like '2.3.0' and '2.3.0-alpine') 128 | - etc 129 | 130 | ## [2.2.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.2.0) (2019-05-27) 131 | Summary Changelog: 132 | - Update all dependencies 133 | - Content (Home Page): split content and routes in: 134 | samples, and routes provided by plugins 135 | - Content (page structure): update header and footer with better/more colors, 136 | and better info; improve navigation bar 137 | - Features: add a feature flag, so that at server startup some environment info 138 | (like Node.js version, OS name/type, OS release, OS platform, etc) 139 | will be written into log 140 | - Features: add a feature flag to disable CloudEvents log to console 141 | - Features: add a feature flag to disable CloudEvents log to file, 142 | a structured log file ('fastify-example.json.log') in the 'logs' folder, 143 | with 1 CloudEvent serialized per line 144 | - Remove some Fastify plugins not really used at the moment, 145 | but keep 'fastify-static' 146 | - Remove dependency on ESDoc and related plugins, not really used here 147 | - Update requirements to Node.js 10.x LTS ('>=10.13.0'), 148 | even if not strictly needed; but note that if run with a previous version 149 | (like Node.js 8.16.0) no error is thrown (it will be enforced later 150 | with a dedicated plugin) 151 | - Update Tap (Node-Tap) to latest (14.x); 152 | force the flag '--no-esm', and re-add the flag '--strict' 153 | - Update requirements to Fastify ('^2.3.0'), even if not strictly needed 154 | - Publish (manually) Docker images at DockerHub, for: 155 | 'latest', 'latest-alpine', and source code tags 156 | (like '2.2.0' and '2.2.0-alpine') 157 | 158 | ## [2.1.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.1.0) (2019-05-05) 159 | Summary Changelog: 160 | - Update all dependencies 161 | - Update Node.js to latest 10.x, but for now only in Docker images 162 | - Update docs 163 | - Update Home Page to add links to published routes and some description 164 | - Update/cleanup to latest standards css styles, 165 | to target only 1 last version of most common browsers 166 | - Use the new release of 'fastify-cloudevents' aligned with v0.2 of the spec, 167 | and update code to align with its breaking changes 168 | - Small other improvements 169 | 170 | ## [2.0.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/2.0.0) (2019-04-10) 171 | [Full Changelog](https://github.com/smartiniOnGitHub/fastify-example/compare/1.0.0...2.0.0) 172 | Summary Changelog: 173 | - Update requirements to Fastify v2, and related plugins 174 | - Update all dependencies 175 | - Same features as '1.0.0' 176 | 177 | ## [1.0.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/1.0.0) (2019-04-05) 178 | Summary Changelog: 179 | - Same as '0.16.0' 180 | 181 | ## [0.16.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.16.0) (2019-04-05) 182 | Summary Changelog: 183 | - Updated all dependencies to latest release 184 | - Update all plugins to latest release for Fastify 1.x 185 | 186 | ## [0.15.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.15.0) (2019-03-24) 187 | - Updated all dependencies to latest release 188 | - Update all plugins to latest release for Fastify 1.x 189 | - Ensure that all works as before with 'fastify-cloudevents' new release '0.4.0' 190 | - Improve normal Dockerfile with some best practices for production environments 191 | - Add a Dockerfile using Alpine Linux (smaller) and ensure all works even there 192 | 193 | ## [0.14.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.14.0) (2019-03-05) 194 | - Updated all dependencies to latest release 195 | - Update all plugins to latest release for Fastify 1.x 196 | - Ensure that all works as before with 'fastify-cloudevents' new release '0.3.0' 197 | - Fix the warning for exposing template routes async, by exposing as normal (non async) functions 198 | 199 | ## [0.13.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.13.0) (2019-01-12) 200 | - Updated all dependencies to latest release 201 | - Simplify and generalize paths, and improve templates 202 | - Add feature flags to disable some functionalities (like those exposed but some plugins); 203 | used a disabling logic, so by default related features are enabled 204 | - Move webapp features in its own source, and load it by passing as arguments 205 | the fastify instance, and maybe an options object 206 | - Keep dependency on 'dotenv' instead of using its wrapper 'fastify-env' which works 207 | in a way different of my setup here: 208 | all variables to read must be declared in the schema (which is good), 209 | env variables are populated in async, etc; so I prefer to stay with normal 'dotenv' usage 210 | - Send CloudEvent instances (serialized) into the NATS queue 211 | 212 | ## [0.12.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.12.0) (2019-01-04) 213 | - Updated all plugins to latest release 214 | - Handle webhook secret key via environmental variable (if given) 215 | - As a sample, call the webhook with something like: `curl http://127.0.0.1:8000/custom-webhook -X POST -H 'Content-Type: application/json' -d '{"payload":"test", "secretKey":"my example Secret Key"}'` 216 | end try different combinations, even when secret key is not set or the given one (in this call) is wrong 217 | - Add some custom npm command to perform a license check; 218 | done, but note that some dependencies currently uses non compatible licenses, 219 | and this need to be fixed soon 220 | - Add custom command to generate documentation of sources via ESDoc and related plugins 221 | 222 | ## [0.11.1](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.11.1) (2018-12-25) 223 | - Updated all plugins to latest release 224 | - Ensure that generated CloudEvent instances now contain client IP address in a custom attribute 225 | (inside the usual data attribute), could be really useful; this is a feature is latest release 226 | of my plugin 'fastify-cloudevents' 227 | 228 | ## [0.11.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.11.0) (2018-12-23) 229 | - Updated to latest Fastify 1.13.3 (stay on 1.x for now) and all plugins to latest release 230 | - Add the ability to load some configuration variables from environment variables, 231 | and from an '.env' file if present; for example handle HTTP_PORT, HTTP_ADDRESS, etc 232 | - Merge features from the branch 'add-fastify-nats' and add some improvement, like 233 | disable the integration with a NATS Server, and publish more content to the queues 234 | - Improve the publish/subscribe of messages with the NATS queue 235 | - Simplify main server source by moving some code blocks in its own sources 236 | 237 | ## [0.10.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.10.0) (2018-12-18) 238 | Update Fastify dependencies to latest (1.13.2) and all plugins 239 | - Small updates to fix some breaking changes of latest 'fastify-cloudevents' release 240 | - Other small changes/improvements 241 | 242 | ## [0.9.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.9.0) (2018-12-09) 243 | - Updated to latest Fastify 1.13.1 (stay on 1.x for now) and all plugins to latest release 244 | - in the branch 'add-fastify-nats' (still not merged with the 'master' branch) 245 | add a sample usage of 'fastify-nats' plugin, or better my fork of that plugin, 'fastify-nats-client', 246 | a little more updated and with some more features; later maybe switch back to the original one. 247 | Note that this branch is not merged into master mainly because the NATS server used is a public one, 248 | so could not be reachable, for example by corporate firewall rules, etc; 249 | anyway it'a available in that branch 250 | 251 | ## [0.8.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.8.0) (2018-11-28) 252 | - Updated to latest Fastify 1.13.0 (stay on 1.x for now) and all plugins to latest release 253 | - Minor tweaks 254 | 255 | ## [0.7.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.7.0) (2018-11-06) 256 | Summary Changelog: 257 | - Update dependencies 258 | - Add my plugin 'fastify-cloudevents' and use it with a sample minimal configuration 259 | 260 | ## [0.6.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.6.0) (2018-11-01) 261 | Summary Changelog: 262 | - Update Fastify to latest (1.13.0) and all dependencies 263 | - Add my plugin 'fastify-healthcheck' and use it even in the Dockerfile 264 | 265 | ## [0.5.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.5.0) (2018-10-22) 266 | Summary Changelog: 267 | - Update to latest Fastify (currently 1.12.1) and update all related plugins (and other dependencies), to ensure all works 268 | - Update point-of-view to new major version 2.0.0 269 | 270 | ## [0.4.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.4.0) (2018-05-19) 271 | Summary Changelog: 272 | - Update to latest Fastify (currently 1.4.0) and update all related plugins (and other dependencies), to ensure all works 273 | - Simplified 'package.json' 274 | - Update 'fastify-favicon' usage to show a sample with custom options, and update to latest release 275 | 276 | ## [0.3.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.3.0) (2018-04-23) 277 | Summary Changelog: 278 | - Update to latest Fastify 1.x.x (currently 1.3.0) and update all related plugins, to ensure all works 279 | - Update sample usage of 'fastify-webhook' with some custom options: now try to call it for example with (from Windows): `curl http://127.0.0.1:8000/custom-webhook -X POST -H "Content-Type: application/json" -d "{\"payload\":\"test\"}"`, otherwise remove the escape of double quote chars and keep the rest, so use: `curl http://127.0.0.1:8000/custom-webhook -X POST -H "Content-Type: application/json" -d '{"payload":"test"}'` 280 | 281 | ## [0.2.5](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.2.5) (2018-04-18) 282 | Summary Changelog: 283 | - Update dependencies to fix the problem in 'fastify-static' on the reload of pages with cache enabled (default behaviour in browsers) 284 | 285 | ## [0.2.4](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.2.4) (2018-04-15) 286 | Summary Changelog: 287 | - Small updates like in latest Fastify: remove dependency from 'request' and use 'simple-get' instead, add 'is-docker', etc 288 | 289 | ## [0.2.3](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.2.3) (2018-04-14) 290 | Summary Changelog: 291 | - Update to latest Fastify 1.x.x (currently 1.2.1) and update all related plugins, to ensure all works 292 | - Update dependencies (and empty all 'peerDependencies') 293 | - Update README with requirements 294 | 295 | ## [0.2.2](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.2.2) (2018-03-29) 296 | Summary Changelog: 297 | - Update to latest Fastify 1.x.x (currently 1.2.0) and update all related plugins, to ensure all works 298 | - Update dependencies (need even to add some 'peerDependencies' as needed by latest release of 'standard' and maybe even other package) 299 | - Update README with requirements 300 | 301 | ## [0.2.1](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.2.1) (2018-03-14) 302 | Summary Changelog: 303 | - Update to latest Fastify 1.x.x (currently 1.1.1) and update all related plugins, to ensure all works 304 | - Update README with requirements 305 | 306 | ## [0.2.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.2.0) (2018-03-07) 307 | Summary Changelog: 308 | - Update to Fastify 1.0.0 (just released) and update all related plugins, to ensure all works 309 | - Update README to add reference to Dockerfile-usage 310 | 311 | ## [0.1.2](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.1.0) (2018-02-22) 312 | Summary Changelog: 313 | - Update Fastify favicon plugin to latest release 314 | 315 | ## [0.1.0](https://github.com/smartiniOnGitHub/fastify-example/releases/tag/0.1.0) (2018-02-21) 316 | Summary Changelog: 317 | - First release 318 | - Add Fastify favicon plugin, to test it 319 | 320 | ---- 321 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict' 17 | 18 | /* eslint no-console: "off" */ 19 | /* eslint no-undef: "off" */ 20 | /* eslint no-unused-vars: "off" */ 21 | /* eslint callback-return: "off" */ 22 | /* eslint no-process-env: "off" */ 23 | /* eslint no-eval: "off" */ 24 | 25 | // define a general object, and assign functions to it ... 26 | // const utils = {} 27 | 28 | // some generic utility functions 29 | module.exports.clearConsole = function () { 30 | if (console.clear) { console.clear() } 31 | } 32 | module.exports.normalizeData = function (data, asArray) { 33 | if (data != null) { return data } else { 34 | if (data instanceof Array || asArray) { return [] } else { return {} } 35 | } 36 | } 37 | module.exports.getOrElse = function (obj, def) { 38 | if (typeof obj !== 'undefined' && obj !== null) { return Object.create(obj) } else { return def } 39 | } 40 | module.exports.getType = function (obj) { 41 | return typeof obj 42 | } 43 | module.exports.getTypeFromConstructor = function (obj) { 44 | if (typeof obj !== 'undefined' && obj !== null) { return obj.constructor.name } else { return null } 45 | } 46 | module.exports.getTypeFromPrototype = function (obj) { 47 | return Object.prototype.toString.call(obj).slice(8, -1) 48 | } 49 | module.exports.has = function (obj, key) { 50 | return key in obj 51 | } 52 | module.exports.hasLocalOrInPrototype = function (obj, key, searchInPrototype) { 53 | if (!searchInPrototype) { return Object.prototype.hasOwnProperty.call(obj, key) } else { 54 | return key in obj 55 | } 56 | } 57 | module.exports.isDefined = function (o) { 58 | return (typeof o !== 'undefined') 59 | } 60 | module.exports.isUndefined = function (o) { 61 | return (typeof o === 'undefined') 62 | } 63 | module.exports.isNull = function (o) { 64 | return (o === null) 65 | } 66 | module.exports.isNotNull = function (o) { 67 | return (o !== null) 68 | } 69 | module.exports.isDefinedAndNotNull = function (o) { 70 | return (o !== undefined && o !== null) 71 | } 72 | module.exports.isUndefinedOrNull = function (o) { 73 | return (o === undefined || o === null) 74 | } 75 | module.exports.isUndefinedOrNullArrayItem = function (a) { 76 | if (this.isUndefinedOrNull(a) && !this.isArray(a)) { 77 | return true 78 | } else { 79 | for (const item of a) { 80 | if (item === null) { return true } 81 | } 82 | return false 83 | } 84 | } 85 | module.exports.isFunction = function (f) { 86 | return (typeof f === 'function') 87 | } 88 | module.exports.isArray = function (o) { 89 | return (Array.isArray(o)) 90 | } 91 | module.exports.isBoolean = function (o) { 92 | return (typeof o === 'boolean') 93 | } 94 | module.exports.isNumber = function (o) { 95 | return (typeof o === 'number') 96 | } 97 | module.exports.isDate = function (o) { 98 | return (typeof o === 'object' || o instanceof Date) 99 | } 100 | 101 | module.exports.isValidDate = function (d) { 102 | return (this.isDate(d) && !isNaN(d)) 103 | } 104 | module.exports.isString = function (o) { 105 | return (typeof (o) === 'string') 106 | } 107 | module.exports.isNullOrEmpty = function (o) { 108 | return (o == null || (this.isString(o) && o.length === 0) || (this.isArray(o) && o.length === 0)) 109 | } 110 | module.exports.isRegExp = function (o) { 111 | return (typeof o === 'object' && o instanceof RegExp) 112 | } 113 | module.exports.isMap = function (o) { // ES6 Maps 114 | return (o instanceof Map || o instanceof WeakMap) 115 | } 116 | module.exports.isSet = function (o) { // ES6 Sets 117 | return (o instanceof Set || o instanceof WeakSet) 118 | } 119 | module.exports.isSymbol = function (o) { // ES6 Symbols 120 | return (typeof o === 'symbol') 121 | } 122 | module.exports.isObject = function (o) { 123 | return (typeof o === 'object') 124 | } 125 | module.exports.isError = function (o) { 126 | return (o instanceof Error && typeof o.message !== 'undefined') 127 | } 128 | module.exports.objectOwnPropertiesNames = function (obj) { 129 | // return all own properties names of the given object, as a list (array) 130 | return Object.keys(obj) 131 | } 132 | module.exports.objectOwnPropertiesList = function (obj) { 133 | // return all own properties of the given object, as a list (array) 134 | const values = [] 135 | for (const prop in obj) { 136 | if (Object.prototype.hasOwnProperty.call(obj, prop)) { 137 | values.push(prop) 138 | } 139 | } 140 | return values 141 | } 142 | module.exports.isArrayEmpty = function (obj) { 143 | if (this.isUndefinedOrNull(obj)) { return true } 144 | return obj.length === 0 145 | } 146 | module.exports.isStringEmpty = function (obj) { 147 | if (this.isUndefinedOrNull(obj)) { return true } 148 | return obj.length === 0 149 | } 150 | module.exports.isStringTrimmedEmpty = function (obj) { 151 | if (this.isUndefinedOrNull(obj)) { return true } 152 | return obj.trim().length === 0 153 | } 154 | module.exports.isEmpty = function (obj) { 155 | if (this.isUndefinedOrNull(obj)) { return true } 156 | if (this.isArray(obj) || this.isString(obj)) { return obj.length === 0 } 157 | if (this.isMap(obj) || this.isSet(obj)) { return obj.size === 0 } 158 | if (this.isObject(obj)) { return this.objectOwnPropertiesNames(obj).length === 0 } 159 | return false 160 | } 161 | module.exports.isStringFalse = function (obj) { 162 | return (this.isString(obj) && ['false', 'f', 'no', 'n', '0'].indexOf(obj.toLowerCase()) > -1) 163 | } 164 | module.exports.isStringTrue = function (obj) { 165 | return (this.isString(obj) && ['true', 't', 'yes', 'y', '1'].indexOf(obj.toLowerCase()) > -1) 166 | } 167 | module.exports.inherit = function (proto) { 168 | function F () { } 169 | F.prototype = proto 170 | return new F() 171 | } 172 | module.exports.formatObjectToJson = function (o) { 173 | return JSON.stringify(o) 174 | } 175 | module.exports.formatObjectToString = function (o, onlyOwnProperties) { 176 | let dump = '' 177 | let oop = false 178 | if (typeof onlyOwnProperties === 'undefined') { oop = true } 179 | for (const prop in o) { 180 | if (oop === false || Object.prototype.hasOwnProperty.call(o, prop)) { 181 | dump = dump + prop + ': ' + o[prop] + ', ' 182 | } 183 | } 184 | return dump 185 | } 186 | module.exports.formatObjectToMap = function (o) { 187 | if (Object.entries && typeof Map !== 'undefined') { return new Map(Object.entries(o)) } else { return null } 188 | } 189 | module.exports.formatDateToTimestampNoCheck = function (d) { 190 | return d.toISOString() 191 | } 192 | module.exports.formatCurrentDateToTimestamp = function () { 193 | return this.formatDateToTimestampNoCheck(new Date()) 194 | } 195 | module.exports.parseDateFromISOStringNoCheck = function (d) { 196 | return Date.parse(d) 197 | } 198 | module.exports.parseStringToBoolean = function (str, def) { 199 | if (this.isUndefinedOrNull(str) && this.isDefinedAndNotNull(def)) { 200 | return def 201 | } 202 | 203 | switch (str.toLowerCase().trim()) { 204 | case 'false': 205 | case 'f': 206 | case 'no': 207 | case 'n': 208 | case '0': 209 | case null: 210 | return false 211 | case 'true': 212 | case 't': 213 | case 'yes': 214 | case 'y': 215 | case '1': 216 | return true 217 | default: 218 | return def 219 | } 220 | } 221 | module.exports.parseJSON = function (str, callback) { 222 | if (!this.isFunction(callback)) { 223 | throw new TypeError(`Illegal argument: callback must be a function, instead got a '${typeof callback}'`) 224 | } 225 | 226 | try { 227 | const parsedJSON = JSON.parse(str) 228 | callback(null, parsedJSON) 229 | } catch (err) { 230 | callback(err, null) 231 | } 232 | } 233 | module.exports.lowercase = function (o) { 234 | return this.isString(o) ? o.toLowerCase() : o.toString().toLowerCase() 235 | } 236 | module.exports.uppercase = function (o) { 237 | return this.isString(o) ? o.toUpperCase() : o.toString().toUpperCase() 238 | } 239 | module.exports.toInt = function (str) { 240 | return parseInt(str, 10) 241 | } 242 | module.exports.executeIfTrue = function (f, c) { 243 | let r = null 244 | if (this.isFunction(f) && c === true) { 245 | const fArgs = Array.prototype.slice.call(arguments, 2) 246 | r = f.apply(this, fArgs) // pass all arguments (if any) after those given to main function ... 247 | } 248 | 249 | return r 250 | } 251 | module.exports.evaluate = function (statement) { 252 | const evaluator = eval 253 | try { 254 | evaluator(statement) 255 | return true 256 | } catch (e) { 257 | return false 258 | } 259 | } 260 | module.exports.isErrorAvailable = function () { 261 | return (typeof Error !== 'undefined') 262 | } 263 | 264 | module.exports.throwError = function (msg) { // note that msg could be a string or an Error, or another Object ... 265 | if (this.isErrorAvailable()) { throw new Error(msg) } else { throw msg } // fallback 266 | } 267 | module.exports.errorNotImplemented = function () { 268 | this.throwError('Not Implemented (implementation missing)') 269 | } 270 | module.exports.errorNotCallable = function () { 271 | this.throwError('Not Callable (abstract, implement it in childs)') 272 | } 273 | module.exports.logDebugMessage = function (message) { 274 | if (!this.isEnvDevelopment) { return } 275 | // else ... 276 | const msg = this.name + ': ' + ((message != null) ? message : '') 277 | console.debug(msg) 278 | } 279 | module.exports.noop = function () { 280 | // do nothing ... 281 | } 282 | 283 | module.exports.userBrowser = function () { 284 | // browser specific 285 | if (window && window.navigator) { return window.navigator.userAgent } else { return null } 286 | } 287 | module.exports.userLanguage = function () { 288 | // browser specific 289 | if (window && window.navigator) { return window.navigator.language || window.navigator.userLanguage } else { return null } 290 | } 291 | module.exports.userLanguages = function () { 292 | // browser specific 293 | if (window && window.navigator) { return window.navigator.languages } else { return null } 294 | } 295 | module.exports.userLocale = function () { 296 | // browser specific 297 | if (window && window.navigator && window.navigator.languages) { return window.navigator.languages[0] } else { return null } 298 | } 299 | 300 | // const process = require('process') // provided by Node.js // implicitly available 301 | 302 | // returns the value of the given variable name from the Node.js environment 303 | module.exports.fromEnv = function (varName) { 304 | return process.env[varName] 305 | } 306 | // returns the current Node.js environment 307 | module.exports.currentEnv = function () { 308 | return process.env.NODE_ENV || 'development' 309 | } 310 | // tell if the current Node.js environment is development 311 | module.exports.isEnvDevelopment = function () { 312 | return (this.currentEnv() === 'development') 313 | } 314 | // tell if the current Node.js environment is production 315 | module.exports.isEnvProduction = function () { 316 | return (this.currentEnv() === 'production') 317 | } 318 | // tell if the current Node.js environment is not production 319 | module.exports.isEnvNotProduction = function () { 320 | return !this.isEnvProduction() 321 | } 322 | // returns the current Node.js version 323 | module.exports.runtimeVersion = function () { 324 | return process.version || 'unknown' 325 | } 326 | // returns the current Platform name for the process 327 | module.exports.platformName = function () { 328 | return process.platform || 'unknown' 329 | } 330 | 331 | // returns the current process uptime (in sec) 332 | module.exports.uptimeProcess = function () { 333 | return process.uptime() || 0.0 334 | } 335 | 336 | // returns the current process id (pid) 337 | module.exports.pid = function () { 338 | return process.pid || 0 339 | } 340 | 341 | // returns the current environment variables 342 | module.exports.envVars = function () { 343 | return process.env || {} 344 | } 345 | 346 | const os = require('os') // provided by Node.js 347 | 348 | // returns the current host uptime (in sec) 349 | module.exports.osUptime = function () { 350 | return os.uptime() || 0 351 | } 352 | 353 | // returns the Platform name of the OS 354 | module.exports.osPlatform = function () { 355 | return os.platform() || 'unknown' 356 | } 357 | 358 | // returns the Architecture name of the OS 359 | module.exports.osArch = function () { 360 | return os.arch() || 'unknown' 361 | } 362 | 363 | // returns detailed CPU info (about core/s) from the OS 364 | module.exports.osCPU = function () { 365 | return os.cpus() || [] 366 | } 367 | 368 | // returns the Version of the OS 369 | module.exports.osVersion = function () { 370 | return os.release() || 'unknown' 371 | } 372 | 373 | // returns the Host name, from the OS 374 | module.exports.osHost = function () { 375 | return os.hostname() || 'unknown' 376 | } 377 | 378 | // returns the total memory available, from the OS 379 | module.exports.osMemoryTotal = function () { 380 | return os.totalmem() || 0 381 | } 382 | 383 | // returns the free memory available, from the OS 384 | module.exports.osMemoryFree = function () { 385 | return os.freemem() || 0 386 | } 387 | 388 | // log to console 389 | module.exports.logToConsole = function (msg) { 390 | console.log(msg) 391 | } 392 | 393 | // log a fastify request, but only the given URL 394 | module.exports.logRoute = function (req) { 395 | req.log.info(`Got request for URL: '${req.req.url}' ...`) 396 | } 397 | 398 | // log a fastify request, full 399 | module.exports.logRequest = function (req) { 400 | const details = this.dumpObject(req.req) 401 | // const details = this.dumpObject(req.req, {method:'inspect'}) 402 | req.log.info(`Got request for URL: '${req.req.url}', details: '${details}' ...`) 403 | } 404 | 405 | // register in the app the given module 406 | module.exports.registerLoadedModule = function (app, loadedModule, opts, uri) { 407 | if (this.isUndefinedOrNullArrayItem([app, loadedModule])) { throw new Error('Missing mandatory argument (undefined or null)') } 408 | app.log.info(`Registering the app module from URI '${uri}' ...`) 409 | app.register(loadedModule, function (err) { 410 | if (err) { throw err } 411 | }) 412 | } 413 | 414 | // build an Error with the given arguments, and throw or return it, depending on the last flag 415 | module.exports.buildError = function (req, msg, code, description, throwError) { 416 | const error = new Error() 417 | error.message = msg 418 | error.statusCode = (code != null) ? code : 0 419 | error.description = (description != null) ? description : null 420 | req.log.error(`Build a new Error: code:${error.statusCode}, message:'${error.message}', description:'${error.description}', and throw it:${throwError}`) 421 | if (throwError === true) { throw error } else { return error } 422 | } 423 | 424 | const util = require('util') // provided by Node.js 425 | 426 | // dump the given object using 'JSON.stringify' or Node.js 'util.inspect' (requires its module 'util'), depending on the given options 427 | // opts is optional, but if given 428 | // opts.method could have one of the following values: 'stringify', 'inspect', 'fast-json-stringify', null (for the default behavior) 429 | module.exports.dumpObject = function (obj, opts) { 430 | if (this.isUndefinedOrNull(opts)) { return (obj != null) ? obj.toString() : obj } 431 | 432 | let val = null 433 | switch (opts.method) { 434 | case 'inspect': 435 | val = util.inspect(obj) 436 | break 437 | case 'fast-json-stringify': 438 | // TODO: implement later ... 439 | // val = stringify(obj) 440 | break 441 | case 'stringify': 442 | val = JSON.stringify(obj) // attention to circular references, some types not dump, etc ... 443 | break 444 | default: 445 | val = obj.toString() // default in js 446 | } 447 | return val 448 | } 449 | 450 | // later add a specific function to log dumpObject using app logger 451 | 452 | // resolve the given value (generic, by default 0), after the number of given seconds (by default 1), returning a Promise 453 | // use it with the async / await syntax (recommended) 454 | module.exports.valueDelayed = function (value = 0, sec = 1) { 455 | return new Promise((resolve) => { 456 | setTimeout(() => { resolve(value) }, sec * 1000) 457 | }) 458 | } 459 | 460 | // tell if a feature is enabled 461 | module.exports.featureIsEnabled = function (trueIsDisabled = false, booleanStringName = '', defaultBooleanValue = true) { 462 | return (trueIsDisabled === true) 463 | ? !this.parseStringToBoolean(booleanStringName, defaultBooleanValue) 464 | : this.parseStringToBoolean(booleanStringName, defaultBooleanValue) 465 | } 466 | 467 | // sort string properties of objects, 468 | // given 'key' (string property name), 'order' ('asc' or 'desc') 469 | module.exports.compareProperties = function (key, order = 'asc') { 470 | return function (a, b) { 471 | if (!Object.prototype.hasOwnProperty.call(a, key) || !Object.prototype.hasOwnProperty.call(b, key)) return 0 472 | const comparison = a[key].localeCompare(b[key]) 473 | return (order === 'asc') ? comparison : (comparison * -1) 474 | } 475 | } 476 | 477 | // const exec = util.promisify(require('child_process').exec) // provided by Node.js 478 | const execFile = util.promisify(require('child_process').execFile) // provided by Node.js 479 | 480 | module.exports.gitVersion = async function () { 481 | const { stdout, stderr } = await execFile('git', ['version']) 482 | // const { stdout, stderr } = await execFile('git', ['version']).catch(e => console.error('Error: ', e.message)) // log some error info here 483 | // otherwise, do the same in callers (a catch for each promise, or wrap in a try/catch block) ... 484 | return stdout.replace('\n', '') 485 | } 486 | 487 | module.exports.gitBranch = async function () { 488 | const { stdout, stderr } = await execFile('git', ['branch']) 489 | return stdout.substring(2).replace('\n', '') 490 | } 491 | 492 | module.exports.gitHashFull = async function () { 493 | const { stdout, stderr } = await execFile('git', ['rev-parse', 'HEAD']) 494 | return stdout.replace('\n', '') 495 | } 496 | 497 | module.exports.gitHashShort = async function () { 498 | const { stdout, stderr } = await execFile('git', ['rev-parse', '--short', 'HEAD']) 499 | return stdout.replace('\n', '') 500 | } 501 | 502 | // utility function to return a value from an Either object: 503 | // error if defined (or throw it if related flag is true), 504 | // or the value (or its default value) 505 | module.exports.getFromEither = function (either, { throwOnError = false, value = {} } = {}) { 506 | if (either === undefined || either === null) { throw new Error('Missing mandatory argument (undefined or null)') } 507 | if (either.err !== undefined && either.err !== null) { 508 | if (throwOnError === true) { throw either.err } 509 | // else 510 | return either.err 511 | } else { 512 | return either.data || value 513 | } 514 | } 515 | 516 | // export main object 517 | // module.exports.utils = utils 518 | --------------------------------------------------------------------------------