Chapter 11: initial samples

This commit is contained in:
Beth Griggs
2020-07-20 01:59:08 +01:00
parent 98bc761e2a
commit 33cdc5ae35
80 changed files with 7888 additions and 0 deletions
@@ -0,0 +1,5 @@
node_modules
npm-debug.log
/dist
# Cache used by TypeScript's incremental build
*.tsbuildinfo
@@ -0,0 +1,3 @@
node_modules/
dist/
coverage/
@@ -0,0 +1,3 @@
module.exports = {
extends: '@loopback/eslint-config',
};
+64
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
@@ -0,0 +1,4 @@
{
"recursive": true,
"require": "source-map-support/register"
}
+2
View File
@@ -0,0 +1,2 @@
package-lock=true
scripts-prepend-node-path=true
@@ -0,0 +1,2 @@
dist
*.json
+7
View File
@@ -0,0 +1,7 @@
{
"bracketSpacing": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "all",
"arrowParens": "avoid"
}
+32
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
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"]
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"@loopback/cli": {
"version": "2.10.0",
"packageManager": "npm"
}
}
@@ -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
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", "." ]
+3
View File
@@ -0,0 +1,3 @@
# loopback-bookstore
[![LoopBack](https://github.com/strongloop/loopback-next/raw/master/docs/site/imgs/branding/Powered-by-LoopBack-Badge-(blue)-@2x.png)](http://loopback.io/)
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
{
"name": "loopback-bookstore",
"version": "1.0.0",
"description": "loopback-bookstore",
"keywords": [
"loopback-application",
"loopback"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"engines": {
"node": ">=10.16"
},
"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 loopback-bookstore .",
"docker:run": "docker run -p 3000:3000 -d loopback-bookstore",
"migrate": "node ./dist/migrate",
"openapi-spec": "node ./dist/openapi-spec",
"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",
"dist",
"src",
"!*/__tests__"
],
"dependencies": {
"@loopback/boot": "^2.3.5",
"@loopback/core": "^2.9.1",
"@loopback/openapi-v3": "^3.4.5",
"@loopback/repository": "^2.9.0",
"@loopback/rest": "^5.2.0",
"@loopback/rest-explorer": "^2.2.6",
"@loopback/service-proxy": "^2.3.4",
"tslib": "^2.0.0"
},
"devDependencies": {
"@loopback/build": "^6.1.0",
"source-map-support": "^0.5.19",
"@loopback/testlab": "^3.2.0",
"@types/node": "^10.17.26",
"@loopback/eslint-config": "^8.0.3",
"eslint": "^7.3.1",
"typescript": "~3.9.5"
}
}
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>loopback-bookstore</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>loopback-bookstore</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>
@@ -0,0 +1,3 @@
# Tests
Please place your tests in this folder.
@@ -0,0 +1,31 @@
import {Client} from '@loopback/testlab';
import {LoopbackBookstoreApplication} from '../..';
import {setupApplication} from './test-helper';
describe('HomePage', () => {
let app: LoopbackBookstoreApplication;
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/);
});
});
@@ -0,0 +1,21 @@
import {Client, expect} from '@loopback/testlab';
import {LoopbackBookstoreApplication} from '../..';
import {setupApplication} from './test-helper';
describe('PingController', () => {
let app: LoopbackBookstoreApplication;
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'});
});
});
@@ -0,0 +1,32 @@
import {LoopbackBookstoreApplication} 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 LoopbackBookstoreApplication({
rest: restConfig,
});
await app.boot();
await app.start();
const client = createRestAppClient(app);
return {app, client};
}
export interface AppWithClient {
app: LoopbackBookstoreApplication;
client: Client;
}
@@ -0,0 +1,44 @@
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 {MySequence} from './sequence';
export {ApplicationConfig};
export class LoopbackBookstoreApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) {
super(options);
// Set up the custom sequence
this.sequence(MySequence);
// 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,
},
};
}
}
@@ -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).
@@ -0,0 +1,173 @@
import {
Count,
CountSchema,
Filter,
FilterExcludingWhere,
repository,
Where,
} from '@loopback/repository';
import {
post,
param,
get,
getModelSchemaRef,
patch,
put,
del,
requestBody,
} from '@loopback/rest';
import {Book} from '../models';
import {BookRepository} from '../repositories';
export class BooksController {
constructor(
@repository(BookRepository)
public bookRepository : BookRepository,
) {}
@post('/books', {
responses: {
'200': {
description: 'Book model instance',
content: {'application/json': {schema: getModelSchemaRef(Book)}},
},
},
})
async create(
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Book, {
title: 'NewBook',
exclude: ['id'],
}),
},
},
})
book: Omit<Book, 'id'>,
): Promise<Book> {
return this.bookRepository.create(book);
}
@get('/books/count', {
responses: {
'200': {
description: 'Book model count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async count(
@param.where(Book) where?: Where<Book>,
): Promise<Count> {
return this.bookRepository.count(where);
}
@get('/books', {
responses: {
'200': {
description: 'Array of Book model instances',
content: {
'application/json': {
schema: {
type: 'array',
items: getModelSchemaRef(Book, {includeRelations: true}),
},
},
},
},
},
})
async find(
@param.filter(Book) filter?: Filter<Book>,
): Promise<Book[]> {
return this.bookRepository.find(filter);
}
@patch('/books', {
responses: {
'200': {
description: 'Book PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Book, {partial: true}),
},
},
})
book: Book,
@param.where(Book) where?: Where<Book>,
): Promise<Count> {
return this.bookRepository.updateAll(book, where);
}
@get('/books/{id}', {
responses: {
'200': {
description: 'Book model instance',
content: {
'application/json': {
schema: getModelSchemaRef(Book, {includeRelations: true}),
},
},
},
},
})
async findById(
@param.path.number('id') id: number,
@param.filter(Book, {exclude: 'where'}) filter?: FilterExcludingWhere<Book>
): Promise<Book> {
return this.bookRepository.findById(id, filter);
}
@patch('/books/{id}', {
responses: {
'204': {
description: 'Book PATCH success',
},
},
})
async updateById(
@param.path.number('id') id: number,
@requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Book, {partial: true}),
},
},
})
book: Book,
): Promise<void> {
await this.bookRepository.updateById(id, book);
}
@put('/books/{id}', {
responses: {
'204': {
description: 'Book PUT success',
},
},
})
async replaceById(
@param.path.number('id') id: number,
@requestBody() book: Book,
): Promise<void> {
await this.bookRepository.replaceById(id, book);
}
@del('/books/{id}', {
responses: {
'204': {
description: 'Book DELETE success',
},
},
})
async deleteById(@param.path.number('id') id: number): Promise<void> {
await this.bookRepository.deleteById(id);
}
}
@@ -0,0 +1,2 @@
export * from './ping.controller';
export * from './books.controller';
@@ -0,0 +1,52 @@
import {Request, RestBindings, get, ResponseObject} from '@loopback/rest';
import {inject} from '@loopback/core';
/**
* 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),
};
}
}
@@ -0,0 +1,3 @@
# Datasources
This directory contains config for datasources used by this app.
@@ -0,0 +1 @@
export * from './local.datasource';
@@ -0,0 +1,27 @@
import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
import {juggler} from '@loopback/repository';
const config = {
name: 'local',
connector: 'memory',
localStorage: '',
file: ''
};
// Observe application's life cycle to disconnect the datasource when
// application is stopped. This allows the application to be shut down
// gracefully. The `stop()` method is inherited from `juggler.DataSource`.
// Learn more at https://loopback.io/doc/en/lb4/Life-cycle.html
@lifeCycleObserver('datasource')
export class LocalDataSource extends juggler.DataSource
implements LifeCycleObserver {
static dataSourceName = 'local';
static readonly defaultConfig = config;
constructor(
@inject('datasources.config.local', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}
+39
View File
@@ -0,0 +1,39 @@
import {ApplicationConfig, LoopbackBookstoreApplication} from './application';
export * from './application';
export async function main(options: ApplicationConfig = {}) {
const app = new LoopbackBookstoreApplication(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;
}
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,
},
},
};
main(config).catch(err => {
console.error('Cannot start the application.', err);
process.exit(1);
});
}
@@ -0,0 +1,20 @@
import {LoopbackBookstoreApplication} 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 LoopbackBookstoreApplication();
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);
});
@@ -0,0 +1,3 @@
# Models
This directory contains code for models provided by this app.
@@ -0,0 +1,34 @@
import {Entity, model, property} from '@loopback/repository';
@model()
export class Book extends Entity {
@property({
type: 'number',
id: true,
generated: true,
})
id?: number;
@property({
type: 'string',
required: true,
})
title: string;
@property({
type: 'string',
required: true,
})
author: string;
constructor(data?: Partial<Book>) {
super(data);
}
}
export interface BookRelations {
// describe navigational properties here
}
export type BookWithRelations = Book & BookRelations;
@@ -0,0 +1 @@
export * from './book.model';
@@ -0,0 +1,23 @@
import {ApplicationConfig} from '@loopback/core';
import {LoopbackBookstoreApplication} from './application';
/**
* Export the OpenAPI spec from the application
*/
async function exportOpenApiSpec(): Promise<void> {
const config: ApplicationConfig = {
rest: {
port: +(process.env.PORT ?? 3000),
host: process.env.HOST ?? 'localhost',
},
};
const outFile = process.argv[2] ?? '';
const app = new LoopbackBookstoreApplication(config);
await app.boot();
await app.exportOpenApiSpec(outFile);
}
exportOpenApiSpec().catch(err => {
console.error('Fail to export OpenAPI spec from the application.', err);
process.exit(1);
});
@@ -0,0 +1,3 @@
# Repositories
This directory contains code for repositories provided by this app.
@@ -0,0 +1,16 @@
import {DefaultCrudRepository} from '@loopback/repository';
import {Book, BookRelations} from '../models';
import {LocalDataSource} from '../datasources';
import {inject} from '@loopback/core';
export class BookRepository extends DefaultCrudRepository<
Book,
typeof Book.prototype.id,
BookRelations
> {
constructor(
@inject('datasources.local') dataSource: LocalDataSource,
) {
super(Book, dataSource);
}
}
@@ -0,0 +1 @@
export * from './book.repository';
@@ -0,0 +1,45 @@
import {inject} from '@loopback/core';
import {
FindRoute,
InvokeMethod,
InvokeMiddleware,
ParseParams,
Reject,
RequestContext,
RestBindings,
Send,
SequenceHandler,
} from '@loopback/rest';
const SequenceActions = RestBindings.SequenceActions;
export class MySequence implements SequenceHandler {
/**
* Optional invoker for registered middleware in a chain.
* To be injected via SequenceActions.INVOKE_MIDDLEWARE.
*/
@inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true})
protected invokeMiddleware: InvokeMiddleware = () => false;
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 finished = await this.invokeMiddleware(context);
if (finished) return;
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);
}
}
}
@@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "@loopback/build/config/tsconfig.common.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}