This commit is contained in:
vzaytsev 2024-11-15 13:59:36 +03:00
parent 4f8a631ae2
commit c5bd7f2bd9
31 changed files with 6418 additions and 1299 deletions

File diff suppressed because it is too large Load Diff

25
LICENSE
View File

@ -1,25 +0,0 @@
Copyright (c) IBM Corp. and LoopBack contributors 2018,2019.
Node module: @loopback/example-hello-world
This project is licensed under the MIT License, full text below.
--------
MIT license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

104
README.md
View File

@ -1,74 +1,92 @@
# @loopback/example-hello-world # loopback4-example-github
A simple hello-world application using LoopBack 4! This LoopBack application is an example to connect to third party REST APIs, GitHub API.
## Summary It shows:
This project shows how to write the simplest LoopBack 4 application possible. - how to define template and options in [REST connector datasource](./src/datasources/githubds.datasource.ts).
Check out - how to traverse pages in the results in the [controller](./src/controllers/gh-query.controller.ts)
[src/application.ts](https://github.com/loopbackio/loopback-next/blob/master/examples/hello-world/src/application.ts)
to learn how we configured our application to always respond with "Hello
World!".
## Prerequisites ## Blog posts
Before we can begin, you'll need to make sure you have some things installed: I'll be creating a series of blog posts on how to create this end-to-end, i.e. from creating APIs in LoopBack application to frontend using React. Stay tuned!
- [Node.js](https://nodejs.org/en/) at v10 or greater - [Part 1: Creating Datasource to GitHub API](https://mobilediana.medium.com/building-an-end-to-end-application-with-loopback-react-js-7a22d726c35d)
- [Part 2: Creating Service Proxy](https://mobilediana.medium.com/building-an-end-to-end-application-with-loopback-react-js-part-2-creating-service-proxy-7ffac2bd7980)
- [Part 3: Pagination in GitHub API Results](https://mobilediana.medium.com/building-an-end-to-end-application-with-loopback-react-js-90cfd7a4813c#a270-8107da706e6f)
Additionally, this tutorial assumes that you are comfortable with certain ---
technologies, languages and concepts.
- JavaScript (ES6) This application is generated using [LoopBack 4 CLI](https://loopback.io/doc/en/lb4/Command-line-interface.html) with the
- [npm](https://www.npmjs.com/) [initial project layout](https://loopback.io/doc/en/lb4/Loopback-application-layout.html).
- [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)
## Installation ## Install dependencies
1. Install the new loopback CLI toolkit. By default, dependencies were installed when this application was generated.
Whenever dependencies in `package.json` are changed, run the following command:
```sh ```sh
npm i -g @loopback/cli npm install
``` ```
2. Download the "hello-world" application. To only install resolved dependencies in `package-lock.json`:
```sh ```sh
lb4 example hello-world npm ci
``` ```
3. Switch to the directory. ## Run the application
```sh
cd loopback4-example-hello-world
```
## Use
Start the app:
```sh ```sh
npm start npm start
``` ```
The application will start on port `3000`. Use your favourite browser or REST You can also run `node .` to skip the build step.
client to access any path with a GET request, and watch it return
`Hello world!`.
## Contributions Open http://127.0.0.1:3000 in your browser.
- [Guidelines](https://github.com/loopbackio/loopback-next/blob/master/docs/CONTRIBUTING.md) ## Rebuild the project
- [Join the team](https://github.com/loopbackio/loopback-next/issues/110)
To incrementally build the project:
```sh
npm run build
```
To force a full build by cleaning up cached artifacts:
```sh
npm run rebuild
```
## Fix code style and formatting issues
```sh
npm run lint
```
To automatically fix such issues:
```sh
npm run lint:fix
```
## Other useful commands
- `npm run migrate`: Migrate database schemas for models
- `npm run openapi-spec`: Generate OpenAPI spec into a file
- `npm run docker:build`: Build a Docker image for this application
- `npm run docker:run`: Run this application inside a Docker container
## Tests ## Tests
Run `npm test` from the root folder. ```sh
npm test
```
## Contributors ## What's next
See Please check out [LoopBack 4 documentation](https://loopback.io/doc/en/lb4/) to
[all contributors](https://github.com/loopbackio/loopback-next/graphs/contributors). understand how you can continue to add features to this application.
## License [![LoopBack](<https://github.com/strongloop/loopback-next/raw/master/docs/site/imgs/branding/Powered-by-LoopBack-Badge-(blue)-@2x.png>)](http://loopback.io/)
MIT

5656
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,20 @@
{ {
"name": "@loopback/example-hello-world", "name": "loopback4-example-github",
"description": "A simple hello-world Application using LoopBack 4", "version": "0.0.1",
"version": "7.0.7", "description": "loopback4-example-github",
"keywords": [ "keywords": [
"loopback", "loopback-application",
"LoopBack", "loopback"
"example",
"tutorial"
], ],
"license": "MIT",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"author": "IBM Corp. and LoopBack contributors",
"copyright.owner": "IBM Corp. and LoopBack contributors",
"repository": {
"type": "git",
"url": "https://github.com/loopbackio/loopback-next.git",
"directory": "examples/hello-world"
},
"engines": { "engines": {
"node": "18 || 20 || 22" "node": ">=10.16"
}, },
"scripts": { "scripts": {
"acceptance": "lb-mocha \"dist/__tests__/acceptance/**/*.js\"",
"build": "lb-tsc", "build": "lb-tsc",
"build:watch": "lb-tsc --watch", "build:watch": "lb-tsc --watch",
"clean": "lb-clean *example-hello-world*.tgz dist *.tsbuildinfo package", "lint": "npm run eslint && npm run prettier:check",
"verify": "npm pack && tar xf *example-hello-world*.tgz && tree package && npm run clean",
"lint": "npm run prettier:check && npm run eslint",
"lint:fix": "npm run eslint:fix && npm run prettier:fix", "lint:fix": "npm run eslint:fix && npm run prettier:fix",
"prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"",
"prettier:check": "npm run prettier:cli -- -l", "prettier:check": "npm run prettier:cli -- -l",
@ -35,27 +22,49 @@
"eslint": "lb-eslint --report-unused-disable-directives .", "eslint": "lb-eslint --report-unused-disable-directives .",
"eslint:fix": "npm run eslint -- --fix", "eslint:fix": "npm run eslint -- --fix",
"pretest": "npm run rebuild", "pretest": "npm run rebuild",
"test": "lb-mocha --allow-console-logs \"dist/__tests__/**/*.js\"", "test": "lb-mocha --allow-console-logs \"dist/__tests__\"",
"posttest": "npm run lint", "posttest": "npm run lint",
"test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest",
"rebuild": "npm run clean && npm run build", "docker:build": "docker build -t loopback4-example-github .",
"docker:run": "docker run -p 3000:3000 -d loopback4-example-github",
"premigrate": "npm run build",
"migrate": "node ./dist/migrate",
"preopenapi-spec": "npm run build",
"openapi-spec": "node ./dist/openapi-spec",
"prestart": "npm run rebuild", "prestart": "npm run rebuild",
"start": "node ." "start": "node -r source-map-support/register .",
"clean": "lb-clean dist *.tsbuildinfo .eslintcache",
"rebuild": "npm run clean && npm run build"
}, },
"publishConfig": { "repository": {
"access": "public" "type": "git",
"url": ""
}, },
"author": "Diana Lau <dhmlau@ca.ibm.com>",
"license": "",
"files": [
"README.md",
"dist",
"src",
"!*/__tests__"
],
"dependencies": { "dependencies": {
"@loopback/core": "^6.1.4", "@loopback/boot": "^3.4.1",
"@loopback/rest": "^14.0.7", "@loopback/core": "^2.16.1",
"tslib": "^2.6.3" "@loopback/repository": "^3.7.0",
"@loopback/rest": "^9.3.1",
"@loopback/rest-explorer": "^3.3.1",
"@loopback/service-proxy": "^3.2.1",
"loopback-connector-rest": "^3.7.0",
"tslib": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@loopback/build": "^11.0.6", "@loopback/build": "^6.4.1",
"@loopback/eslint-config": "^15.0.4", "source-map-support": "^0.5.19",
"@loopback/testlab": "^7.0.6", "@loopback/testlab": "^3.4.1",
"@types/node": "^16.18.119", "@types/node": "^10.17.60",
"eslint": "^8.57.0", "@loopback/eslint-config": "^10.2.1",
"typescript": "~5.2.2" "eslint": "^7.28.0",
"typescript": "~4.3.2"
} }
} }

88
public/index.html Normal file
View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>loopback4-example-github</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>loopback4-example-github</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

@ -1,38 +0,0 @@
// Copyright IBM Corp. and LoopBack contributors 2019. All Rights Reserved.
// Node module: @loopback/example-hello-world
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {
Client,
createRestAppClient,
expect,
givenHttpServerConfig,
} from '@loopback/testlab';
import {HelloWorldApplication} from '../../application';
describe('Application', () => {
let app: HelloWorldApplication;
let client: Client;
before(givenAnApplication);
before(async () => {
await app.start();
client = createRestAppClient(app);
});
after(async () => {
await app.stop();
});
it('responds with hello world', async () => {
const response = await client.get('/').expect(200);
expect(response.text).to.eql('Hello World!');
});
function givenAnApplication() {
app = new HelloWorldApplication({
rest: givenHttpServerConfig(),
disableConsoleLog: true,
});
}
});

View File

@ -0,0 +1,31 @@
import {Client} from '@loopback/testlab';
import {Loopback4ExampleGithubApplication} from '../..';
import {setupApplication} from './test-helper';
describe('HomePage', () => {
let app: Loopback4ExampleGithubApplication;
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 {Loopback4ExampleGithubApplication} from '../..';
import {setupApplication} from './test-helper';
describe('PingController', () => {
let app: Loopback4ExampleGithubApplication;
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 {Loopback4ExampleGithubApplication} 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 Loopback4ExampleGithubApplication({
rest: restConfig,
});
await app.boot();
await app.start();
const client = createRestAppClient(app);
return {app, client};
}
export interface AppWithClient {
app: Loopback4ExampleGithubApplication;
client: Client;
}

View File

@ -1,33 +1,44 @@
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. import {BootMixin} from '@loopback/boot';
// Node module: @loopback/example-hello-world
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {ApplicationConfig} from '@loopback/core'; import {ApplicationConfig} from '@loopback/core';
import {RestApplication, RestServer} from '@loopback/rest'; 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 {ApplicationConfig};
export class HelloWorldApplication extends RestApplication { export class Loopback4ExampleGithubApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) { constructor(options: ApplicationConfig = {}) {
super(options); super(options);
// In this example project, we configure a sequence that always // Set up the custom sequence
// returns the same HTTP response: Hello World! this.sequence(MySequence);
// Learn more about the concept of Sequence in our docs:
// http://loopback.io/doc/en/lb4/Sequence.html // Set up default home page
this.handler(({response}, sequence) => { this.static('/', path.join(__dirname, '../public'));
sequence.send(response, 'Hello World!');
// Customize @loopback/rest-explorer configuration here
this.configure(RestExplorerBindings.COMPONENT).to({
path: '/explorer',
}); });
} this.component(RestExplorerComponent);
async start() { this.projectRoot = __dirname;
await super.start(); // Customize @loopback/boot Booter Conventions here
this.bootOptions = {
if (!this.options?.disableConsoleLog) { controllers: {
const rest = await this.getServer(RestServer); // Customize ControllerBooter Conventions here
console.log( dirs: ['controllers'],
`REST server running on port: ${await rest.get('rest.port')}`, extensions: ['.controller.js'],
); nested: true,
} },
};
} }
} }

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).

View File

@ -0,0 +1,134 @@
// Uncomment these imports to begin using these cool features!
import {inject} from '@loopback/context';
import {get, getModelSchemaRef, param} from '@loopback/openapi-v3';
import {QueryResult, ResultIssueInfo} from '../models';
import {GhQueryService, IssueInfo, QueryResponse} from '../services';
// import {inject} from '@loopback/core';
export class GhQueryController {
// inject the GhQueryService service proxy
constructor(@inject('services.GhQueryService') protected queryService:GhQueryService) {}
// create the API that get the issues by providing:
// repo: <GitHub org>/<GitHub repo>. For example, `strongloop/loopback-next`
// label: If it has special characters, you need to escape it.
// For example, if the label is "help wanted", it will be "help+wanted".
@get('/issues/repo/{repo}/label/{label}', {
responses: {
'200': {
description: 'Array of GitHub issues info',
content: {
'application/json': {
schema: getModelSchemaRef(QueryResult)
}
}
}
}
})
async getIssuesByLabel(
@param.path.string('repo') repo: string,
@param.path.string('label') label:string): Promise<QueryResult> {
let result:QueryResponse = await this.queryService.getIssuesByLabel(repo, label);
let queryResult = new QueryResult();
queryResult.items = [];
queryResult.total_count = result.body.total_count;
result.body.items.forEach(issue => {
this.addToResult(issue, queryResult);
});
// check if there is next page of the results
const nextLink = this.getNextLink(result.headers.link);
if (nextLink == null) return queryResult;
await this.getIssueByURL(nextLink, this.queryService, queryResult);
return queryResult;
}
/**
* Get issues from URL
* @param nextLinkURL
* @param queryService
* @param queryResult
* @returns
*/
async getIssueByURL(nextLinkURL: string, queryService: GhQueryService, queryResult:QueryResult) {
let result = await queryService.getIssuesByURL(nextLinkURL);
result.body.items.forEach(issue => {
this.addToResult(issue, queryResult);
});
const nextLink2 = this.getNextLink(result.headers.link);
if (nextLink2 == null) return;
await this.getIssueByURL(nextLink2, queryService, queryResult);
}
/**
* Get the URL for the "next" page.
* The Link header is in the format of:
* Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next",
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last"
* @param link
* @returns
*/
getNextLink(link: string): string|null {
if (link == undefined) return null;
let tokens: string[] = link.split(',');
let url: string|null = null;
tokens.forEach(token => {
if (token.indexOf('rel="next"')!=-1) {
url = token.substring(token.indexOf('<')+1, token.indexOf(';')-1);
}
});
return url;
}
/**
* Add the issue to the QueryResult object
* @param issue
* @param queryResult
*/
addToResult(issue: IssueInfo, queryResult: QueryResult) {
let issueInfo:ResultIssueInfo = new ResultIssueInfo();
issueInfo.html_url = issue.html_url;
issueInfo.title = issue.title;
issueInfo.state = issue.state;
issueInfo.age = this.getIssueAge(issue.created_at);
queryResult.items?.push(issueInfo);
}
/**
* Calculate the age of the issue
* i.e. take today's date and find the number of days difference from
* the issue creation date
* @param created_at
* @returns
*/
getIssueAge(created_at: string): number {
let todayDate: Date = new Date();
let createDate: Date = new Date(created_at);
let differenceInTime = todayDate.getTime() - createDate.getTime();
//get the difference in day
return Math.floor(differenceInTime / (1000 * 3600 * 24));
}
}
// /**
// * QueryResult
// */
// class QueryResult {
// total_count: number;
// items: ResultIssueInfo[];
// }
// class ResultIssueInfo {
// title: string;
// html_url: string;
// state: string;
// age: number;
// }

2
src/controllers/index.ts Normal file
View File

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

View File

@ -0,0 +1,55 @@
import {inject} from '@loopback/core';
import {
Request,
RestBindings,
get,
response,
ResponseObject,
} from '@loopback/rest';
/**
* 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')
@response(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,3 @@
# Datasources
This directory contains config for datasources used by this app.

View File

@ -0,0 +1,66 @@
import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
import {juggler} from '@loopback/repository';
const config = {
name: 'githubds',
connector: 'rest',
baseURL: 'https://api.github.ibm.com',
crud: false,
options: {
headers: {
accept: 'application/json',
Authorization: process.env.TOKEN,
'User-Agent': 'loopback4-example-github',
'X-RateLimit-Limit': 5000,
'content-type': 'application/json'
}
},
operations: [
{
template: {
method: 'GET',
fullResponse: true,
url: 'https://api.github.com/search/issues?q=repo:{repo}+label:"{label}"'
},
functions: {
getIssuesByLabel: ['repo','label']
}
}, {
template: {
method: 'GET',
fullResponse: true,
url: '{url}'
},
functions: {
getIssuesByURL: ['url']
}
}, {
template: {
method: 'GET',
fullResponse: true,
url: 'https://api.github.com/search/issues?q=repo:{repo}+{querystring}'
},
functions: {
getIssuesWithQueryString: ['repo','querystring']
}
}
]
};
// 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 GithubdsDataSource extends juggler.DataSource
implements LifeCycleObserver {
static dataSourceName = 'githubds';
static readonly defaultConfig = config;
constructor(
@inject('datasources.config.githubds', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}

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

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

View File

@ -1,13 +1,16 @@
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. import {ApplicationConfig, Loopback4ExampleGithubApplication} from './application';
// Node module: @loopback/example-hello-world
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {ApplicationConfig, HelloWorldApplication} from './application'; export * from './application';
export async function main(config: ApplicationConfig) { export async function main(options: ApplicationConfig = {}) {
const app = new HelloWorldApplication(); const app = new Loopback4ExampleGithubApplication(options);
await app.boot();
await app.start(); await app.start();
const url = app.restServer.url;
console.log(`Server is running at ${url}`);
console.log(`Try ${url}/ping`);
return app; return app;
} }
@ -16,7 +19,13 @@ if (require.main === module) {
const config = { const config = {
rest: { rest: {
port: +(process.env.PORT ?? 3000), port: +(process.env.PORT ?? 3000),
host: process.env.HOST ?? 'localhost', 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: { openApiSpec: {
// useful when used with OpenAPI-to-GraphQL to locate your application // useful when used with OpenAPI-to-GraphQL to locate your application
setServersFromRequest: true, setServersFromRequest: true,

20
src/migrate.ts Normal file
View File

@ -0,0 +1,20 @@
import {Loopback4ExampleGithubApplication} 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 Loopback4ExampleGithubApplication();
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.

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

@ -0,0 +1,2 @@
export * from './query-result.model';
export * from './result-issue-info.model';

View File

@ -0,0 +1,24 @@
import {Model, model, property} from '@loopback/repository';
import {ResultIssueInfo} from './result-issue-info.model';
@model()
export class QueryResult extends Model {
@property({
type: 'number',
})
total_count?: number;
@property.array(ResultIssueInfo)
items?: ResultIssueInfo[];
constructor(data?: Partial<QueryResult>) {
super(data);
}
}
export interface QueryResultRelations {
// describe navigational properties here
}
export type QueryResultWithRelations = QueryResult & QueryResultRelations;

View File

@ -0,0 +1,35 @@
import {Model, model, property} from '@loopback/repository';
@model()
export class ResultIssueInfo extends Model {
@property({
type: 'string',
})
title?: string;
@property({
type: 'string',
})
html_url?: string;
@property({
type: 'string',
})
state?: string;
@property({
type: 'number',
})
age?: number;
constructor(data?: Partial<ResultIssueInfo>) {
super(data);
}
}
export interface ResultIssueInfoRelations {
// describe navigational properties here
}
export type ResultIssueInfoWithRelations = ResultIssueInfo & ResultIssueInfoRelations;

23
src/openapi-spec.ts Normal file
View File

@ -0,0 +1,23 @@
import {ApplicationConfig} from '@loopback/core';
import {Loopback4ExampleGithubApplication} 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 Loopback4ExampleGithubApplication(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);
});

View File

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

3
src/sequence.ts Normal file
View File

@ -0,0 +1,3 @@
import {MiddlewareSequence} from '@loopback/rest';
export class MySequence extends MiddlewareSequence {}

View File

@ -0,0 +1,45 @@
import {inject, Provider} from '@loopback/core';
import {getService} from '@loopback/service-proxy';
import {GithubdsDataSource} from '../datasources';
export interface GhQueryService {
// this is where you define the Node.js methods that will be
// mapped to REST/SOAP/gRPC operations as stated in the datasource
// json file.
// Add the three methods here.
// Make sure the function names and the parameter names matches
// the ones you defined in the datasource
getIssuesByLabel(repo: string, label: string): Promise<QueryResponse>;
getIssuesByURL(url: string): Promise<QueryResponse>;
getIssuesWithQueryString(repo:string, querystring: string): Promise<QueryResponse>;
}
export interface QueryResponse {
headers: any;
body: QueryResponseBody;
}
export interface QueryResponseBody {
total_count: number;
items: IssueInfo[];
}
export class IssueInfo {
title: string;
html_url: string;
state: string;
created_at: string;
}
export class GhQueryServiceProvider implements Provider<GhQueryService> {
constructor(
// githubds must match the name property in the datasource json file
@inject('datasources.githubds')
protected dataSource: GithubdsDataSource = new GithubdsDataSource(),
) {}
value(): Promise<GhQueryService> {
return getService(this.dataSource);
}
}

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

@ -0,0 +1 @@
export * from './gh-query-service.service';

View File

@ -1,24 +1,9 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "http://json.schemastore.org/tsconfig",
"extends": "@loopback/build/config/tsconfig.common.json", "extends": "@loopback/build/config/tsconfig.common.json",
"compilerOptions": { "compilerOptions": {
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src"
"composite": true
}, },
"include": [ "include": ["src"]
"src/**/*",
"src/**/*.json"
],
"references": [
{
"path": "../../packages/core/tsconfig.json"
},
{
"path": "../../packages/rest/tsconfig.json"
},
{
"path": "../../packages/testlab/tsconfig.json"
}
]
} }