Initial commit.
This commit is contained in:
commit
863e113aa8
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
/dist
|
||||||
|
# Cache used by TypeScript's incremental build
|
||||||
|
*.tsbuildinfo
|
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
coverage/
|
3
.eslintrc.js
Normal file
3
.eslintrc.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: '@loopback/eslint-config',
|
||||||
|
};
|
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Transpiled JavaScript files from Typescript
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# Cache used by TypeScript's incremental build
|
||||||
|
*.tsbuildinfo
|
4
.mocharc.json
Normal file
4
.mocharc.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recursive": true,
|
||||||
|
"require": "source-map-support/register"
|
||||||
|
}
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
*.json
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
32
.vscode/settings.json
vendored
Normal file
32
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"editor.rulers": [80],
|
||||||
|
"editor.tabCompletion": "on",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.trimAutoWhitespace": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true,
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/CVS": true,
|
||||||
|
"dist": true,
|
||||||
|
},
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
|
||||||
|
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||||
|
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false,
|
||||||
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
"eslint.run": "onSave",
|
||||||
|
"eslint.nodePath": "./node_modules",
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"typescript"
|
||||||
|
]
|
||||||
|
}
|
29
.vscode/tasks.json
vendored
Normal file
29
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Watch and Compile Project",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm",
|
||||||
|
"args": ["--silent", "run", "build:watch"],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": "$tsc-watch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build, Test and Lint",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "npm",
|
||||||
|
"args": ["--silent", "run", "test:dev"],
|
||||||
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": ["$tsc", "$eslint-compact", "$eslint-stylish"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
.yo-rc.json
Normal file
5
.yo-rc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"@loopback/cli": {
|
||||||
|
"version": "2.3.1"
|
||||||
|
}
|
||||||
|
}
|
36
DEVELOPING.md
Normal file
36
DEVELOPING.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Developer's Guide
|
||||||
|
|
||||||
|
We use Visual Studio Code for developing LoopBack and recommend the same to our
|
||||||
|
users.
|
||||||
|
|
||||||
|
## VSCode setup
|
||||||
|
|
||||||
|
Install the following extensions:
|
||||||
|
|
||||||
|
- [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||||
|
- [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
|
|
||||||
|
## Development workflow
|
||||||
|
|
||||||
|
### Visual Studio Code
|
||||||
|
|
||||||
|
1. Start the build task (Cmd+Shift+B) to run TypeScript compiler in the
|
||||||
|
background, watching and recompiling files as you change them. Compilation
|
||||||
|
errors will be shown in the VSCode's "PROBLEMS" window.
|
||||||
|
|
||||||
|
2. Execute "Run Rest Task" from the Command Palette (Cmd+Shift+P) to re-run the
|
||||||
|
test suite and lint the code for both programming and style errors. Linting
|
||||||
|
errors will be shown in VSCode's "PROBLEMS" window. Failed tests are printed
|
||||||
|
to terminal output only.
|
||||||
|
|
||||||
|
### Other editors/IDEs
|
||||||
|
|
||||||
|
1. Open a new terminal window/tab and start the continuous build process via
|
||||||
|
`npm run build:watch`. It will run TypeScript compiler in watch mode,
|
||||||
|
recompiling files as you change them. Any compilation errors will be printed
|
||||||
|
to the terminal.
|
||||||
|
|
||||||
|
2. In your main terminal window/tab, run `npm run test:dev` to re-run the test
|
||||||
|
suite and lint the code for both programming and style errors. You should run
|
||||||
|
this command manually whenever you have new changes to test. Test failures
|
||||||
|
and linter errors will be printed to the terminal.
|
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Check out https://hub.docker.com/_/node to select a new base image
|
||||||
|
FROM node:10-slim
|
||||||
|
|
||||||
|
# Set to a non-root built-in user `node`
|
||||||
|
USER node
|
||||||
|
|
||||||
|
# Create app directory (with user `node`)
|
||||||
|
RUN mkdir -p /home/node/app
|
||||||
|
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
|
||||||
|
# Install app dependencies
|
||||||
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||||
|
# where available (npm@5+)
|
||||||
|
COPY --chown=node package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Bundle app source code
|
||||||
|
COPY --chown=node . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Bind to all network interfaces so that it can be mapped to the host OS
|
||||||
|
ENV HOST=0.0.0.0 PORT=3000
|
||||||
|
|
||||||
|
EXPOSE ${PORT}
|
||||||
|
CMD [ "node", "." ]
|
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# stock-recording
|
||||||
|
|
||||||
|
Abandonné, car documentation de faible qualité et authentification moins simple que dans loopback3 et flask-restx.
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
yohan@y1:~$ sudo apt-get -y install npm nodejs
|
||||||
|
yohan@y1:~$ mkdir ~/.npm-global
|
||||||
|
yohan@y1:~$ npm config set prefix '~/.npm-global'
|
||||||
|
yohan@y1:~$ echo "PATH=~/.npm-global/bin:$PATH" >> ~/.profile
|
||||||
|
yohan@y1:~$ npm install -g @loopback/cli
|
||||||
|
yohan@y1:~$ lb4 app
|
||||||
|
yohan@y1:~$ cd stock-recording
|
||||||
|
yohan@y1:~/stock-recording$ lb4 model
|
||||||
|
yohan@y1:~/stock-recording$ lb4 datasource
|
||||||
|
yohan@y1:~/stock-recording$ lb4 repository
|
||||||
|
yohan@y1:~/stock-recording$ lb4 controller
|
||||||
|
yohan@y1:~/stock-recording$ npm install --save @loopback/authentication
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
yohan@y1:~/stock-recording$ rsync -itrlpgovDHzP --delete-after ./ ovh1:/home/yohan/repository/stock-recording/
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
[yohan@ovh1 repository]$ cd stock-recording
|
||||||
|
[yohan@ovh1 stock-recording]$ sudo docker exec -it mysql-server bash
|
||||||
|
root@42ebf0d5ad35:/# mysql -u root -p
|
||||||
|
MariaDB [(none)]> CREATE USER 'stock-recording'@'%' IDENTIFIED BY 'FIXME';
|
||||||
|
MariaDB [(none)]> CREATE DATABASE IF NOT EXISTS stock_recording;
|
||||||
|
MariaDB [(none)]> GRANT ALL PRIVILEGES ON stock_recording.* TO 'stock-recording'@'%' IDENTIFIED BY 'FIXME';
|
||||||
|
MariaDB [(none)]> quit
|
||||||
|
root@42ebf0d5ad35:/# exit
|
||||||
|
|
||||||
|
## Run
|
||||||
|
[yohan@ovh1 stock-recording]$ sudo docker-compose up -d --build
|
||||||
|
[yohan@ovh1 stock-recording]$ sudo docker exec stock-recording npm run migrate
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
https://medium.com/@iqbaldjulfri/role-based-authentication-with-jwt-in-loopback-4-4f9ab63daa52
|
||||||
|
https://www.freecodecamp.org/news/build-restful-api-with-authentication-under-5-minutes-using-loopback-by-expressjs-no-programming-31231b8472ca/
|
||||||
|
[![LoopBack](https://github.com/strongloop/loopback-next/raw/master/docs/site/imgs/branding/Powered-by-LoopBack-Badge-(blue)-@2x.png)](http://loopback.io/)
|
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
version: "2.1"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
stock-recording:
|
||||||
|
container_name: stock-recording
|
||||||
|
image: stock-recording:latest
|
||||||
|
build: ./
|
||||||
|
networks:
|
||||||
|
- mysqlnet
|
||||||
|
ports:
|
||||||
|
- "3000:3000/tcp"
|
||||||
|
expose:
|
||||||
|
- "3000/tcp"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
|
||||||
|
mysqlnet:
|
||||||
|
external: true
|
27
index.js
Normal file
27
index.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const application = require('./dist');
|
||||||
|
|
||||||
|
module.exports = application;
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
// Run the application
|
||||||
|
const config = {
|
||||||
|
rest: {
|
||||||
|
port: +(process.env.PORT || 3000),
|
||||||
|
host: process.env.HOST,
|
||||||
|
// The `gracePeriodForClose` provides a graceful close for http/https
|
||||||
|
// servers with keep-alive clients. The default value is `Infinity`
|
||||||
|
// (don't force-close). If you want to immediately destroy all sockets
|
||||||
|
// upon stop, set its value to `0`.
|
||||||
|
// See https://www.npmjs.com/package/stoppable
|
||||||
|
gracePeriodForClose: 5000, // 5 seconds
|
||||||
|
openApiSpec: {
|
||||||
|
// useful when used with OpenAPI-to-GraphQL to locate your application
|
||||||
|
setServersFromRequest: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
application.main(config).catch(err => {
|
||||||
|
console.error('Cannot start the application.', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
5423
package-lock.json
generated
Normal file
5423
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
package.json
Normal file
74
package.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "stock-recording",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "API to record stock market data.",
|
||||||
|
"keywords": [
|
||||||
|
"loopback-application",
|
||||||
|
"loopback"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "lb-tsc",
|
||||||
|
"build:watch": "lb-tsc --watch",
|
||||||
|
"lint": "npm run prettier:check && npm run eslint",
|
||||||
|
"lint:fix": "npm run eslint:fix && npm run prettier:fix",
|
||||||
|
"prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"",
|
||||||
|
"prettier:check": "npm run prettier:cli -- -l",
|
||||||
|
"prettier:fix": "npm run prettier:cli -- --write",
|
||||||
|
"eslint": "lb-eslint --report-unused-disable-directives .",
|
||||||
|
"eslint:fix": "npm run eslint -- --fix",
|
||||||
|
"pretest": "npm run clean && npm run build",
|
||||||
|
"test": "lb-mocha --allow-console-logs \"dist/__tests__\"",
|
||||||
|
"posttest": "npm run lint",
|
||||||
|
"test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest",
|
||||||
|
"docker:build": "docker build -t stock-recording .",
|
||||||
|
"docker:run": "docker run -p 3000:3000 -d stock-recording",
|
||||||
|
"migrate": "node ./dist/migrate",
|
||||||
|
"prestart": "npm run build",
|
||||||
|
"start": "node -r source-map-support/register .",
|
||||||
|
"clean": "lb-clean dist *.tsbuildinfo .eslintcache"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "",
|
||||||
|
"files": [
|
||||||
|
"README.md",
|
||||||
|
"index.js",
|
||||||
|
"index.d.ts",
|
||||||
|
"dist",
|
||||||
|
"src",
|
||||||
|
"!*/__tests__"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@loopback/authentication": "^4.1.3",
|
||||||
|
"@loopback/boot": "^2.0.4",
|
||||||
|
"@loopback/context": "^3.4.0",
|
||||||
|
"@loopback/core": "^2.3.0",
|
||||||
|
"@loopback/openapi-v3": "^3.1.3",
|
||||||
|
"@loopback/repository": "^2.1.1",
|
||||||
|
"@loopback/rest": "^3.2.1",
|
||||||
|
"@loopback/rest-explorer": "^2.0.4",
|
||||||
|
"@loopback/service-proxy": "^2.0.4",
|
||||||
|
"loopback-connector-mysql": "^5.4.3",
|
||||||
|
"tslib": "^1.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@loopback/build": "^5.0.1",
|
||||||
|
"source-map-support": "^0.5.16",
|
||||||
|
"@loopback/testlab": "^3.0.1",
|
||||||
|
"@types/node": "^10.17.19",
|
||||||
|
"@typescript-eslint/parser": "^2.27.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
||||||
|
"@loopback/eslint-config": "^6.0.3",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-config-prettier": "^6.10.1",
|
||||||
|
"eslint-plugin-eslint-plugin": "^2.2.1",
|
||||||
|
"eslint-plugin-mocha": "^6.3.0",
|
||||||
|
"typescript": "~3.8.3"
|
||||||
|
}
|
||||||
|
}
|
88
public/index.html
Normal file
88
public/index.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>API to record stock market data.</title>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="https://loopback.io/favicon.ico">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h3 {
|
||||||
|
margin-left: 25px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited {
|
||||||
|
color: #3f5dff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 a {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, a:focus, a:active {
|
||||||
|
color: #001956;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 25px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%)
|
||||||
|
}
|
||||||
|
|
||||||
|
.info h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info p {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: rgb(29, 30, 32);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited {
|
||||||
|
color: #4990e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, a:focus, a:active {
|
||||||
|
color: #2b78ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="info">
|
||||||
|
<h1>stock-recording</h1>
|
||||||
|
<p>Version 1.0.0</p>
|
||||||
|
|
||||||
|
<h3>OpenAPI spec: <a href="/openapi.json">/openapi.json</a></h3>
|
||||||
|
<h3>API Explorer: <a href="/explorer">/explorer</a></h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="power">
|
||||||
|
<a href="https://loopback.io" target="_blank">
|
||||||
|
<img src="https://loopback.io/images/branding/powered-by-loopback/blue/powered-by-loopback-sm.png" />
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
3
src/__tests__/README.md
Normal file
3
src/__tests__/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Tests
|
||||||
|
|
||||||
|
Please place your tests in this folder.
|
31
src/__tests__/acceptance/home-page.acceptance.ts
Normal file
31
src/__tests__/acceptance/home-page.acceptance.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {Client} from '@loopback/testlab';
|
||||||
|
import {StockRecordingApplication} from '../..';
|
||||||
|
import {setupApplication} from './test-helper';
|
||||||
|
|
||||||
|
describe('HomePage', () => {
|
||||||
|
let app: StockRecordingApplication;
|
||||||
|
let client: Client;
|
||||||
|
|
||||||
|
before('setupApplication', async () => {
|
||||||
|
({app, client} = await setupApplication());
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await app.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes a default home page', async () => {
|
||||||
|
await client
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /text\/html/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes self-hosted explorer', async () => {
|
||||||
|
await client
|
||||||
|
.get('/explorer/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /text\/html/)
|
||||||
|
.expect(/<title>LoopBack API Explorer/);
|
||||||
|
});
|
||||||
|
});
|
21
src/__tests__/acceptance/ping.controller.acceptance.ts
Normal file
21
src/__tests__/acceptance/ping.controller.acceptance.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {Client, expect} from '@loopback/testlab';
|
||||||
|
import {StockRecordingApplication} from '../..';
|
||||||
|
import {setupApplication} from './test-helper';
|
||||||
|
|
||||||
|
describe('PingController', () => {
|
||||||
|
let app: StockRecordingApplication;
|
||||||
|
let client: Client;
|
||||||
|
|
||||||
|
before('setupApplication', async () => {
|
||||||
|
({app, client} = await setupApplication());
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await app.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes GET /ping', async () => {
|
||||||
|
const res = await client.get('/ping?msg=world').expect(200);
|
||||||
|
expect(res.body).to.containEql({greeting: 'Hello from LoopBack'});
|
||||||
|
});
|
||||||
|
});
|
32
src/__tests__/acceptance/test-helper.ts
Normal file
32
src/__tests__/acceptance/test-helper.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {StockRecordingApplication} from '../..';
|
||||||
|
import {
|
||||||
|
createRestAppClient,
|
||||||
|
givenHttpServerConfig,
|
||||||
|
Client,
|
||||||
|
} from '@loopback/testlab';
|
||||||
|
|
||||||
|
export async function setupApplication(): Promise<AppWithClient> {
|
||||||
|
const restConfig = givenHttpServerConfig({
|
||||||
|
// Customize the server configuration here.
|
||||||
|
// Empty values (undefined, '') will be ignored by the helper.
|
||||||
|
//
|
||||||
|
// host: process.env.HOST,
|
||||||
|
// port: +process.env.PORT,
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = new StockRecordingApplication({
|
||||||
|
rest: restConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.boot();
|
||||||
|
await app.start();
|
||||||
|
|
||||||
|
const client = createRestAppClient(app);
|
||||||
|
|
||||||
|
return {app, client};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppWithClient {
|
||||||
|
app: StockRecordingApplication;
|
||||||
|
client: Client;
|
||||||
|
}
|
87
src/application.ts
Normal file
87
src/application.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {BootMixin} from '@loopback/boot';
|
||||||
|
import {ApplicationConfig} from '@loopback/core';
|
||||||
|
import {
|
||||||
|
RestExplorerBindings,
|
||||||
|
RestExplorerComponent,
|
||||||
|
} from '@loopback/rest-explorer';
|
||||||
|
import {RepositoryMixin} from '@loopback/repository';
|
||||||
|
import {RestApplication} from '@loopback/rest';
|
||||||
|
import {ServiceMixin} from '@loopback/service-proxy';
|
||||||
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
AuthenticationComponent,
|
||||||
|
registerAuthenticationStrategy,
|
||||||
|
} from '@loopback/authentication';
|
||||||
|
import {MyAuthenticatingSequence} from './sequence';
|
||||||
|
import {BasicAuthenticationStrategy} from './basic-strategy';
|
||||||
|
import {UserRepository} from './user.repository';
|
||||||
|
|
||||||
|
export class StockRecordingApplication extends BootMixin(
|
||||||
|
ServiceMixin(RepositoryMixin(RestApplication)),
|
||||||
|
) {
|
||||||
|
constructor(options: ApplicationConfig = {}) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
// load the authentication component
|
||||||
|
this.component(AuthenticationComponent);
|
||||||
|
|
||||||
|
// register your custom authentication strategy
|
||||||
|
registerAuthenticationStrategy(this, BasicAuthenticationStrategy);
|
||||||
|
|
||||||
|
// use your custom authenticating sequence
|
||||||
|
this.sequence(MyAuthenticatingSequence);
|
||||||
|
|
||||||
|
// Set up default home page
|
||||||
|
this.static('/', path.join(__dirname, '../public'));
|
||||||
|
|
||||||
|
// Customize @loopback/rest-explorer configuration here
|
||||||
|
this.configure(RestExplorerBindings.COMPONENT).to({
|
||||||
|
path: '/explorer',
|
||||||
|
});
|
||||||
|
this.component(RestExplorerComponent);
|
||||||
|
|
||||||
|
this.projectRoot = __dirname;
|
||||||
|
// Customize @loopback/boot Booter Conventions here
|
||||||
|
this.bootOptions = {
|
||||||
|
controllers: {
|
||||||
|
// Customize ControllerBooter Conventions here
|
||||||
|
dirs: ['controllers'],
|
||||||
|
extensions: ['.controller.js'],
|
||||||
|
nested: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserRepository(): UserRepository {
|
||||||
|
return new UserRepository({
|
||||||
|
joe888: {
|
||||||
|
id: '1',
|
||||||
|
firstName: 'joe',
|
||||||
|
lastName: 'joeman',
|
||||||
|
username: 'joe888',
|
||||||
|
password: 'joepa55w0rd',
|
||||||
|
},
|
||||||
|
jill888: {
|
||||||
|
id: '2',
|
||||||
|
firstName: 'jill',
|
||||||
|
lastName: 'jillman',
|
||||||
|
username: 'jill888',
|
||||||
|
password: 'jillpa55w0rd',
|
||||||
|
},
|
||||||
|
jack888: {
|
||||||
|
id: '3',
|
||||||
|
firstName: 'jack',
|
||||||
|
lastName: 'jackman',
|
||||||
|
username: 'jack888',
|
||||||
|
password: 'jackpa55w0rd',
|
||||||
|
},
|
||||||
|
janice888: {
|
||||||
|
id: '4',
|
||||||
|
firstName: 'janice',
|
||||||
|
lastName: 'janiceman',
|
||||||
|
username: 'janice888',
|
||||||
|
password: 'janicepa55w0rd',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
66
src/basic-auth-user-service.ts
Normal file
66
src/basic-auth-user-service.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright IBM Corp. 2019. All Rights Reserved.
|
||||||
|
// Node module: @loopback/authentication
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
import {inject} from '@loopback/context';
|
||||||
|
import {HttpErrors} from '@loopback/rest';
|
||||||
|
import {UserProfile} from '@loopback/security';
|
||||||
|
import {AuthenticationBindings} from '@loopback/authentication';
|
||||||
|
import {UserService} from '@loopback/authentication';
|
||||||
|
import {UserProfileFactory} from '@loopback/authentication';
|
||||||
|
import {USER_REPO} from './keys';
|
||||||
|
import {BasicAuthenticationStrategyCredentials} from './basic-strategy';
|
||||||
|
import {User} from './user';
|
||||||
|
import {UserRepository} from './user.repository';
|
||||||
|
|
||||||
|
export class BasicAuthenticationUserService
|
||||||
|
implements UserService<User, BasicAuthenticationStrategyCredentials> {
|
||||||
|
constructor(
|
||||||
|
@inject(USER_REPO)
|
||||||
|
private userRepository: UserRepository,
|
||||||
|
@inject(AuthenticationBindings.USER_PROFILE_FACTORY)
|
||||||
|
public userProfileFactory: UserProfileFactory<User>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async verifyCredentials(
|
||||||
|
credentials: BasicAuthenticationStrategyCredentials,
|
||||||
|
): Promise<User> {
|
||||||
|
if (!credentials) {
|
||||||
|
throw new HttpErrors.Unauthorized(`'credentials' is null`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!credentials.username) {
|
||||||
|
throw new HttpErrors.Unauthorized(`'credentials.username' is null`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!credentials.password) {
|
||||||
|
throw new HttpErrors.Unauthorized(`'credentials.password' is null`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundUser = this.userRepository.find(credentials.username);
|
||||||
|
if (!foundUser) {
|
||||||
|
throw new HttpErrors['Unauthorized'](
|
||||||
|
`User with username ${credentials.username} not found.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentials.password !== foundUser.password) {
|
||||||
|
throw new HttpErrors.Unauthorized('The password is not correct.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToUserProfile(user: User): UserProfile {
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpErrors.Unauthorized(`'user' is null`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.id) {
|
||||||
|
throw new HttpErrors.Unauthorized(`'user id' is null`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userProfileFactory(user);
|
||||||
|
}
|
||||||
|
}
|
95
src/basic-strategy.ts
Normal file
95
src/basic-strategy.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright IBM Corp. 2019,2020. All Rights Reserved.
|
||||||
|
// Node module: @loopback/authentication
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
import {bind, inject} from '@loopback/context';
|
||||||
|
import {
|
||||||
|
asSpecEnhancer,
|
||||||
|
mergeSecuritySchemeToSpec,
|
||||||
|
OASEnhancer,
|
||||||
|
OpenApiSpec,
|
||||||
|
} from '@loopback/openapi-v3';
|
||||||
|
import {HttpErrors, Request} from '@loopback/rest';
|
||||||
|
import {UserProfile} from '@loopback/security';
|
||||||
|
import {asAuthStrategy, AuthenticationStrategy} from '@loopback/authentication';
|
||||||
|
import {BasicAuthenticationStrategyBindings} from './keys';
|
||||||
|
import {BasicAuthenticationUserService} from './basic-auth-user-service';
|
||||||
|
|
||||||
|
export interface BasicAuthenticationStrategyCredentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind(asAuthStrategy, asSpecEnhancer)
|
||||||
|
export class BasicAuthenticationStrategy
|
||||||
|
implements AuthenticationStrategy, OASEnhancer {
|
||||||
|
name = 'basic';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@inject(BasicAuthenticationStrategyBindings.USER_SERVICE)
|
||||||
|
private userService: BasicAuthenticationUserService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async authenticate(request: Request): Promise<UserProfile | undefined> {
|
||||||
|
const credentials: BasicAuthenticationStrategyCredentials = this.extractCredentials(
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
const user = await this.userService.verifyCredentials(credentials);
|
||||||
|
const userProfile = this.userService.convertToUserProfile(user);
|
||||||
|
|
||||||
|
return userProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractCredentials(request: Request): BasicAuthenticationStrategyCredentials {
|
||||||
|
if (!request.headers.authorization) {
|
||||||
|
throw new HttpErrors.Unauthorized(`Authorization header not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for example : Basic Z2l6bW9AZ21haWwuY29tOnBhc3N3b3Jk
|
||||||
|
const authHeaderValue = request.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeaderValue.startsWith('Basic')) {
|
||||||
|
throw new HttpErrors.Unauthorized(
|
||||||
|
`Authorization header is not of type 'Basic'.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//split the string into 2 parts. We are interested in the base64 portion
|
||||||
|
const parts = authHeaderValue.split(' ');
|
||||||
|
if (parts.length !== 2)
|
||||||
|
throw new HttpErrors.Unauthorized(
|
||||||
|
`Authorization header value has too many parts. It must follow the pattern: 'Basic xxyyzz' where xxyyzz is a base64 string.`,
|
||||||
|
);
|
||||||
|
const encryptedCredentails = parts[1];
|
||||||
|
|
||||||
|
// decrypt the credentials. Should look like : 'username:password'
|
||||||
|
const decryptedCredentails = Buffer.from(
|
||||||
|
encryptedCredentails,
|
||||||
|
'base64',
|
||||||
|
).toString('utf8');
|
||||||
|
|
||||||
|
//split the string into 2 parts
|
||||||
|
const decryptedParts = decryptedCredentails.split(':');
|
||||||
|
|
||||||
|
if (decryptedParts.length !== 2) {
|
||||||
|
throw new HttpErrors.Unauthorized(
|
||||||
|
`Authorization header 'Basic' value does not contain two parts separated by ':'.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const creds: BasicAuthenticationStrategyCredentials = {
|
||||||
|
username: decryptedParts[0],
|
||||||
|
password: decryptedParts[1],
|
||||||
|
};
|
||||||
|
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifySpec(spec: OpenApiSpec): OpenApiSpec {
|
||||||
|
return mergeSecuritySchemeToSpec(spec, this.name, {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'basic',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
9
src/controllers/README.md
Normal file
9
src/controllers/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Controllers
|
||||||
|
|
||||||
|
This directory contains source files for the controllers exported by this app.
|
||||||
|
|
||||||
|
To add a new empty controller, type in `lb4 controller [<name>]` from the
|
||||||
|
command-line of your application's root directory.
|
||||||
|
|
||||||
|
For more information, please visit
|
||||||
|
[Controller generator](http://loopback.io/doc/en/lb4/Controller-generator.html).
|
2
src/controllers/index.ts
Normal file
2
src/controllers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './ping.controller';
|
||||||
|
export * from './stock-recording.controller';
|
52
src/controllers/ping.controller.ts
Normal file
52
src/controllers/ping.controller.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import {Request, RestBindings, get, ResponseObject} from '@loopback/rest';
|
||||||
|
import {inject} from '@loopback/context';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAPI response for ping()
|
||||||
|
*/
|
||||||
|
const PING_RESPONSE: ResponseObject = {
|
||||||
|
description: 'Ping Response',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
title: 'PingResponse',
|
||||||
|
properties: {
|
||||||
|
greeting: {type: 'string'},
|
||||||
|
date: {type: 'string'},
|
||||||
|
url: {type: 'string'},
|
||||||
|
headers: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
'Content-Type': {type: 'string'},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple controller to bounce back http requests
|
||||||
|
*/
|
||||||
|
export class PingController {
|
||||||
|
constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {}
|
||||||
|
|
||||||
|
// Map to `GET /ping`
|
||||||
|
@get('/ping', {
|
||||||
|
responses: {
|
||||||
|
'200': PING_RESPONSE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ping(): object {
|
||||||
|
// Reply with a greeting, the current time, the url, and request headers
|
||||||
|
return {
|
||||||
|
greeting: 'Hello from LoopBack',
|
||||||
|
date: new Date(),
|
||||||
|
url: this.req.url,
|
||||||
|
headers: Object.assign({}, this.req.headers),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
176
src/controllers/stock-recording.controller.ts
Normal file
176
src/controllers/stock-recording.controller.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import {
|
||||||
|
Count,
|
||||||
|
CountSchema,
|
||||||
|
Filter,
|
||||||
|
FilterExcludingWhere,
|
||||||
|
repository,
|
||||||
|
Where,
|
||||||
|
} from '@loopback/repository';
|
||||||
|
import {
|
||||||
|
post,
|
||||||
|
param,
|
||||||
|
get,
|
||||||
|
getModelSchemaRef,
|
||||||
|
patch,
|
||||||
|
put,
|
||||||
|
del,
|
||||||
|
requestBody,
|
||||||
|
} from '@loopback/rest';
|
||||||
|
import {Stock} from '../models';
|
||||||
|
import {StockRepository} from '../repositories';
|
||||||
|
import {AuthenticationBindings, authenticate} from '@loopback/authentication';
|
||||||
|
|
||||||
|
export class StockRecordingController {
|
||||||
|
constructor(
|
||||||
|
@repository(StockRepository)
|
||||||
|
public stockRepository : StockRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@post('/stocks', {
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: 'Stock model instance',
|
||||||
|
content: {'application/json': {schema: getModelSchemaRef(Stock)}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@authenticate('basic')
|
||||||
|
async create(
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Stock, {
|
||||||
|
title: 'NewStock',
|
||||||
|
exclude: ['id'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
stock: Omit<Stock, 'id'>,
|
||||||
|
): Promise<Stock> {
|
||||||
|
return this.stockRepository.create(stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@get('/stocks/count', {
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: 'Stock model count',
|
||||||
|
content: {'application/json': {schema: CountSchema}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async count(
|
||||||
|
@param.where(Stock) where?: Where<Stock>,
|
||||||
|
): Promise<Count> {
|
||||||
|
return this.stockRepository.count(where);
|
||||||
|
}
|
||||||
|
|
||||||
|
@get('/stocks', {
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: 'Array of Stock model instances',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: getModelSchemaRef(Stock, {includeRelations: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@authenticate('basic')
|
||||||
|
async find(
|
||||||
|
@param.filter(Stock) filter?: Filter<Stock>,
|
||||||
|
): Promise<Stock[]> {
|
||||||
|
return this.stockRepository.find(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch('/stocks', {
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: 'Stock PATCH success count',
|
||||||
|
content: {'application/json': {schema: CountSchema}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async updateAll(
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Stock, {partial: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
stock: Stock,
|
||||||
|
@param.where(Stock) where?: Where<Stock>,
|
||||||
|
): Promise<Count> {
|
||||||
|
return this.stockRepository.updateAll(stock, where);
|
||||||
|
}
|
||||||
|
|
||||||
|
@get('/stocks/{id}', {
|
||||||
|
responses: {
|
||||||
|
'200': {
|
||||||
|
description: 'Stock model instance',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Stock, {includeRelations: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async findById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@param.filter(Stock, {exclude: 'where'}) filter?: FilterExcludingWhere<Stock>
|
||||||
|
): Promise<Stock> {
|
||||||
|
return this.stockRepository.findById(id, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch('/stocks/{id}', {
|
||||||
|
responses: {
|
||||||
|
'204': {
|
||||||
|
description: 'Stock PATCH success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async updateById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Stock, {partial: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
stock: Stock,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.stockRepository.updateById(id, stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@put('/stocks/{id}', {
|
||||||
|
responses: {
|
||||||
|
'204': {
|
||||||
|
description: 'Stock PUT success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async replaceById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@requestBody() stock: Stock,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.stockRepository.replaceById(id, stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@del('/stocks/{id}', {
|
||||||
|
responses: {
|
||||||
|
'204': {
|
||||||
|
description: 'Stock DELETE success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async deleteById(@param.path.number('id') id: number): Promise<void> {
|
||||||
|
await this.stockRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
3
src/datasources/README.md
Normal file
3
src/datasources/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Datasources
|
||||||
|
|
||||||
|
This directory contains config for datasources used by this app.
|
1
src/datasources/index.ts
Normal file
1
src/datasources/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './stock-recording-mariadb.datasource';
|
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "stock_recording_mariadb",
|
||||||
|
"connector": "mysql",
|
||||||
|
"url": "",
|
||||||
|
"host": "mysql-server",
|
||||||
|
"port": 3306,
|
||||||
|
"user": "stock-recording",
|
||||||
|
"password": "FIXME",
|
||||||
|
"database": "stock_recording"
|
||||||
|
}
|
36
src/datasources/stock-recording-mariadb.datasource.ts
Normal file
36
src/datasources/stock-recording-mariadb.datasource.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
inject,
|
||||||
|
lifeCycleObserver,
|
||||||
|
LifeCycleObserver,
|
||||||
|
ValueOrPromise,
|
||||||
|
} from '@loopback/core';
|
||||||
|
import {juggler} from '@loopback/repository';
|
||||||
|
import config from './stock-recording-mariadb.datasource.config.json';
|
||||||
|
|
||||||
|
@lifeCycleObserver('datasource')
|
||||||
|
export class StockRecordingMariadbDataSource extends juggler.DataSource
|
||||||
|
implements LifeCycleObserver {
|
||||||
|
static dataSourceName = 'stock_recording_mariadb';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@inject('datasources.config.stock_recording_mariadb', {optional: true})
|
||||||
|
dsConfig: object = config,
|
||||||
|
) {
|
||||||
|
super(dsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the datasource when application is started
|
||||||
|
*/
|
||||||
|
start(): ValueOrPromise<void> {
|
||||||
|
// Add your logic here to be invoked when the application is started
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the datasource when application is stopped. This allows the
|
||||||
|
* application to be shut down gracefully.
|
||||||
|
*/
|
||||||
|
stop(): ValueOrPromise<void> {
|
||||||
|
return super.disconnect();
|
||||||
|
}
|
||||||
|
}
|
16
src/index.ts
Normal file
16
src/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {StockRecordingApplication} from './application';
|
||||||
|
import {ApplicationConfig} from '@loopback/core';
|
||||||
|
|
||||||
|
export {StockRecordingApplication};
|
||||||
|
|
||||||
|
export async function main(options: ApplicationConfig = {}) {
|
||||||
|
const app = new StockRecordingApplication(options);
|
||||||
|
await app.boot();
|
||||||
|
await app.start();
|
||||||
|
|
||||||
|
const url = app.restServer.url;
|
||||||
|
console.log(`Server is running at ${url}`);
|
||||||
|
console.log(`Try ${url}/ping`);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
18
src/keys.ts
Normal file
18
src/keys.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright IBM Corp. 2019. All Rights Reserved.
|
||||||
|
// Node module: @loopback/authentication
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
import {BindingKey} from '@loopback/context';
|
||||||
|
import {BasicAuthenticationUserService} from './basic-auth-user-service';
|
||||||
|
import {UserRepository} from './user.repository';
|
||||||
|
|
||||||
|
export const USER_REPO = BindingKey.create<UserRepository>(
|
||||||
|
'authentication.user.repo',
|
||||||
|
);
|
||||||
|
|
||||||
|
export namespace BasicAuthenticationStrategyBindings {
|
||||||
|
export const USER_SERVICE = BindingKey.create<BasicAuthenticationUserService>(
|
||||||
|
'services.authentication.basic.user.service',
|
||||||
|
);
|
||||||
|
}
|
20
src/migrate.ts
Normal file
20
src/migrate.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {StockRecordingApplication} from './application';
|
||||||
|
|
||||||
|
export async function migrate(args: string[]) {
|
||||||
|
const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter';
|
||||||
|
console.log('Migrating schemas (%s existing schema)', existingSchema);
|
||||||
|
|
||||||
|
const app = new StockRecordingApplication();
|
||||||
|
await app.boot();
|
||||||
|
await app.migrateSchema({existingSchema});
|
||||||
|
|
||||||
|
// Connectors usually keep a pool of opened connections,
|
||||||
|
// this keeps the process running even after all work is done.
|
||||||
|
// We need to exit explicitly.
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate(process.argv).catch(err => {
|
||||||
|
console.error('Cannot migrate database schema', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
3
src/models/README.md
Normal file
3
src/models/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Models
|
||||||
|
|
||||||
|
This directory contains code for models provided by this app.
|
1
src/models/index.ts
Normal file
1
src/models/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './stock.model';
|
46
src/models/stock.model.ts
Normal file
46
src/models/stock.model.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {Entity, model, property} from '@loopback/repository';
|
||||||
|
|
||||||
|
@model()
|
||||||
|
export class Stock extends Entity {
|
||||||
|
@property({
|
||||||
|
type: 'date',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
time: string;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: 'number',
|
||||||
|
id: true,
|
||||||
|
generated: true,
|
||||||
|
})
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
price: number;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
volume: number;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
metric: string;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(data?: Partial<Stock>) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StockRelations {
|
||||||
|
// describe navigational properties here
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StockWithRelations = Stock & StockRelations;
|
3
src/repositories/README.md
Normal file
3
src/repositories/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Repositories
|
||||||
|
|
||||||
|
This directory contains code for repositories provided by this app.
|
1
src/repositories/index.ts
Normal file
1
src/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './stock.repository';
|
16
src/repositories/stock.repository.ts
Normal file
16
src/repositories/stock.repository.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {DefaultCrudRepository} from '@loopback/repository';
|
||||||
|
import {Stock, StockRelations} from '../models';
|
||||||
|
import {StockRecordingMariadbDataSource} from '../datasources';
|
||||||
|
import {inject} from '@loopback/core';
|
||||||
|
|
||||||
|
export class StockRepository extends DefaultCrudRepository<
|
||||||
|
Stock,
|
||||||
|
typeof Stock.prototype.id,
|
||||||
|
StockRelations
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
@inject('datasources.stock_recording_mariadb') dataSource: StockRecordingMariadbDataSource,
|
||||||
|
) {
|
||||||
|
super(Stock, dataSource);
|
||||||
|
}
|
||||||
|
}
|
100
src/sequence.ts
Normal file
100
src/sequence.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import {inject} from '@loopback/context';
|
||||||
|
import {
|
||||||
|
FindRoute,
|
||||||
|
InvokeMethod,
|
||||||
|
ParseParams,
|
||||||
|
Reject,
|
||||||
|
RequestContext,
|
||||||
|
RestBindings,
|
||||||
|
Send,
|
||||||
|
SequenceHandler,
|
||||||
|
} from '@loopback/rest';
|
||||||
|
|
||||||
|
import {AuthenticateFn, AuthenticationBindings} from '@loopback/authentication';
|
||||||
|
import {
|
||||||
|
AUTHENTICATION_STRATEGY_NOT_FOUND,
|
||||||
|
USER_PROFILE_NOT_FOUND,
|
||||||
|
} from '@loopback/authentication';
|
||||||
|
const SequenceActions = RestBindings.SequenceActions;
|
||||||
|
|
||||||
|
//export class MySequence implements SequenceHandler {
|
||||||
|
// constructor(
|
||||||
|
// @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
|
||||||
|
// @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
|
||||||
|
// @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
|
||||||
|
// @inject(SequenceActions.SEND) public send: Send,
|
||||||
|
// @inject(SequenceActions.REJECT) public reject: Reject,
|
||||||
|
// ) {}
|
||||||
|
//
|
||||||
|
// async handle(context: RequestContext) {
|
||||||
|
// try {
|
||||||
|
// const {request, response} = context;
|
||||||
|
// const route = this.findRoute(request);
|
||||||
|
// const args = await this.parseParams(request, route);
|
||||||
|
// const result = await this.invoke(route, args);
|
||||||
|
// this.send(response, result);
|
||||||
|
// } catch (err) {
|
||||||
|
// this.reject(context, err);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
export class MyAuthenticatingSequence implements SequenceHandler {
|
||||||
|
constructor(
|
||||||
|
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
|
||||||
|
@inject(SequenceActions.PARSE_PARAMS)
|
||||||
|
protected parseParams: ParseParams,
|
||||||
|
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
|
||||||
|
@inject(SequenceActions.SEND) protected send: Send,
|
||||||
|
@inject(SequenceActions.REJECT) protected reject: Reject,
|
||||||
|
@inject(AuthenticationBindings.AUTH_ACTION)
|
||||||
|
protected authenticateRequest: AuthenticateFn,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(context: RequestContext) {
|
||||||
|
try {
|
||||||
|
const {request, response} = context;
|
||||||
|
const route = this.findRoute(request);
|
||||||
|
|
||||||
|
//call authentication action
|
||||||
|
await this.authenticateRequest(request);
|
||||||
|
|
||||||
|
// Authentication successful, proceed to invoke controller
|
||||||
|
const args = await this.parseParams(request, route);
|
||||||
|
const result = await this.invoke(route, args);
|
||||||
|
this.send(response, result);
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
// The authentication action utilizes a strategy resolver to find
|
||||||
|
// an authentication strategy by name, and then it calls
|
||||||
|
// strategy.authenticate(request).
|
||||||
|
//
|
||||||
|
// The strategy resolver throws a non-http error if it cannot
|
||||||
|
// resolve the strategy. When the strategy resolver obtains
|
||||||
|
// a strategy, it calls strategy.authenticate(request) which
|
||||||
|
// is expected to return a user profile. If the user profile
|
||||||
|
// is undefined, then it throws a non-http error.
|
||||||
|
//
|
||||||
|
// It is necessary to catch these errors and add HTTP-specific status
|
||||||
|
// code property.
|
||||||
|
//
|
||||||
|
// Errors thrown by the strategy implementations already come
|
||||||
|
// with statusCode set.
|
||||||
|
//
|
||||||
|
// In the future, we want to improve `@loopback/rest` to provide
|
||||||
|
// an extension point allowing `@loopback/authentication` to contribute
|
||||||
|
// mappings from error codes to HTTP status codes, so that application
|
||||||
|
// don't have to map codes themselves.
|
||||||
|
if (
|
||||||
|
error.code === AUTHENTICATION_STRATEGY_NOT_FOUND ||
|
||||||
|
error.code === USER_PROFILE_NOT_FOUND
|
||||||
|
) {
|
||||||
|
Object.assign(error, {statusCode: 401 /* Unauthorized */});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reject(context, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
16
src/user.repository.ts
Normal file
16
src/user.repository.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright IBM Corp. 2019. All Rights Reserved.
|
||||||
|
// Node module: @loopback/authentication
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
import {User} from './user';
|
||||||
|
|
||||||
|
export class UserRepository {
|
||||||
|
constructor(readonly list: {[key: string]: User}) {}
|
||||||
|
find(username: string): User | undefined {
|
||||||
|
const found = Object.keys(this.list).find(
|
||||||
|
k => this.list[k].username === username,
|
||||||
|
);
|
||||||
|
return found ? this.list[found] : undefined;
|
||||||
|
}
|
||||||
|
}
|
12
src/user.ts
Normal file
12
src/user.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright IBM Corp. 2019. All Rights Reserved.
|
||||||
|
// Node module: @loopback/authentication
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
}
|
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@loopback/build/config/tsconfig.common.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user