Initial commit.

This commit is contained in:
yohan 2020-04-30 14:53:27 +02:00
commit 863e113aa8
48 changed files with 6776 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules
npm-debug.log
/dist
# Cache used by TypeScript's incremental build
*.tsbuildinfo

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dist/
coverage/

3
.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: '@loopback/eslint-config',
};

64
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,4 @@
{
"recursive": true,
"require": "source-map-support/register"
}

1
.npmrc Normal file
View File

@ -0,0 +1 @@
package-lock=true

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
dist
*.json

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"bracketSpacing": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "all",
"arrowParens": "avoid"
}

32
.vscode/settings.json vendored Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
{
"@loopback/cli": {
"version": "2.3.1"
}
}

36
DEVELOPING.md Normal file
View 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
View 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
View 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
View 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
View 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);
});
}

1
index.ts Normal file
View File

@ -0,0 +1 @@
export * from './src';

5423
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

74
package.json Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
# Tests
Please place your tests in this folder.

View 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/);
});
});

View 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'});
});
});

View 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
View 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',
},
});
}

View 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
View 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',
});
}
}

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

@ -0,0 +1,2 @@
export * from './ping.controller';
export * from './stock-recording.controller';

View 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),
};
}
}

View 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);
}
}

View File

@ -0,0 +1,3 @@
# Datasources
This directory contains config for datasources used by this app.

1
src/datasources/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './stock-recording-mariadb.datasource';

View File

@ -0,0 +1,10 @@
{
"name": "stock_recording_mariadb",
"connector": "mysql",
"url": "",
"host": "mysql-server",
"port": 3306,
"user": "stock-recording",
"password": "FIXME",
"database": "stock_recording"
}

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

@ -0,0 +1,3 @@
# Models
This directory contains code for models provided by this app.

1
src/models/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './stock.model';

46
src/models/stock.model.ts Normal file
View 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;

View File

@ -0,0 +1,3 @@
# Repositories
This directory contains code for repositories provided by this app.

View File

@ -0,0 +1 @@
export * from './stock.repository';

View 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
View 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
View 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
View 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
View 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"]
}