Initial commit
This commit is contained in:
21
src/Server.integration.spec.ts
Normal file
21
src/Server.integration.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { expect, describe, it, beforeAll, afterAll } from "vitest";
|
||||
import { PlatformTest } from "@tsed/common";
|
||||
import SuperTest from "supertest";
|
||||
import { Server } from "./Server";
|
||||
|
||||
describe("Server", () => {
|
||||
beforeAll(PlatformTest.bootstrap(Server));
|
||||
afterAll(PlatformTest.reset);
|
||||
|
||||
it("should call GET /rest", async () => {
|
||||
const request = SuperTest(PlatformTest.callback());
|
||||
const response = await request.get("/rest").expect(404);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
errors: [],
|
||||
message: 'Resource "/rest" not found',
|
||||
name: "NOT_FOUND",
|
||||
status: 404
|
||||
});
|
||||
});
|
||||
});
|
68
src/Server.ts
Normal file
68
src/Server.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { join } from "path";
|
||||
import { Configuration, Inject } from "@tsed/di";
|
||||
import { PlatformApplication } from "@tsed/common";
|
||||
import "@tsed/platform-express"; // /!\ keep this import
|
||||
import "@tsed/ajv";
|
||||
import "@tsed/swagger";
|
||||
import "@tsed/passport";
|
||||
import { config } from "./config/index";
|
||||
import * as rest from "./controllers/rest/index";
|
||||
import * as pages from "./controllers/pages/index";
|
||||
|
||||
@Configuration({
|
||||
...config,
|
||||
acceptMimes: ["application/json"],
|
||||
httpPort: process.env.PORT || 8083,
|
||||
httpsPort: false, // CHANGE
|
||||
disableComponentsScan: true,
|
||||
ajv: {
|
||||
returnsCoercedValues: true
|
||||
},
|
||||
mount: {
|
||||
"/rest": [...Object.values(rest)],
|
||||
"/": [...Object.values(pages)]
|
||||
},
|
||||
swagger: [
|
||||
{
|
||||
path: "/doc",
|
||||
specVersion: "3.0.1"
|
||||
}
|
||||
],
|
||||
middlewares: [
|
||||
"cors",
|
||||
"cookie-parser",
|
||||
"compression",
|
||||
"method-override",
|
||||
"json-parser",
|
||||
{ use: "urlencoded-parser", options: { extended: true } },
|
||||
{
|
||||
use: "express-session",
|
||||
options: {
|
||||
secret: process.env.PASSPORT_SESSION_KEY || "mysecretkey",
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
// maxAge: 36000,
|
||||
cookie: {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: false
|
||||
// maxAge: null
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
views: {
|
||||
root: join(process.cwd(), "../views"),
|
||||
extensions: {
|
||||
ejs: "ejs"
|
||||
}
|
||||
},
|
||||
exclude: ["**/*.spec.ts"]
|
||||
})
|
||||
export class Server {
|
||||
@Inject()
|
||||
protected app: PlatformApplication;
|
||||
|
||||
@Configuration()
|
||||
protected settings: Configuration;
|
||||
}
|
7
src/config/envs/index.ts
Normal file
7
src/config/envs/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import dotenv from "dotenv-flow";
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || "development";
|
||||
|
||||
export const config = dotenv.config();
|
||||
export const isProduction = process.env.NODE_ENV === "production";
|
||||
export const envs = process.env;
|
10
src/config/index.ts
Normal file
10
src/config/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { envs } from "./envs/index";
|
||||
import loggerConfig from "./logger/index";
|
||||
const pkg = JSON.parse(readFileSync("./package.json", { encoding: "utf8" }));
|
||||
|
||||
export const config: Partial<TsED.Configuration> = {
|
||||
version: pkg.version,
|
||||
envs,
|
||||
logger: loggerConfig
|
||||
};
|
24
src/config/logger/index.ts
Normal file
24
src/config/logger/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { $log, DILoggerOptions } from "@tsed/common";
|
||||
import { isProduction } from "../envs/index";
|
||||
|
||||
if (isProduction) {
|
||||
$log.appenders.set("stdout", {
|
||||
type: "stdout",
|
||||
levels: ["info", "debug"],
|
||||
layout: {
|
||||
type: "json"
|
||||
}
|
||||
});
|
||||
|
||||
$log.appenders.set("stderr", {
|
||||
levels: ["trace", "fatal", "error", "warn"],
|
||||
type: "stderr",
|
||||
layout: {
|
||||
type: "json"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default <DILoggerOptions>{
|
||||
disableRoutesSummary: isProduction
|
||||
};
|
29
src/controllers/pages/IndexController.ts
Normal file
29
src/controllers/pages/IndexController.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Constant, Controller } from "@tsed/di";
|
||||
import { HeaderParams } from "@tsed/platform-params";
|
||||
import { View } from "@tsed/platform-views";
|
||||
import { SwaggerSettings } from "@tsed/swagger";
|
||||
import { Hidden, Get, Returns } from "@tsed/schema";
|
||||
|
||||
@Hidden()
|
||||
@Controller("/")
|
||||
export class IndexController {
|
||||
@Constant("swagger", [])
|
||||
private swagger: SwaggerSettings[];
|
||||
|
||||
@Get("/")
|
||||
@View("swagger.ejs")
|
||||
@(Returns(200, String).ContentType("text/html"))
|
||||
get(@HeaderParams("x-forwarded-proto") protocol: string, @HeaderParams("host") host: string) {
|
||||
const hostUrl = `${protocol || "http"}://${host}`;
|
||||
|
||||
return {
|
||||
BASE_URL: hostUrl,
|
||||
docs: this.swagger.map((conf) => {
|
||||
return {
|
||||
url: hostUrl + conf.path,
|
||||
...conf
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
5
src/controllers/pages/index.ts
Normal file
5
src/controllers/pages/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from "./IndexController";
|
39
src/controllers/rest/LinkController.integration.spec.ts
Normal file
39
src/controllers/rest/LinkController.integration.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { expect, describe, it, afterAll, beforeAll } from "vitest";
|
||||
import { PlatformTest } from "@tsed/common";
|
||||
import SuperTest from "supertest";
|
||||
import { LinkController } from "./LinkController";
|
||||
import { Server } from "../../Server";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
describe("LinkController", () => {
|
||||
beforeAll(
|
||||
PlatformTest.bootstrap(Server, {
|
||||
mount: {
|
||||
"/rest": [LinkController]
|
||||
}
|
||||
})
|
||||
);
|
||||
afterAll(PlatformTest.reset);
|
||||
|
||||
it("should call POST /rest/links and GET /rest/links/:id", async () => {
|
||||
const request = SuperTest(PlatformTest.callback());
|
||||
const username = `silentsilas-${randomUUID()}`;
|
||||
const response = await request
|
||||
.post("/rest/links")
|
||||
.send({
|
||||
service: "github",
|
||||
serviceUsername: username
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const response2 = await request.get(`/rest/users/${response.body.userId}`).expect(200);
|
||||
|
||||
expect(response.body.id).toBeTruthy();
|
||||
expect(response.body.service).toEqual("github");
|
||||
expect(response.body.serviceUsername).toEqual(username);
|
||||
|
||||
expect(response2.body.id).toEqual(response.body.userId);
|
||||
expect(response2.body.service).toEqual("github");
|
||||
expect(response2.body.serviceUsername).toEqual(username);
|
||||
});
|
||||
});
|
67
src/controllers/rest/LinkController.ts
Normal file
67
src/controllers/rest/LinkController.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { BodyParams, PathParams } from "@tsed/platform-params";
|
||||
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
||||
import { Controller, Inject } from "@tsed/di";
|
||||
import { Link } from "../../entities/link/Link";
|
||||
import { SqliteDatasource } from "../../datasources/SqliteDatasource";
|
||||
import { DataSource } from "typeorm";
|
||||
import { User } from "../../entities/User";
|
||||
import { CreateLinkDto } from "../../entities/link/CreateLinkDTO";
|
||||
import { executeWithRetry } from "../../datasources/SqliteDatasource";
|
||||
|
||||
@Controller("/links")
|
||||
export class LinkController {
|
||||
constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {}
|
||||
|
||||
@Post("/")
|
||||
@Summary("Create a new link")
|
||||
@Description("Creates a new link and associates it with a user")
|
||||
@Returns(201, Link)
|
||||
async create(@BodyParams() linkData: CreateLinkDto): Promise<Link> {
|
||||
return executeWithRetry(async (queryRunner) => {
|
||||
const userRepository = queryRunner.manager.getRepository(User);
|
||||
const linkRepository = queryRunner.manager.getRepository(Link);
|
||||
|
||||
let user = await userRepository.findOne({
|
||||
where: {
|
||||
service: linkData.service,
|
||||
serviceUsername: linkData.serviceUsername
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = userRepository.create({
|
||||
service: linkData.service,
|
||||
serviceUsername: linkData.serviceUsername
|
||||
});
|
||||
user = await queryRunner.manager.save(User, user);
|
||||
}
|
||||
|
||||
const link = linkRepository.create({
|
||||
...linkData,
|
||||
user
|
||||
});
|
||||
|
||||
return queryRunner.manager.save(Link, link);
|
||||
}, this.sqliteDataSource);
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@Summary("Get all links")
|
||||
@(Returns(200, Array).Of(Link))
|
||||
async getList(): Promise<Link[]> {
|
||||
return executeWithRetry(async (queryRunner) => {
|
||||
const linkRepository = queryRunner.manager.getRepository(Link);
|
||||
return linkRepository.find({ relations: ["user"] });
|
||||
}, this.sqliteDataSource);
|
||||
}
|
||||
|
||||
@Get("/:id")
|
||||
@Summary("Get a link by ID")
|
||||
@Returns(200, Link)
|
||||
async getOne(@PathParams("id") id: string): Promise<Link | null> {
|
||||
return executeWithRetry(async (queryRunner) => {
|
||||
const linkRepository = queryRunner.manager.getRepository(Link);
|
||||
return linkRepository.findOne({ where: { id }, relations: ["user"] });
|
||||
}, this.sqliteDataSource);
|
||||
}
|
||||
}
|
44
src/controllers/rest/UserController.integration.spec.ts
Normal file
44
src/controllers/rest/UserController.integration.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { expect, describe, it, beforeEach, afterEach, beforeAll } from "vitest";
|
||||
import { PlatformTest } from "@tsed/common";
|
||||
import { UserController } from "./UserController";
|
||||
import { User } from "../../entities/User";
|
||||
import { DataSource } from "typeorm";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Server } from "../../Server";
|
||||
import { sqliteDatasource } from "src/datasources/SqliteDatasource";
|
||||
|
||||
describe("UserController", () => {
|
||||
let controller: UserController;
|
||||
|
||||
beforeAll(PlatformTest.bootstrap(Server));
|
||||
|
||||
beforeEach(async () => {
|
||||
controller = await PlatformTest.invoke(UserController, [
|
||||
{
|
||||
token: DataSource,
|
||||
use: sqliteDatasource
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
PlatformTest.reset();
|
||||
});
|
||||
|
||||
it("should get a user by ID", async () => {
|
||||
const userId = uuidv4();
|
||||
const user = new User();
|
||||
user.id = userId;
|
||||
user.service = "github";
|
||||
user.serviceUsername = `silentsilas-${userId}`;
|
||||
const repo = sqliteDatasource.getRepository(User);
|
||||
await repo.save(user);
|
||||
|
||||
const result = await controller.getOne(userId);
|
||||
|
||||
expect(result).toEqual(user);
|
||||
expect(result?.id).toEqual(userId);
|
||||
expect(result?.service).toEqual("github");
|
||||
expect(result?.serviceUsername).toEqual(`silentsilas-${userId}`);
|
||||
});
|
||||
});
|
23
src/controllers/rest/UserController.spec.ts
Normal file
23
src/controllers/rest/UserController.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect, describe, it, beforeEach, afterEach } from "vitest";
|
||||
import { PlatformTest } from "@tsed/common";
|
||||
import { UserController } from "./UserController";
|
||||
|
||||
describe("UserController", () => {
|
||||
beforeEach(PlatformTest.create);
|
||||
afterEach(PlatformTest.reset);
|
||||
|
||||
it("works", () => {
|
||||
const instance = PlatformTest.get<UserController>(UserController);
|
||||
// const instance = PlatformTest.invoke<HelloWorldController>(HelloWorldController); // get fresh instance
|
||||
|
||||
expect(instance).toBeInstanceOf(UserController);
|
||||
});
|
||||
|
||||
it("gets a list of users", async () => {
|
||||
const instance = PlatformTest.get<UserController>(UserController);
|
||||
|
||||
const users = await instance.getList();
|
||||
|
||||
expect(users).toBeInstanceOf(Array);
|
||||
});
|
||||
});
|
44
src/controllers/rest/UserController.ts
Normal file
44
src/controllers/rest/UserController.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { PathParams } from "@tsed/platform-params";
|
||||
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
||||
import { Controller, Inject } from "@tsed/di";
|
||||
import { User } from "../../entities/User";
|
||||
import { SqliteDatasource } from "../../datasources/SqliteDatasource";
|
||||
import { DataSource } from "typeorm";
|
||||
import { executeWithRetry } from "../../datasources/SqliteDatasource";
|
||||
import { Forbidden } from "@tsed/exceptions";
|
||||
|
||||
@Controller("/users")
|
||||
export class UserController {
|
||||
constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {}
|
||||
|
||||
// disable the create method and endpoint
|
||||
@Post("/")
|
||||
@Summary("Create a new user")
|
||||
@Description("This endpoint can not be used. Users are created automatically when a new link is created.")
|
||||
@Returns(403)
|
||||
async create(): Promise<void> {
|
||||
throw new Forbidden("Users are created automatically when a new link is created.");
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@Summary("Get all users")
|
||||
@(Returns(200, Array).Of(User))
|
||||
async getList(): Promise<User[]> {
|
||||
return executeWithRetry(async (queryRunner) => {
|
||||
return queryRunner.manager.find(User);
|
||||
}, this.sqliteDataSource);
|
||||
}
|
||||
|
||||
@Get("/:id")
|
||||
@Summary("Get a user by ID")
|
||||
@Returns(200, User)
|
||||
async getOne(@PathParams("id") id: string): Promise<User | null> {
|
||||
return executeWithRetry(async (queryRunner) => {
|
||||
return queryRunner.manager.findOne(User, {
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}, this.sqliteDataSource);
|
||||
}
|
||||
}
|
6
src/controllers/rest/index.ts
Normal file
6
src/controllers/rest/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from "./LinkController";
|
||||
export * from "./UserController";
|
16
src/datasources/SqliteDatasource.spec.ts
Normal file
16
src/datasources/SqliteDatasource.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { expect, describe, it, beforeEach, afterEach } from "vitest";
|
||||
import { PlatformTest } from "@tsed/common";
|
||||
import { SqliteDatasource } from "./SqliteDatasource";
|
||||
import { DataSource } from "typeorm";
|
||||
|
||||
describe("SqliteDatasource", () => {
|
||||
beforeEach(PlatformTest.create);
|
||||
afterEach(PlatformTest.reset);
|
||||
|
||||
it("should do something", () => {
|
||||
const instance = PlatformTest.get<SqliteDatasource>(SqliteDatasource);
|
||||
// const instance = PlatformTest.invoke<SqliteDatasource>(SqliteDatasource); // get fresh instance
|
||||
|
||||
expect(instance).toBeInstanceOf(DataSource);
|
||||
});
|
||||
});
|
84
src/datasources/SqliteDatasource.ts
Normal file
84
src/datasources/SqliteDatasource.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { registerProvider } from "@tsed/di";
|
||||
import { DataSource } from "typeorm";
|
||||
import { Logger } from "@tsed/logger";
|
||||
import { User } from "../entities/User";
|
||||
import { Link } from "../entities/link/Link";
|
||||
import { QueryRunner } from "typeorm";
|
||||
|
||||
export const SqliteDatasource = Symbol.for("SqliteDatasource");
|
||||
export type SqliteDatasource = DataSource;
|
||||
export const sqliteDatasource =
|
||||
process.env.NODE_ENV === "test"
|
||||
? new DataSource({
|
||||
type: "sqlite",
|
||||
entities: [User, Link],
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
extra: {
|
||||
connectionLimit: 1,
|
||||
busyTimeout: 4000,
|
||||
busyErrorRetry: 10
|
||||
}
|
||||
})
|
||||
: new DataSource({
|
||||
type: "sqlite",
|
||||
entities: [User, Link],
|
||||
database: "database.sqlite",
|
||||
synchronize: true,
|
||||
extra: {
|
||||
connectionLimit: 1,
|
||||
busyTimeout: 4000,
|
||||
busyErrorRetry: 10
|
||||
}
|
||||
});
|
||||
|
||||
registerProvider<DataSource>({
|
||||
provide: SqliteDatasource,
|
||||
type: "typeorm:datasource",
|
||||
deps: [Logger],
|
||||
async useAsyncFactory(logger: Logger) {
|
||||
await sqliteDatasource.initialize();
|
||||
|
||||
logger.info("Connected with typeorm to database: Sqlite");
|
||||
|
||||
return sqliteDatasource;
|
||||
},
|
||||
hooks: {
|
||||
$onDestroy(dataSource) {
|
||||
return dataSource.isInitialized && dataSource.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function executeWithRetry<T>(
|
||||
operation: (queryRunner: QueryRunner) => Promise<T>,
|
||||
dataSource: DataSource,
|
||||
maxRetries = 10,
|
||||
delay = 1000
|
||||
): Promise<T> {
|
||||
let retries = 0;
|
||||
while (true) {
|
||||
const queryRunner = dataSource.createQueryRunner();
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const result = await operation(queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
if (error.code === "SQLITE_BUSY" && retries < maxRetries) {
|
||||
retries++;
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
32
src/entities/User.ts
Normal file
32
src/entities/User.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { CollectionOf, MaxLength, Property, Required } from "@tsed/schema";
|
||||
import { BeforeInsert, Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { Link } from "./link/Link";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryColumn("uuid")
|
||||
@Property()
|
||||
id: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
@MaxLength(100)
|
||||
@Required()
|
||||
service: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
@MaxLength(100)
|
||||
@Required()
|
||||
serviceUsername: string;
|
||||
|
||||
@OneToMany(() => Link, (link) => link.user)
|
||||
@CollectionOf(() => Link)
|
||||
links: Link[];
|
||||
|
||||
@BeforeInsert()
|
||||
generateId() {
|
||||
if (!this.id) {
|
||||
this.id = uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
13
src/entities/link/CreateLinkDTO.ts
Normal file
13
src/entities/link/CreateLinkDTO.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Property, Required, MaxLength } from "@tsed/schema";
|
||||
|
||||
export class CreateLinkDto {
|
||||
@Property()
|
||||
@Required()
|
||||
@MaxLength(100)
|
||||
service: string;
|
||||
|
||||
@Property()
|
||||
@Required()
|
||||
@MaxLength(100)
|
||||
serviceUsername: string;
|
||||
}
|
36
src/entities/link/Link.ts
Normal file
36
src/entities/link/Link.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { MaxLength, Property, Required } from "@tsed/schema";
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn, JoinColumn, BeforeInsert } from "typeorm";
|
||||
import { User } from "../User";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
@Entity()
|
||||
export class Link {
|
||||
@PrimaryColumn("uuid")
|
||||
@Property()
|
||||
id: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
@MaxLength(100)
|
||||
@Required()
|
||||
service: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
@MaxLength(100)
|
||||
@Required()
|
||||
serviceUsername: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.links, { onDelete: "SET NULL", onUpdate: "CASCADE" })
|
||||
@JoinColumn({ name: "userId" })
|
||||
user: User;
|
||||
|
||||
@Column({ type: "uuid", nullable: true })
|
||||
@Property()
|
||||
userId: string | null;
|
||||
|
||||
@BeforeInsert()
|
||||
generateId() {
|
||||
if (!this.id) {
|
||||
this.id = uuidv4();
|
||||
}
|
||||
}
|
||||
}
|
18
src/index.ts
Normal file
18
src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { $log } from "@tsed/common";
|
||||
import { PlatformExpress } from "@tsed/platform-express";
|
||||
import { Server } from "./Server";
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
const platform = await PlatformExpress.bootstrap(Server);
|
||||
await platform.listen();
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
platform.stop();
|
||||
});
|
||||
} catch (error) {
|
||||
$log.error({ event: "SERVER_BOOTSTRAP_ERROR", message: error.message, stack: error.stack });
|
||||
}
|
||||
}
|
||||
|
||||
bootstrap();
|
Reference in New Issue
Block a user