init
This commit is contained in:
commit
6e32b61bb7
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
node_modules/
|
||||
build/
|
||||
tmp/
|
||||
temp/
|
||||
.env
|
||||
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Ground Control
|
||||
|
||||
|
||||
```shell script
|
||||
npm install -g dtsgenerator
|
||||
dtsgen openapi.yaml > src/openapi/api.ts
|
||||
npm i
|
||||
npm start
|
||||
```
|
||||
|
||||
|
||||
|
||||
137
openapi.yaml
Normal file
137
openapi.yaml
Normal file
@ -0,0 +1,137 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: GroundControl push server API
|
||||
description: Push notifications server for BlueWallet
|
||||
version: 0.0.3
|
||||
servers:
|
||||
- url: http://localhost:3001
|
||||
paths:
|
||||
|
||||
/lightningInvoiceGotSettled:
|
||||
post:
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LightningInvoiceSettledNotification'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
|
||||
/majorTomToGroundControl:
|
||||
post:
|
||||
summary: "Associate bitcoin addressess / ln preimage hashes that you wish to be notified about to specific push token. Token serves as unique identifier of a device/user. Also, OS of the token"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
addresses:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
hashes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
os:
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
|
||||
/ping:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServerInfo'
|
||||
components:
|
||||
schemas:
|
||||
|
||||
ServerInfo:
|
||||
type: "object"
|
||||
properties:
|
||||
name:
|
||||
type: "string"
|
||||
description:
|
||||
type: "string"
|
||||
version:
|
||||
type: "string"
|
||||
uptime:
|
||||
type: "number"
|
||||
|
||||
LightningInvoiceSettledNotification:
|
||||
description: object thats posted to GroundControl to notify end-user that his specific invoice was paid by someone
|
||||
type: "object"
|
||||
properties:
|
||||
memo:
|
||||
type: "string"
|
||||
description: "text that was embedded in invoice paid"
|
||||
preimage:
|
||||
type: "string"
|
||||
description: "hex string preimage"
|
||||
hash:
|
||||
type: "string"
|
||||
description: "hex string preimage hash"
|
||||
amt_paid_sat:
|
||||
type: "number"
|
||||
description: "exactly how much satoshis was paid to make this invoice settked (>= invoice amount)"
|
||||
|
||||
|
||||
PushNotification:
|
||||
description: payload for push notification delivered to phone
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- os
|
||||
- token
|
||||
properties:
|
||||
"type":
|
||||
type: "integer"
|
||||
enum:
|
||||
- 1
|
||||
- 2
|
||||
"token":
|
||||
type: "string"
|
||||
"os":
|
||||
type: "string"
|
||||
enum:
|
||||
- "android"
|
||||
description: >
|
||||
type:
|
||||
* `1` - Your lightning invoice was paid
|
||||
* `2` - New transaction to one of your addresses
|
||||
badge:
|
||||
type: "integer"
|
||||
|
||||
|
||||
PushNotificationLightningInvoicePaid:
|
||||
allOf: # Combines PushNotification and the inline model
|
||||
- $ref: '#/components/schemas/PushNotification'
|
||||
- type: object
|
||||
required:
|
||||
- sat
|
||||
- hash
|
||||
- memo
|
||||
properties:
|
||||
type:
|
||||
type: "integer"
|
||||
enum: [1]
|
||||
sat:
|
||||
type: "integer"
|
||||
description: amount of satoshis
|
||||
hash:
|
||||
type: "string"
|
||||
description: hash of specific ln invoice preimage
|
||||
memo:
|
||||
type: "string"
|
||||
description: text attached to bolt11
|
||||
1460
package-lock.json
generated
Normal file
1460
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "groundcontrol",
|
||||
"version": "0.0.4",
|
||||
"description": "GroundControl push server API",
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
"@types/node": "^8.0.29",
|
||||
"body-parser": "^1.18.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.15.4",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"frisbee": "^3.1.4",
|
||||
"mysql": "^2.14.1",
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"ts-node": "3.3.0",
|
||||
"typeorm": "0.2.25",
|
||||
"typescript": "3.3.3333"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ts-node src/index.ts"
|
||||
}
|
||||
}
|
||||
65
src/class/pusher.ts
Normal file
65
src/class/pusher.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import '../openapi/api';
|
||||
import {getRepository} from "typeorm";
|
||||
import {PushLog} from "../entity/PushLog";
|
||||
import {TokenToAddress} from "../entity/TokenToAddress";
|
||||
const Frisbee = require('frisbee');
|
||||
|
||||
/**
|
||||
* Since we cant attach any code to openapi schema definition, this is a repository of transforming pushnotification object
|
||||
* (thats created from apenapi schema) to actual payload thats gona be pushed to fcm/apns. In most basic case we would
|
||||
* need to only fill title/body according to user language.
|
||||
*
|
||||
* One method per each notification type.
|
||||
*/
|
||||
export class Pusher {
|
||||
|
||||
static async pushLightningInvoicePaid(serverKey: string, pushNotification: Components.Schemas.PushNotificationLightningInvoicePaid) : Promise <[object, object]> {
|
||||
const fcmPayload = {
|
||||
"data" : {},
|
||||
"notification" : {
|
||||
"body" : 'Paid: ' + (pushNotification.memo || 'your invoice'),
|
||||
"title": '+' + pushNotification.sat + ' sats',
|
||||
"badge": pushNotification.badge,
|
||||
}
|
||||
};
|
||||
|
||||
return Pusher._pushToFcm(serverKey, pushNotification.token, fcmPayload, pushNotification);
|
||||
}
|
||||
|
||||
protected static async _pushToFcm(serverKey: string, token: string, fcmPayload: object, pushNotification: Components.Schemas.PushNotification) : Promise <[object, object]> {
|
||||
const _api = new Frisbee({ baseURI: 'https://fcm.googleapis.com' });
|
||||
|
||||
fcmPayload['to'] = token;
|
||||
fcmPayload['priority'] = 'high';
|
||||
|
||||
// now, we pass some of the notification properties as data properties to FCM payload:
|
||||
for (let dataKey of Object.keys(pushNotification)) {
|
||||
if (['token', 'os', 'badge'].includes(dataKey)) continue;
|
||||
fcmPayload['data'][dataKey] = pushNotification[dataKey];
|
||||
}
|
||||
|
||||
const apiResponse = await _api.post(
|
||||
'/fcm/send',
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': 'key=' + serverKey,
|
||||
'Content-Type': 'application/json',
|
||||
'Host': 'fcm.googleapis.com',
|
||||
},
|
||||
body: fcmPayload,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let responseJson = {};
|
||||
if (typeof apiResponse.body === 'object') responseJson = apiResponse.body;
|
||||
delete fcmPayload['to']; // compacting a bit, we dont need token in payload as well
|
||||
|
||||
const PushLogRepository = getRepository(PushLog);
|
||||
await PushLogRepository.save( { token: token, payload: JSON.stringify(fcmPayload), success: !!responseJson['success'] });
|
||||
|
||||
return [fcmPayload, responseJson];
|
||||
}
|
||||
}
|
||||
98
src/controller/GroundControl.ts
Normal file
98
src/controller/GroundControl.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import '../openapi/api';
|
||||
import {getRepository} from "typeorm";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import {User} from "../entity/User";
|
||||
import {TokenToAddress} from "../entity/TokenToAddress";
|
||||
import { TokenToHash } from "../entity/TokenToHash";
|
||||
import {Pusher} from "../class/pusher";
|
||||
const pck = require('../../package.json');
|
||||
|
||||
export class GroundControl {
|
||||
|
||||
private tokenToAddressRepository = getRepository(TokenToAddress);
|
||||
private tokenToHashRepository = getRepository(TokenToHash);
|
||||
|
||||
/**
|
||||
* Submit bitcoin addressess that you wish to be notified about to specific push token. Token serves as unique identifier of a device/user. Also, OS of the token
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param next
|
||||
*/
|
||||
async majorTomToGroundControl(request: Request, response: Response, next: NextFunction) {
|
||||
const body: Paths.MajorTomToGroundControl.Post.RequestBody = request.body;
|
||||
// todo: checks that we are receiving data and that there are not too much records in it (probably 1000 addresses for a start is enough)
|
||||
|
||||
if (!body.addresses || !Array.isArray(body.addresses)) {
|
||||
response.status(500).send('addresses not provided');
|
||||
return;
|
||||
}
|
||||
if (!body.hashes || !Array.isArray(body.hashes)) {
|
||||
response.status(500).send('hashes not provided');
|
||||
return;
|
||||
}
|
||||
if (!body.token || !body.os) {
|
||||
response.status(500).send('token not provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// todo: refactor into single batch save
|
||||
for (const address of body.addresses) {
|
||||
// todo: validate bitcoin address
|
||||
console.log(body.token, '->', address);
|
||||
try {
|
||||
await this.tokenToAddressRepository.save({address, token: body.token, os: body.os});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// todo: refactor into single batch save
|
||||
for (const hash of body.hashes) {
|
||||
// todo: validate hash
|
||||
console.log(body.token, '->', hash);
|
||||
try {
|
||||
await this.tokenToHashRepository.save({ hash, token: body.token, os: body.os});
|
||||
} catch (_) {}
|
||||
}
|
||||
response.status(201).send('');
|
||||
}
|
||||
|
||||
async lightningInvoiceGotSettled(request: Request, response: Response, next: NextFunction) {
|
||||
const body: Paths.LightningInvoiceGotSettled.Post.RequestBody = request.body;
|
||||
// todo: check preimage and hash, lookup token in the db table, and actually send push request
|
||||
|
||||
const hashShouldBe = require('crypto').createHash('sha256').update(Buffer.from(body.preimage, 'hex')).digest('hex');
|
||||
if (hashShouldBe !== body.hash) {
|
||||
response.status(500).send('preimage doesnt match hash');
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenToHash = await this.tokenToHashRepository.findOne({ hash: hashShouldBe });
|
||||
const serverKey = process.env.FCM_SERVER_KEY;
|
||||
if (tokenToHash && serverKey) {
|
||||
console.warn('pushing to token', tokenToHash.token);
|
||||
const pushNotification: Components.Schemas.PushNotificationLightningInvoicePaid = {
|
||||
sat: body.amt_paid_sat,
|
||||
badge: 1,
|
||||
type: 1,
|
||||
os: 'android', // tokenToHash.os
|
||||
token: tokenToHash.token,
|
||||
hash: hashShouldBe,
|
||||
memo: body.memo,
|
||||
};
|
||||
|
||||
await Pusher.pushLightningInvoicePaid(serverKey, pushNotification);
|
||||
}
|
||||
|
||||
response.status(200).send('');
|
||||
}
|
||||
|
||||
async ping(request: Request, response: Response, next: NextFunction) {
|
||||
const serverInfo: Paths.Ping.Get.Responses.$200 = {
|
||||
description: pck.description,
|
||||
version: pck.version,
|
||||
uptime: 666
|
||||
};
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
}
|
||||
26
src/controller/UserController.ts
Normal file
26
src/controller/UserController.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {getRepository} from "typeorm";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import {User} from "../entity/User";
|
||||
|
||||
export class UserController {
|
||||
|
||||
private userRepository = getRepository(User);
|
||||
|
||||
async all(request: Request, response: Response, next: NextFunction) {
|
||||
return this.userRepository.find();
|
||||
}
|
||||
|
||||
async one(request: Request, response: Response, next: NextFunction) {
|
||||
return this.userRepository.findOne(request.params.id);
|
||||
}
|
||||
|
||||
async save(request: Request, response: Response, next: NextFunction) {
|
||||
return this.userRepository.save(request.body);
|
||||
}
|
||||
|
||||
async remove(request: Request, response: Response, next: NextFunction) {
|
||||
let userToRemove = await this.userRepository.findOne(request.params.id);
|
||||
await this.userRepository.remove(userToRemove);
|
||||
}
|
||||
|
||||
}
|
||||
21
src/entity/PushLog.ts
Normal file
21
src/entity/PushLog.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm";
|
||||
|
||||
@Entity()
|
||||
@Index(["token"], { unique: false })
|
||||
export class PushLog {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
token: string;
|
||||
|
||||
@Column()
|
||||
payload: string;
|
||||
|
||||
@Column()
|
||||
success: boolean;
|
||||
|
||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})
|
||||
created: Date;
|
||||
}
|
||||
21
src/entity/TokenToAddress.ts
Normal file
21
src/entity/TokenToAddress.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm";
|
||||
|
||||
@Entity()
|
||||
@Index(["token", "address"], { unique: true })
|
||||
export class TokenToAddress {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
token: string;
|
||||
|
||||
@Column()
|
||||
os: string;
|
||||
|
||||
@Column()
|
||||
address: string;
|
||||
|
||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})
|
||||
created: Date;
|
||||
}
|
||||
21
src/entity/TokenToHash.ts
Normal file
21
src/entity/TokenToHash.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {Entity, PrimaryGeneratedColumn, Column, Index} from "typeorm";
|
||||
|
||||
@Entity()
|
||||
@Index(["token", "hash"], { unique: true })
|
||||
export class TokenToHash {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
token: string;
|
||||
|
||||
@Column()
|
||||
os: string;
|
||||
|
||||
@Column()
|
||||
hash: string;
|
||||
|
||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})
|
||||
created: Date;
|
||||
}
|
||||
18
src/entity/User.ts
Normal file
18
src/entity/User.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
firstName: string;
|
||||
|
||||
@Column()
|
||||
lastName: string;
|
||||
|
||||
@Column()
|
||||
age: number;
|
||||
|
||||
}
|
||||
69
src/index.ts
Normal file
69
src/index.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import "reflect-metadata";
|
||||
import {createConnection} from "typeorm";
|
||||
import * as express from "express";
|
||||
import * as bodyParser from "body-parser";
|
||||
import {Request, Response} from "express";
|
||||
import {Routes} from "./routes";
|
||||
require('dotenv').config();
|
||||
const cors = require('cors')
|
||||
const url = require("url");
|
||||
const parsed = url.parse(process.env.JAWSDB_MARIA_URL);
|
||||
|
||||
createConnection(
|
||||
{
|
||||
"type": "mariadb",
|
||||
"host": parsed.hostname,
|
||||
"port": parsed.port,
|
||||
"username": parsed.auth.split(":")[0],
|
||||
"password": parsed.auth.split(":")[1],
|
||||
"database": parsed.path.replace("/", ""),
|
||||
"synchronize": true,
|
||||
"logging": false,
|
||||
"entities": [
|
||||
"src/entity/**/*.ts"
|
||||
],
|
||||
"migrations": [
|
||||
"src/migration/**/*.ts"
|
||||
],
|
||||
"subscribers": [
|
||||
"src/subscriber/**/*.ts"
|
||||
],
|
||||
"cli": {
|
||||
"entitiesDir": "src/entity",
|
||||
"migrationsDir": "src/migration",
|
||||
"subscribersDir": "src/subscriber"
|
||||
}
|
||||
}
|
||||
).then(async connection => {
|
||||
|
||||
// create express app
|
||||
const app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.use(cors());
|
||||
|
||||
// register express routes from defined application routes
|
||||
Routes.forEach(route => {
|
||||
(app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
|
||||
const result = (new (route.controller as any))[route.action](req, res, next);
|
||||
if (result instanceof Promise) {
|
||||
result.then(result => result !== null && result !== undefined ? res.send(result) : undefined);
|
||||
|
||||
} else if (result !== null && result !== undefined) {
|
||||
res.json(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.set("trust proxy", 1);
|
||||
const rateLimit = require("express-rate-limit");
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
app.listen(process.env.PORT || 3001);
|
||||
|
||||
console.log("Express server has started on port ", (process.env.PORT || 3001));
|
||||
|
||||
}).catch(error => console.log(error));
|
||||
106
src/openapi/api.ts
Normal file
106
src/openapi/api.ts
Normal file
@ -0,0 +1,106 @@
|
||||
declare namespace Components {
|
||||
namespace Schemas {
|
||||
/**
|
||||
* object thats posted to GroundControl to notify end-user that his specific invoice was paid by someone
|
||||
*/
|
||||
export interface LightningInvoiceSettledNotification {
|
||||
/**
|
||||
* text that was embedded in invoice paid
|
||||
*/
|
||||
memo?: string;
|
||||
/**
|
||||
* hex string preimage
|
||||
*/
|
||||
preimage?: string;
|
||||
/**
|
||||
* hex string preimage hash
|
||||
*/
|
||||
hash?: string;
|
||||
/**
|
||||
* exactly how much satoshis was paid to make this invoice settked (>= invoice amount)
|
||||
*/
|
||||
amt_paid_sat?: number;
|
||||
}
|
||||
/**
|
||||
* payload for push notification delivered to phone
|
||||
*/
|
||||
export interface PushNotification {
|
||||
type: 1 | 2;
|
||||
token: string;
|
||||
/**
|
||||
* type:
|
||||
* * `1` - Your lightning invoice was paid
|
||||
* * `2` - New transaction to one of your addresses
|
||||
*
|
||||
*/
|
||||
os: "android";
|
||||
badge?: number;
|
||||
}
|
||||
/**
|
||||
* payload for push notification delivered to phone
|
||||
*/
|
||||
export interface PushNotificationLightningInvoicePaid {
|
||||
type: 1;
|
||||
token: string;
|
||||
/**
|
||||
* type:
|
||||
* * `1` - Your lightning invoice was paid
|
||||
* * `2` - New transaction to one of your addresses
|
||||
*
|
||||
*/
|
||||
os: "android";
|
||||
badge?: number;
|
||||
/**
|
||||
* amount of satoshis
|
||||
*/
|
||||
sat: number;
|
||||
/**
|
||||
* hash of specific ln invoice preimage
|
||||
*/
|
||||
hash: string;
|
||||
/**
|
||||
* text attached to bolt11
|
||||
*/
|
||||
memo: string;
|
||||
}
|
||||
export interface ServerInfo {
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
uptime?: number;
|
||||
}
|
||||
}
|
||||
}
|
||||
declare namespace Paths {
|
||||
namespace LightningInvoiceGotSettled {
|
||||
namespace Post {
|
||||
export type RequestBody = /* object thats posted to GroundControl to notify end-user that his specific invoice was paid by someone */ Components.Schemas.LightningInvoiceSettledNotification;
|
||||
namespace Responses {
|
||||
export interface $200 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace MajorTomToGroundControl {
|
||||
namespace Post {
|
||||
export interface RequestBody {
|
||||
addresses?: string[];
|
||||
hashes?: string[];
|
||||
token?: string;
|
||||
os?: string;
|
||||
}
|
||||
namespace Responses {
|
||||
export interface $201 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace Ping {
|
||||
namespace Get {
|
||||
namespace Responses {
|
||||
export type $200 = Components.Schemas.ServerInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
src/routes.ts
Normal file
45
src/routes.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {UserController} from "./controller/UserController";
|
||||
import {GroundControl} from "./controller/GroundControl";
|
||||
|
||||
export const Routes = [{
|
||||
method: "get",
|
||||
route: "/users",
|
||||
controller: UserController,
|
||||
action: "all"
|
||||
}, {
|
||||
method: "get",
|
||||
route: "/users/:id",
|
||||
controller: UserController,
|
||||
action: "one"
|
||||
}, {
|
||||
method: "post",
|
||||
route: "/users",
|
||||
controller: UserController,
|
||||
action: "save"
|
||||
}, {
|
||||
method: "delete",
|
||||
route: "/users/:id",
|
||||
controller: UserController,
|
||||
action: "remove"
|
||||
}
|
||||
|
||||
|
||||
,{
|
||||
method: "post",
|
||||
route: "/majorTomToGroundControl",
|
||||
controller: GroundControl,
|
||||
action: "majorTomToGroundControl"
|
||||
},{
|
||||
method: "post",
|
||||
route: "/lightningInvoiceGotSettled",
|
||||
controller: GroundControl,
|
||||
action: "lightningInvoiceGotSettled"
|
||||
},{
|
||||
method: "get",
|
||||
route: "/ping",
|
||||
controller: GroundControl,
|
||||
action: "ping"
|
||||
}
|
||||
|
||||
|
||||
];
|
||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6"
|
||||
],
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user