Initial commit

This commit is contained in:
Silas 2024-09-11 16:40:12 -04:00
commit 6bdf7a32a9
38 changed files with 11219 additions and 0 deletions

5
.barrelsby.json Normal file
View File

@ -0,0 +1,5 @@
{
"directory": ["./src/controllers/rest", "./src/controllers/pages"],
"exclude": ["__mock__", "__mocks__", ".spec.ts"],
"delete": true
}

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
Dockerfile
.env.local
.env.development

9
.editorconfig Normal file
View File

@ -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

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
dist
processes.config.js
node_modules
coverage

22
.eslintrc Normal file
View File

@ -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
}
}

59
.gitignore vendored Normal file
View File

@ -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

5
.prettierignore Normal file
View File

@ -0,0 +1,5 @@
dist
docs
node_modules
*-lock.json
*.lock

10
.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 140,
"singleQuote": false,
"jsxSingleQuote": true,
"semi": true,
"tabWidth": 2,
"bracketSpacing": true,
"arrowParens": "always",
"trailingComma": "none"
}

2
.tool-versions Normal file
View File

@ -0,0 +1,2 @@
nodejs 20.17.0
sqlite 3.46.1

48
Dockerfile Normal file
View File

@ -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"]

60
README.md Normal file
View File

@ -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>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="https://cli.tsed.io/getting-started.html">Getting started</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<a href="https://api.tsed.io/rest/slack/tsedio/tsed">Slack</a>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</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
}
```

13
docker-compose.yml Normal file
View File

@ -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"

10102
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

100
package.json Normal file
View File

@ -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"
}
}

22
processes.config.js Normal file
View File

@ -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
}
]
};

View 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
View 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
View 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
View 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
};

View 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
};

View 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
};
})
};
}
}

View File

@ -0,0 +1,5 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./IndexController";

View 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);
});
});

View 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);
}
}

View 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}`);
});
});

View 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);
});
});

View 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);
}
}

View File

@ -0,0 +1,6 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./LinkController";
export * from "./UserController";

View 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);
});
});

View 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
View 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();
}
}
}

View 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
View 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
View 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();

15
tsconfig.compile.json Normal file
View File

@ -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
}
}

32
tsconfig.json Normal file
View File

@ -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": []
}
}

100
views/swagger.ejs Normal file
View File

@ -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>

21
vitest.config.mts Normal file
View File

@ -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
}
}
})
]
});