Initial commit
This commit is contained in:
commit
6bdf7a32a9
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"directory": ["./src/controllers/rest", "./src/controllers/pages"],
|
||||
"exclude": ["__mock__", "__mocks__", ".spec.ts"],
|
||||
"delete": true
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
Dockerfile
|
||||
.env.local
|
||||
.env.development
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1,4 @@
|
|||
dist
|
||||
processes.config.js
|
||||
node_modules
|
||||
coverage
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"prettier",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-inferrable-types": 0,
|
||||
"@typescript-eslint/no-unused-vars": 2,
|
||||
"@typescript-eslint/no-var-requires": 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
### Node template
|
||||
.DS_Store
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||
node_modules
|
||||
.npmrc
|
||||
*.log
|
||||
|
||||
# Typings
|
||||
typings/
|
||||
|
||||
# Typescript
|
||||
src/**/*.js
|
||||
src/**/*.js.map
|
||||
test/**/*.js
|
||||
test/**/*.js.map
|
||||
|
||||
# Test
|
||||
/.tmp
|
||||
/.nyc_output
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Project
|
||||
/public
|
||||
/dist
|
||||
|
||||
#env
|
||||
.env.local
|
||||
.env.development
|
||||
|
||||
database.sqlite
|
|
@ -0,0 +1,5 @@
|
|||
dist
|
||||
docs
|
||||
node_modules
|
||||
*-lock.json
|
||||
*.lock
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"printWidth": 140,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"trailingComma": "none"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
nodejs 20.17.0
|
||||
sqlite 3.46.1
|
|
@ -0,0 +1,48 @@
|
|||
###############################################################################
|
||||
###############################################################################
|
||||
## _______ _____ ______ _____ ##
|
||||
## |__ __/ ____| ____| __ \ ##
|
||||
## | | | (___ | |__ | | | | ##
|
||||
## | | \___ \| __| | | | | ##
|
||||
## | | ____) | |____| |__| | ##
|
||||
## |_| |_____/|______|_____/ ##
|
||||
## ##
|
||||
## description : Dockerfile for TsED Application ##
|
||||
## author : TsED team ##
|
||||
## date : 2023-12-11 ##
|
||||
## version : 3.0 ##
|
||||
## ##
|
||||
###############################################################################
|
||||
###############################################################################
|
||||
|
||||
ARG NODE_VERSION=20.10.0
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine as build
|
||||
WORKDIR /opt
|
||||
|
||||
COPY package.json package-lock.json tsconfig.json tsconfig.compile.json .barrelsby.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY ./src ./src
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine as runtime
|
||||
ENV WORKDIR /opt
|
||||
WORKDIR $WORKDIR
|
||||
|
||||
RUN apk update && apk add build-base git curl
|
||||
RUN npm install -g pm2
|
||||
|
||||
COPY --from=build /opt .
|
||||
|
||||
RUN npm ci --omit=dev --ignore-scripts
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8081
|
||||
ENV PORT 8081
|
||||
ENV NODE_ENV production
|
||||
|
||||
CMD ["pm2-runtime", "start", "processes.config.js", "--env", "production"]
|
|
@ -0,0 +1,60 @@
|
|||
<p style="text-align: center" align="center">
|
||||
<a href="https://tsed.io" target="_blank"><img src="https://tsed.io/tsed-og.png" width="200" alt="Ts.ED logo"/></a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<h1>Ts.ED - intended-server</h1>
|
||||
<br />
|
||||
<div align="center">
|
||||
<a href="https://cli.tsed.io/">Website</a>
|
||||
<span> • </span>
|
||||
<a href="https://cli.tsed.io/getting-started.html">Getting started</a>
|
||||
<span> • </span>
|
||||
<a href="https://api.tsed.io/rest/slack/tsedio/tsed">Slack</a>
|
||||
<span> • </span>
|
||||
<a href="https://twitter.com/TsED_io">Twitter</a>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
> An awesome project based on Ts.ED framework
|
||||
|
||||
## Getting started
|
||||
|
||||
> **Important!** Ts.ED requires Node >= 14, Express >= 4 and TypeScript >= 4.
|
||||
|
||||
```batch
|
||||
# install dependencies
|
||||
$ npm install
|
||||
|
||||
# serve
|
||||
$ npm run start
|
||||
|
||||
# build for production
|
||||
$ npm run build
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```
|
||||
# build docker image
|
||||
docker compose build
|
||||
|
||||
# start docker image
|
||||
docker compose up
|
||||
```
|
||||
|
||||
## Barrelsby
|
||||
|
||||
This project uses [barrelsby](https://www.npmjs.com/package/barrelsby) to generate index files to import the controllers.
|
||||
|
||||
Edit `.barreslby.json` to customize it:
|
||||
|
||||
```json
|
||||
{
|
||||
"directory": ["./src/controllers/rest", "./src/controllers/pages"],
|
||||
"exclude": ["__mock__", "__mocks__", ".spec.ts"],
|
||||
"delete": true
|
||||
}
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
version: "3.5"
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile
|
||||
args:
|
||||
- http_proxy
|
||||
- https_proxy
|
||||
- no_proxy
|
||||
image: intended-server/server:latest
|
||||
ports:
|
||||
- "8081:8081"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"name": "intended-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "npm run barrels && tsc --project tsconfig.compile.json",
|
||||
"barrels": "barrelsby --config .barrelsby.json",
|
||||
"start": "rimraf ./database.sqlite && npm run barrels && tsnd --inspect --exit-child --cls --ignore-watch node_modules --respawn --transpile-only src/index.ts",
|
||||
"start:prod": "cross-env NODE_ENV=production node dist/index.js",
|
||||
"test": "npm run test:lint && npm run test:coverage ",
|
||||
"test:unit": "cross-env NODE_ENV=test vitest run",
|
||||
"test:watch": "cross-env NODE_ENV=test vitest",
|
||||
"test:coverage": "rimraf ./database.sqlite && cross-env NODE_ENV=test vitest run --coverage",
|
||||
"typeorm": "typeorm-ts-node-commonjs",
|
||||
"test:lint": "eslint '**/*.{ts,js}'",
|
||||
"test:lint:fix": "eslint '**/*.{ts,js}' --fix",
|
||||
"prettier": "prettier '**/*.{ts,js,json,md,yml,yaml}' --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsed/ajv": "^7.83.0",
|
||||
"@tsed/common": "^7.83.0",
|
||||
"@tsed/core": "^7.83.0",
|
||||
"@tsed/di": "^7.83.0",
|
||||
"@tsed/engines": "^7.83.0",
|
||||
"@tsed/exceptions": "^7.83.0",
|
||||
"@tsed/json-mapper": "^7.83.0",
|
||||
"@tsed/logger": "^6.7.5",
|
||||
"@tsed/logger-file": "^6.7.5",
|
||||
"@tsed/openspec": "^7.83.0",
|
||||
"@tsed/passport": "^7.83.0",
|
||||
"@tsed/platform-cache": "^7.83.0",
|
||||
"@tsed/platform-exceptions": "^7.83.0",
|
||||
"@tsed/platform-express": "^7.83.0",
|
||||
"@tsed/platform-log-middleware": "^7.83.0",
|
||||
"@tsed/platform-middlewares": "^7.83.0",
|
||||
"@tsed/platform-params": "^7.83.0",
|
||||
"@tsed/platform-response-filter": "^7.83.0",
|
||||
"@tsed/platform-views": "^7.83.0",
|
||||
"@tsed/schema": "^7.83.0",
|
||||
"@tsed/swagger": "^7.83.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
"barrelsby": "^2.8.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-expand": "^11.0.6",
|
||||
"dotenv-flow": "^4.1.0",
|
||||
"express": "^4.20.0",
|
||||
"express-session": "^1.18.0",
|
||||
"method-override": "^3.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.7.24",
|
||||
"@tsed/cli-plugin-eslint": "5.2.10",
|
||||
"@tsed/cli-plugin-passport": "5.2.10",
|
||||
"@tsed/cli-plugin-typeorm": "5.2.10",
|
||||
"@tsed/cli-plugin-vitest": "5.2.10",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/method-override": "^0.0.35",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"sinon": "^18.0.1",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tslib": "^2.7.0",
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.6.2",
|
||||
"unplugin-swc": "^1.5.1",
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"tsed": {
|
||||
"convention": "conv_default",
|
||||
"architecture": "arc_default",
|
||||
"packageManager": "npm",
|
||||
"runtime": "node"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const defaultLogFile = path.join(__dirname, "/logs/project-server.log");
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "api",
|
||||
script: `./dist/index.js`,
|
||||
cwd: "./",
|
||||
exec_mode: "cluster",
|
||||
instances: process.env.NODE_ENV === "test" ? 1 : process.env.NB_INSTANCES || 2,
|
||||
autorestart: true,
|
||||
max_memory_restart: process.env.MAX_MEMORY_RESTART || "750M",
|
||||
out_file: defaultLogFile,
|
||||
error_file: defaultLogFile,
|
||||
merge_logs: true,
|
||||
kill_timeout: 30000
|
||||
}
|
||||
]
|
||||
};
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from "./IndexController";
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from "./LinkController";
|
||||
export * from "./UserController";
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"noResolve": false,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": false,
|
||||
"inlineSources": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"sourceRoot": "src",
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": false,
|
||||
"suppressImplicitAnyIndexErrors": false,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importHelpers": true,
|
||||
"newLine": "LF",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"useDefineForClassFields": false,
|
||||
"lib": ["es7", "dom", "ESNext.AsyncIterable"],
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"linterOptions": {
|
||||
"exclude": []
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>client</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700" rel="stylesheet" />
|
||||
<style>
|
||||
body, h1 {
|
||||
font-family: Source Sans Pro,sans-serif;
|
||||
}
|
||||
body:after {
|
||||
content: "";
|
||||
background-image: radial-gradient(#eef2f5 0,#f4f7f8 40%,transparent 75%);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.container-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.container-logo img {
|
||||
max-width: 150px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
ul li a {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: .25rem;
|
||||
padding-bottom: .25rem;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
border: 2px solid #504747;
|
||||
min-width: 110px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
border-radius: 1rem;
|
||||
color: #504747;
|
||||
text-decoration: none;
|
||||
transition: all ease-in-out 0.5s;
|
||||
}
|
||||
ul li a:hover {
|
||||
color: #14a5c2;
|
||||
border-color: #14a5c2;
|
||||
}
|
||||
ul li a span {
|
||||
margin: .25rem;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div>
|
||||
<div class="container-logo">
|
||||
<img src="https://tsed.io/tsed-og.png" alt="Ts.ED">
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<% docs.forEach((doc) => { %>
|
||||
|
||||
<li><a href="<%= doc.path %>"><span>OpenSpec <%= doc.specVersion %></span></a></li>
|
||||
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,21 @@
|
|||
import swc from "unplugin-swc";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
root: "./"
|
||||
},
|
||||
plugins: [
|
||||
// This is required to build the test files with SWC
|
||||
swc.vite({
|
||||
// Explicitly set the module type to avoid inheriting this value from a `.swcrc` config file
|
||||
module: { type: "es6" },
|
||||
jsc: {
|
||||
transform: {
|
||||
useDefineForClassFields: false
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
Loading…
Reference in New Issue