Moved api into it's own folder
This commit is contained in:
40
api/.eslintrc.json
Normal file
40
api/.eslintrc.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 8
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:import/typescript",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"radix": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"no-await-in-loop": 0,
|
||||
"no-console": 0,
|
||||
"consistent-return": 0,
|
||||
"@typescript-eslint/no-unused-vars": 0,
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"import/prefer-default-export": 0,
|
||||
"import/no-cycle": 0
|
||||
},
|
||||
"settings": {
|
||||
// Allows us to use absolute imports within codebase
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"moduleDirectory": ["node_modules", "src/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
api/.prettierignore
Normal file
1
api/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
tsconfig.json
|
||||
5
api/.prettierrc
Normal file
5
api/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
1
api/README.md
Normal file
1
api/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Jira clone API
|
||||
4634
api/package-lock.json
generated
Normal file
4634
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
62
api/package.json
Normal file
62
api/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "jira_api",
|
||||
"author": "Ivor Reic",
|
||||
"license": "ISC",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "nodemon --exec ts-node --files src/index.ts",
|
||||
"db-seed": "nodemon --exec ts-node --files src/database/seeds/development/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"faker": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.15",
|
||||
"module-alias": "^2.2.2",
|
||||
"pg": "^7.14.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"typeorm": "^0.2.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/faker": "^4.1.7",
|
||||
"@types/jsonapi-serializer": "^3.6.2",
|
||||
"@types/jsonwebtoken": "^8.3.5",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/node": "^12.12.11",
|
||||
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
||||
"@typescript-eslint/parser": "^2.7.0",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-config-airbnb-base": "^14.0.0",
|
||||
"eslint-config-prettier": "^6.7.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"husky": "^3.1.0",
|
||||
"lint-staged": "^9.4.3",
|
||||
"nodemon": "^2.0.0",
|
||||
"prettier": "^1.19.1",
|
||||
"ts-node": "^8.5.2",
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"_moduleDirectories": [
|
||||
"src"
|
||||
],
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": [
|
||||
"eslint --fix",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
||||
20
api/src/constants/issues.ts
Normal file
20
api/src/constants/issues.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export enum IssueType {
|
||||
TASK = 'task',
|
||||
BUG = 'bug',
|
||||
STORY = 'story',
|
||||
}
|
||||
|
||||
export enum IssueStatus {
|
||||
BACKLOG = 'backlog',
|
||||
SELECTED = 'selected',
|
||||
INPROGRESS = 'inprogress',
|
||||
DONE = 'done',
|
||||
}
|
||||
|
||||
export enum IssuePriority {
|
||||
HIGHEST = '5',
|
||||
HIGH = '4',
|
||||
MEDIUM = '3',
|
||||
LOW = '2',
|
||||
LOWEST = '1',
|
||||
}
|
||||
5
api/src/constants/projects.ts
Normal file
5
api/src/constants/projects.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum ProjectCategory {
|
||||
SOFTWARE = 'software',
|
||||
MARKETING = 'marketing',
|
||||
BUSINESS = 'business',
|
||||
}
|
||||
22
api/src/controllers/authentication.ts
Normal file
22
api/src/controllers/authentication.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import express from 'express';
|
||||
|
||||
import { User } from 'entities';
|
||||
import { catchErrors } from 'errors';
|
||||
import { createEntity } from 'utils/typeorm';
|
||||
import { signToken } from 'utils/authToken';
|
||||
import seedGuestUserEntities from 'database/seeds/guestUser';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post(
|
||||
'/authentication/guest',
|
||||
catchErrors(async (req, res) => {
|
||||
const user = await createEntity(User, req.body);
|
||||
await seedGuestUserEntities(user);
|
||||
res.respond({
|
||||
authToken: signToken({ sub: user.id }),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
export default router;
|
||||
41
api/src/controllers/issues.ts
Normal file
41
api/src/controllers/issues.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import express from 'express';
|
||||
|
||||
import { Issue } from 'entities';
|
||||
import { catchErrors } from 'errors';
|
||||
import { updateEntity, deleteEntity, createEntity, findEntityOrThrow } from 'utils/typeorm';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/issues/:issueId',
|
||||
catchErrors(async (req, res) => {
|
||||
const issue = await findEntityOrThrow(Issue, req.params.issueId);
|
||||
res.respond({ issue });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/issues',
|
||||
catchErrors(async (req, res) => {
|
||||
const issue = await createEntity(Issue, req.body);
|
||||
res.respond({ issue });
|
||||
}),
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/issues/:issueId',
|
||||
catchErrors(async (req, res) => {
|
||||
const issue = await updateEntity(Issue, req.params.issueId, req.body);
|
||||
res.respond({ issue });
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/issues/:issueId',
|
||||
catchErrors(async (req, res) => {
|
||||
const issue = await deleteEntity(Issue, req.params.issueId);
|
||||
res.respond({ issue });
|
||||
}),
|
||||
);
|
||||
|
||||
export default router;
|
||||
51
api/src/controllers/projects.ts
Normal file
51
api/src/controllers/projects.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import express from 'express';
|
||||
|
||||
import { Project } from 'entities';
|
||||
import { catchErrors } from 'errors';
|
||||
import { findEntityOrThrow, updateEntity, deleteEntity, createEntity } from 'utils/typeorm';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/projects',
|
||||
catchErrors(async (_req, res) => {
|
||||
const projects = await Project.find();
|
||||
res.respond({ projects });
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/projects/:projectId',
|
||||
catchErrors(async (req, res) => {
|
||||
const project = await findEntityOrThrow(Project, req.params.projectId, {
|
||||
relations: ['users', 'issues', 'issues.comments'],
|
||||
});
|
||||
res.respond({ project });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/projects',
|
||||
catchErrors(async (req, res) => {
|
||||
const project = await createEntity(Project, req.body);
|
||||
res.respond({ project });
|
||||
}),
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/projects/:projectId',
|
||||
catchErrors(async (req, res) => {
|
||||
const project = await updateEntity(Project, req.params.projectId, req.body);
|
||||
res.respond({ project });
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/projects/:projectId',
|
||||
catchErrors(async (req, res) => {
|
||||
const project = await deleteEntity(Project, req.params.projectId);
|
||||
res.respond({ project });
|
||||
}),
|
||||
);
|
||||
|
||||
export default router;
|
||||
16
api/src/database/connection.ts
Normal file
16
api/src/database/connection.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createConnection, Connection } from 'typeorm';
|
||||
|
||||
import * as entities from 'entities';
|
||||
|
||||
const createDatabaseConnection = (): Promise<Connection> =>
|
||||
createConnection({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
entities: Object.values(entities),
|
||||
});
|
||||
|
||||
export default createDatabaseConnection;
|
||||
10
api/src/database/seeds/development/comment.ts
Normal file
10
api/src/database/seeds/development/comment.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import faker from 'faker';
|
||||
|
||||
import Comment from 'entities/Comment';
|
||||
|
||||
const generateComment = (data: Partial<Comment> = {}): Partial<Comment> => ({
|
||||
body: faker.lorem.paragraph(),
|
||||
...data,
|
||||
});
|
||||
|
||||
export default generateComment;
|
||||
68
api/src/database/seeds/development/index.ts
Normal file
68
api/src/database/seeds/development/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'module-alias/register';
|
||||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
import { times, sample } from 'lodash';
|
||||
|
||||
import { Comment, Issue, Project, User } from 'entities';
|
||||
import createDatabaseConnection from 'database/connection';
|
||||
import { createEntity } from 'utils/typeorm';
|
||||
import generateUser from './user';
|
||||
import generateProject from './project';
|
||||
import generateIssue from './issue';
|
||||
import generateComment from './comment';
|
||||
|
||||
const seedUsers = (): Promise<User[]> => {
|
||||
const users = times(4, () => createEntity(User, generateUser()));
|
||||
return Promise.all(users);
|
||||
};
|
||||
|
||||
const seedProjects = (users: User[]): Promise<Project[]> => {
|
||||
const projects = times(2, () => createEntity(Project, generateProject({ users })));
|
||||
return Promise.all(projects);
|
||||
};
|
||||
|
||||
const seedIssues = (projects: Project[]): Promise<Issue[]> => {
|
||||
const issues = projects
|
||||
.map(project =>
|
||||
times(10, () =>
|
||||
createEntity(
|
||||
Issue,
|
||||
generateIssue({
|
||||
reporterId: (sample(project.users) as User).id,
|
||||
project,
|
||||
users: [sample(project.users) as User],
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.flat();
|
||||
return Promise.all(issues);
|
||||
};
|
||||
|
||||
const seedComments = (issues: Issue[]): Promise<Comment[]> => {
|
||||
const comments = issues.map(issue =>
|
||||
createEntity(Comment, generateComment({ issue, user: sample(issue.project.users) })),
|
||||
);
|
||||
return Promise.all(comments);
|
||||
};
|
||||
|
||||
const seedEntities = async (): Promise<void> => {
|
||||
const users = await seedUsers();
|
||||
const projects = await seedProjects(users);
|
||||
const issues = await seedIssues(projects);
|
||||
await seedComments(issues);
|
||||
};
|
||||
|
||||
const initializeSeed = async (): Promise<void> => {
|
||||
try {
|
||||
const Connection = await createDatabaseConnection();
|
||||
await Connection.dropDatabase();
|
||||
await Connection.synchronize();
|
||||
await seedEntities();
|
||||
console.log('Seeding completed!');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeSeed();
|
||||
17
api/src/database/seeds/development/issue.ts
Normal file
17
api/src/database/seeds/development/issue.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import faker from 'faker';
|
||||
import { sample, random } from 'lodash';
|
||||
|
||||
import Issue from 'entities/Issue';
|
||||
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
|
||||
|
||||
const generateIssue = (data: Partial<Issue> = {}): Partial<Issue> => ({
|
||||
title: faker.company.catchPhrase(),
|
||||
type: sample(Object.values(IssueType)),
|
||||
status: sample(Object.values(IssueStatus)),
|
||||
priority: sample(Object.values(IssuePriority)),
|
||||
description: faker.lorem.paragraph(),
|
||||
estimate: random(1, 40),
|
||||
...data,
|
||||
});
|
||||
|
||||
export default generateIssue;
|
||||
15
api/src/database/seeds/development/project.ts
Normal file
15
api/src/database/seeds/development/project.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import faker from 'faker';
|
||||
import { sample } from 'lodash';
|
||||
|
||||
import Project from 'entities/Project';
|
||||
import { ProjectCategory } from 'constants/projects';
|
||||
|
||||
const generateProject = (data: Partial<Project> = {}): Partial<Project> => ({
|
||||
name: faker.company.companyName(),
|
||||
url: faker.internet.url(),
|
||||
description: faker.lorem.paragraph(),
|
||||
category: sample(Object.values(ProjectCategory)),
|
||||
...data,
|
||||
});
|
||||
|
||||
export default generateProject;
|
||||
11
api/src/database/seeds/development/user.ts
Normal file
11
api/src/database/seeds/development/user.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import faker from 'faker';
|
||||
|
||||
import User from 'entities/User';
|
||||
|
||||
const generateUser = (data: Partial<User> = {}): Partial<User> => ({
|
||||
name: faker.company.companyName(),
|
||||
email: faker.internet.email(),
|
||||
...data,
|
||||
});
|
||||
|
||||
export default generateUser;
|
||||
106
api/src/database/seeds/guestUser.ts
Normal file
106
api/src/database/seeds/guestUser.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Comment, Issue, Project, User } from 'entities';
|
||||
import { ProjectCategory } from 'constants/projects';
|
||||
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
|
||||
import { createEntity } from 'utils/typeorm';
|
||||
|
||||
const seedProject = (user: User): Promise<Project> =>
|
||||
createEntity(Project, {
|
||||
name: 'Project: Hello World',
|
||||
url: 'https://www.atlassian.com/software/jira',
|
||||
description:
|
||||
'Plan, track, and manage your agile and software development projects in Jira. Customize your workflow, collaborate, and release great software.',
|
||||
category: ProjectCategory.SOFTWARE,
|
||||
users: [user],
|
||||
});
|
||||
|
||||
const seedIssues = (project: Project): Promise<Issue[]> => {
|
||||
const user = project.users[0];
|
||||
const issues = [
|
||||
createEntity(Issue, {
|
||||
title: 'This is an issue of type: Task.',
|
||||
type: IssueType.TASK,
|
||||
status: IssueStatus.BACKLOG,
|
||||
priority: IssuePriority.LOWEST,
|
||||
estimate: 8,
|
||||
reporterId: user.id,
|
||||
project,
|
||||
users: [user],
|
||||
}),
|
||||
createEntity(Issue, {
|
||||
title: "Click on an issue to see what's behind it.",
|
||||
type: IssueType.TASK,
|
||||
status: IssueStatus.BACKLOG,
|
||||
priority: IssuePriority.LOW,
|
||||
description: 'Nothing in particular.',
|
||||
estimate: 40,
|
||||
reporterId: user.id,
|
||||
project,
|
||||
}),
|
||||
createEntity(Issue, {
|
||||
title: 'Try dragging and sorting issues.',
|
||||
type: IssueType.BUG,
|
||||
status: IssueStatus.BACKLOG,
|
||||
priority: IssuePriority.MEDIUM,
|
||||
estimate: 15,
|
||||
reporterId: user.id,
|
||||
project,
|
||||
users: [user],
|
||||
}),
|
||||
createEntity(Issue, {
|
||||
title: 'You can use markdown for issue descriptions.',
|
||||
type: IssueType.STORY,
|
||||
status: IssueStatus.BACKLOG,
|
||||
priority: IssuePriority.HIGH,
|
||||
description:
|
||||
"#### Colons can be used to align columns.\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | |\n| col 2 is | centered | |\n| zebra stripes | are neat | |\n\nThe outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.\n\nMarkdown | Less | Pretty\n--- | --- | ---\n*Still* | `renders` | **nicely**\n1 | 2 | 3",
|
||||
estimate: 4,
|
||||
reporterId: user.id,
|
||||
project,
|
||||
users: [user],
|
||||
}),
|
||||
createEntity(Issue, {
|
||||
title: 'You must assign priority from lowest to highest to all issues.',
|
||||
type: IssueType.TASK,
|
||||
status: IssueStatus.SELECTED,
|
||||
priority: IssuePriority.HIGHEST,
|
||||
estimate: 15,
|
||||
reporterId: user.id,
|
||||
project,
|
||||
}),
|
||||
createEntity(Issue, {
|
||||
title: 'You can assign labels to issues.',
|
||||
type: IssueType.STORY,
|
||||
status: IssueStatus.SELECTED,
|
||||
priority: IssuePriority.MEDIUM,
|
||||
estimate: 55,
|
||||
reporterId: user.id,
|
||||
project,
|
||||
users: [user],
|
||||
}),
|
||||
createEntity(Issue, {
|
||||
title: 'Try leaving a comment on this issue.',
|
||||
type: IssueType.TASK,
|
||||
status: IssueStatus.SELECTED,
|
||||
priority: IssuePriority.MEDIUM,
|
||||
estimate: 12,
|
||||
reporterId: user.id,
|
||||
project,
|
||||
}),
|
||||
];
|
||||
return Promise.all(issues);
|
||||
};
|
||||
|
||||
const seedComments = (issue: Issue, user: User): Promise<Comment> =>
|
||||
createEntity(Comment, {
|
||||
body: "Be nice to each other! Don't be mean to each other!",
|
||||
issue,
|
||||
user,
|
||||
});
|
||||
|
||||
const seedGuestUserEntities = async (user: User): Promise<void> => {
|
||||
const project = await seedProject(user);
|
||||
const issues = await seedIssues(project);
|
||||
await seedComments(issues[issues.length - 1], project.users[0]);
|
||||
};
|
||||
|
||||
export default seedGuestUserEntities;
|
||||
49
api/src/entities/Comment.ts
Normal file
49
api/src/entities/Comment.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
RelationId,
|
||||
} from 'typeorm';
|
||||
|
||||
import is from 'utils/validation';
|
||||
import { Issue, User } from '.';
|
||||
|
||||
@Entity()
|
||||
class Comment extends BaseEntity {
|
||||
static validations = {
|
||||
body: [is.required(), is.maxLength(50000)],
|
||||
};
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column('text')
|
||||
body: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp' })
|
||||
updatedAt: Date;
|
||||
|
||||
@ManyToOne(
|
||||
() => User,
|
||||
user => user.comments,
|
||||
)
|
||||
user: User;
|
||||
|
||||
@RelationId((comment: Comment) => comment.user)
|
||||
userId: number;
|
||||
|
||||
@ManyToOne(
|
||||
() => Issue,
|
||||
issue => issue.comments,
|
||||
)
|
||||
issue: Issue;
|
||||
}
|
||||
|
||||
export default Comment;
|
||||
88
api/src/entities/Issue.ts
Normal file
88
api/src/entities/Issue.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
RelationId,
|
||||
} from 'typeorm';
|
||||
|
||||
import is from 'utils/validation';
|
||||
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
|
||||
import { Comment, Project, User } from '.';
|
||||
|
||||
@Entity()
|
||||
class Issue extends BaseEntity {
|
||||
static validations = {
|
||||
title: [is.required(), is.maxLength(200)],
|
||||
type: [is.required(), is.oneOf(Object.values(IssueType))],
|
||||
status: [is.required(), is.oneOf(Object.values(IssueStatus))],
|
||||
priority: [is.required(), is.oneOf(Object.values(IssuePriority))],
|
||||
description: is.maxLength(100000),
|
||||
};
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column('varchar')
|
||||
title: string;
|
||||
|
||||
@Column('varchar')
|
||||
type: IssueType;
|
||||
|
||||
@Column('varchar')
|
||||
status: IssueStatus;
|
||||
|
||||
@Column('varchar')
|
||||
priority: IssuePriority;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
estimate: number | null;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
timeSpent: number | null;
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
timeRemaining: number | null;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column('integer')
|
||||
reporterId: number;
|
||||
|
||||
@ManyToOne(
|
||||
() => Project,
|
||||
project => project.issues,
|
||||
)
|
||||
project: Project;
|
||||
|
||||
@OneToMany(
|
||||
() => Comment,
|
||||
comment => comment.issue,
|
||||
)
|
||||
comments: Comment[];
|
||||
|
||||
@ManyToMany(
|
||||
() => User,
|
||||
user => user.issues,
|
||||
)
|
||||
@JoinTable()
|
||||
users: User[];
|
||||
|
||||
@RelationId((issue: Issue) => issue.users)
|
||||
userIds: number[];
|
||||
}
|
||||
|
||||
export default Issue;
|
||||
61
api/src/entities/Project.ts
Normal file
61
api/src/entities/Project.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
|
||||
import is from 'utils/validation';
|
||||
import { ProjectCategory } from 'constants/projects';
|
||||
import { Issue, User } from '.';
|
||||
|
||||
@Entity()
|
||||
class Project extends BaseEntity {
|
||||
static validations = {
|
||||
name: [is.required(), is.maxLength(100)],
|
||||
url: is.url(),
|
||||
description: is.maxLength(10000),
|
||||
category: [is.required(), is.oneOf(Object.values(ProjectCategory))],
|
||||
};
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column('varchar')
|
||||
name: string;
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
url: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Column('varchar')
|
||||
category: ProjectCategory;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp' })
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(
|
||||
() => Issue,
|
||||
issue => issue.project,
|
||||
)
|
||||
issues: Issue[];
|
||||
|
||||
@ManyToMany(
|
||||
() => User,
|
||||
user => user.projects,
|
||||
)
|
||||
@JoinTable()
|
||||
users: User[];
|
||||
}
|
||||
|
||||
export default Project;
|
||||
56
api/src/entities/User.ts
Normal file
56
api/src/entities/User.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
ManyToMany,
|
||||
} from 'typeorm';
|
||||
|
||||
import is from 'utils/validation';
|
||||
import { Comment, Issue, Project } from '.';
|
||||
|
||||
@Entity()
|
||||
class User extends BaseEntity {
|
||||
static validations = {
|
||||
name: [is.required(), is.maxLength(100)],
|
||||
email: [is.required(), is.email(), is.maxLength(200)],
|
||||
};
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column('varchar')
|
||||
name: string;
|
||||
|
||||
@Column('varchar')
|
||||
email: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamp' })
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(
|
||||
() => Comment,
|
||||
comment => comment.user,
|
||||
)
|
||||
comments: Comment[];
|
||||
|
||||
@ManyToMany(
|
||||
() => Issue,
|
||||
issue => issue.users,
|
||||
)
|
||||
issues: Issue[];
|
||||
|
||||
@ManyToMany(
|
||||
() => Project,
|
||||
project => project.users,
|
||||
)
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
export default User;
|
||||
4
api/src/entities/index.ts
Normal file
4
api/src/entities/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Comment } from './Comment';
|
||||
export { default as Issue } from './Issue';
|
||||
export { default as Project } from './Project';
|
||||
export { default as User } from './User';
|
||||
11
api/src/errors/asyncCatch.ts
Normal file
11
api/src/errors/asyncCatch.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
export const catchErrors = (requestHandler: RequestHandler): RequestHandler => {
|
||||
return async (req, res, next): Promise<any> => {
|
||||
try {
|
||||
return await requestHandler(req, res, next);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
38
api/src/errors/customErrors.ts
Normal file
38
api/src/errors/customErrors.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
type ErrorData = { [key: string]: any };
|
||||
|
||||
export class CustomError extends Error {
|
||||
constructor(
|
||||
public message: string,
|
||||
public code: string | number = 'INTERNAL_ERROR',
|
||||
public status: number = 500,
|
||||
public data: ErrorData = {},
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteNotFoundError extends CustomError {
|
||||
constructor(originalUrl: string) {
|
||||
super(`Route '${originalUrl}' does not exist.`, 'ROUTE_NOT_FOUND', 404);
|
||||
}
|
||||
}
|
||||
|
||||
export class EntityNotFoundError extends CustomError {
|
||||
constructor(entityName: string) {
|
||||
super(`${entityName} not found.`, 'ENTITY_NOT_FOUND', 404);
|
||||
}
|
||||
}
|
||||
|
||||
export class BadUserInputError extends CustomError {
|
||||
constructor(errorData: ErrorData) {
|
||||
super('There were validation errors.', 'BAD_USER_INPUT', 400, errorData);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidTokenError extends CustomError {
|
||||
constructor(message = 'Authentication token is invalid.') {
|
||||
super(message, 'INVALID_TOKEN', 401);
|
||||
}
|
||||
}
|
||||
21
api/src/errors/errorHandler.ts
Normal file
21
api/src/errors/errorHandler.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ErrorRequestHandler } from 'express';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { CustomError } from 'errors';
|
||||
|
||||
export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
||||
console.error(error);
|
||||
|
||||
const isErrorSafeForClient = error instanceof CustomError;
|
||||
|
||||
const errorData = isErrorSafeForClient
|
||||
? pick(error, ['message', 'code', 'status', 'data'])
|
||||
: {
|
||||
message: 'Something went wrong, please contact our support.',
|
||||
code: 'INTERNAL_ERROR',
|
||||
status: 500,
|
||||
data: {},
|
||||
};
|
||||
|
||||
res.status(errorData.status).send({ errors: [errorData] });
|
||||
};
|
||||
2
api/src/errors/index.ts
Normal file
2
api/src/errors/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './customErrors';
|
||||
export { catchErrors } from './asyncCatch';
|
||||
56
api/src/index.ts
Normal file
56
api/src/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'module-alias/register';
|
||||
import 'dotenv/config';
|
||||
import 'reflect-metadata';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
|
||||
import createDatabaseConnection from 'database/connection';
|
||||
import { authenticateUser } from 'middleware/authentication';
|
||||
import authenticationRoutes from 'controllers/authentication';
|
||||
import projectsRoutes from 'controllers/projects';
|
||||
import issuesRoutes from 'controllers/issues';
|
||||
import { RouteNotFoundError } from 'errors';
|
||||
import { errorHandler } from 'errors/errorHandler';
|
||||
|
||||
const establishDatabaseConnection = async (): Promise<void> => {
|
||||
try {
|
||||
await createDatabaseConnection();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeExpress = (): void => {
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded());
|
||||
|
||||
app.use((_req, res, next) => {
|
||||
res.respond = (data): void => {
|
||||
res.status(200).send({ data });
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/', authenticationRoutes);
|
||||
|
||||
app.use('/', authenticateUser);
|
||||
|
||||
app.use('/', projectsRoutes);
|
||||
app.use('/', issuesRoutes);
|
||||
|
||||
app.use((req, _res, next) => next(new RouteNotFoundError(req.originalUrl)));
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(PORT, () => console.log(`App listening on port ${PORT}`));
|
||||
};
|
||||
|
||||
const initializeApp = async (): Promise<void> => {
|
||||
await establishDatabaseConnection();
|
||||
initializeExpress();
|
||||
};
|
||||
|
||||
initializeApp();
|
||||
25
api/src/middleware/authentication.ts
Normal file
25
api/src/middleware/authentication.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
import { verifyToken } from 'utils/authToken';
|
||||
import { findEntityOrThrow } from 'utils/typeorm';
|
||||
import { catchErrors, InvalidTokenError } from 'errors';
|
||||
import { User } from 'entities';
|
||||
|
||||
const getAuthTokenFromRequest = (req: Request): string | null => {
|
||||
const header = req.get('Authorization') || '';
|
||||
const [bearer, token] = header.split(' ');
|
||||
return bearer === 'Bearer' && token ? token : null;
|
||||
};
|
||||
|
||||
export const authenticateUser = catchErrors(async (req, _res, next) => {
|
||||
const token = getAuthTokenFromRequest(req);
|
||||
if (!token) {
|
||||
throw new InvalidTokenError('Authentication token not found.');
|
||||
}
|
||||
const userId = verifyToken(token).sub;
|
||||
if (!userId) {
|
||||
throw new InvalidTokenError('Authentication token is invalid.');
|
||||
}
|
||||
req.currentUser = await findEntityOrThrow(User, userId);
|
||||
next();
|
||||
});
|
||||
10
api/src/types/env.d.ts
vendored
Normal file
10
api/src/types/env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
DB_HOST: string;
|
||||
DB_PORT: string;
|
||||
DB_USERNAME: string;
|
||||
DB_PASSWORD: string;
|
||||
DB_DATABASE: string;
|
||||
JWT_SECRET: string;
|
||||
}
|
||||
}
|
||||
8
api/src/types/express.d.ts
vendored
Normal file
8
api/src/types/express.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare namespace Express {
|
||||
export interface Response {
|
||||
respond: (data: any) => void;
|
||||
}
|
||||
export interface Request {
|
||||
currentUser: import('entities').User;
|
||||
}
|
||||
}
|
||||
23
api/src/utils/authToken.ts
Normal file
23
api/src/utils/authToken.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import jwt, { SignOptions } from 'jsonwebtoken';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
import { InvalidTokenError } from 'errors';
|
||||
|
||||
export const signToken = (payload: object, options?: SignOptions): string =>
|
||||
jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
expiresIn: '7 days',
|
||||
...options,
|
||||
});
|
||||
|
||||
export const verifyToken = (token: string): { [key: string]: any } => {
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
if (isPlainObject(payload)) {
|
||||
return payload as { [key: string]: any };
|
||||
}
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
throw new InvalidTokenError();
|
||||
}
|
||||
};
|
||||
62
api/src/utils/typeorm.ts
Normal file
62
api/src/utils/typeorm.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { FindOneOptions } from 'typeorm/find-options/FindOneOptions';
|
||||
|
||||
import { Project, User, Issue, Comment } from 'entities';
|
||||
import { EntityNotFoundError, BadUserInputError } from 'errors';
|
||||
import { generateErrors } from 'utils/validation';
|
||||
|
||||
type EntityConstructor = typeof Project | typeof User | typeof Issue | typeof Comment;
|
||||
type EntityInstance = Project | User | Issue | Comment;
|
||||
|
||||
const entities: { [key: string]: EntityConstructor } = { Comment, Issue, Project, User };
|
||||
|
||||
export const findEntityOrThrow = async <T extends EntityConstructor>(
|
||||
Constructor: T,
|
||||
id: number | string,
|
||||
options?: FindOneOptions,
|
||||
): Promise<InstanceType<T>> => {
|
||||
const instance = await Constructor.findOne(id, options);
|
||||
if (!instance) {
|
||||
throw new EntityNotFoundError(Constructor.name);
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
|
||||
export const validateAndSaveEntity = async <T extends EntityInstance>(instance: T): Promise<T> => {
|
||||
const Constructor = entities[instance.constructor.name];
|
||||
|
||||
if ('validations' in Constructor) {
|
||||
const errorFields = generateErrors(instance, Constructor.validations);
|
||||
|
||||
if (Object.keys(errorFields).length > 0) {
|
||||
throw new BadUserInputError({ fields: errorFields });
|
||||
}
|
||||
}
|
||||
return instance.save() as Promise<T>;
|
||||
};
|
||||
|
||||
export const createEntity = async <T extends EntityConstructor>(
|
||||
Constructor: T,
|
||||
input: Partial<InstanceType<T>>,
|
||||
): Promise<InstanceType<T>> => {
|
||||
const instance = Constructor.create(input);
|
||||
return validateAndSaveEntity(instance as InstanceType<T>);
|
||||
};
|
||||
|
||||
export const updateEntity = async <T extends EntityConstructor>(
|
||||
Constructor: T,
|
||||
id: number | string,
|
||||
input: Partial<InstanceType<T>>,
|
||||
): Promise<InstanceType<T>> => {
|
||||
const instance = await findEntityOrThrow(Constructor, id);
|
||||
Object.assign(instance, input);
|
||||
return validateAndSaveEntity(instance);
|
||||
};
|
||||
|
||||
export const deleteEntity = async <T extends EntityConstructor>(
|
||||
Constructor: T,
|
||||
id: number | string,
|
||||
): Promise<InstanceType<T>> => {
|
||||
const instance = await findEntityOrThrow(Constructor, id);
|
||||
await instance.remove();
|
||||
return instance;
|
||||
};
|
||||
59
api/src/utils/validation.ts
Normal file
59
api/src/utils/validation.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
type Value = any;
|
||||
type ErrorMessage = false | string;
|
||||
type FieldValues = { [key: string]: Value };
|
||||
type Validator = (value: Value, fieldValues?: FieldValues) => ErrorMessage;
|
||||
type FieldValidators = { [key: string]: Validator | Validator[] };
|
||||
type FieldErrors = { [key: string]: string };
|
||||
|
||||
const is = {
|
||||
match: (testFn: Function, message = '') => (
|
||||
value: Value,
|
||||
fieldValues: FieldValues,
|
||||
): ErrorMessage => !testFn(value, fieldValues) && message,
|
||||
|
||||
required: () => (value: Value): ErrorMessage =>
|
||||
isNilOrEmptyString(value) && 'This field is required',
|
||||
|
||||
minLength: (min: number) => (value: Value): ErrorMessage =>
|
||||
!!value && value.length < min && `Must be at least ${min} characters`,
|
||||
|
||||
maxLength: (max: number) => (value: Value): ErrorMessage =>
|
||||
!!value && value.length > max && `Must be at most ${max} characters`,
|
||||
|
||||
oneOf: (arr: any[]) => (value: Value): ErrorMessage =>
|
||||
!!value && !arr.includes(value) && `Must be one of: ${arr.join(', ')}`,
|
||||
|
||||
notEmptyArray: () => (value: Value): ErrorMessage =>
|
||||
Array.isArray(value) && value.length === 0 && 'Please add at least one item',
|
||||
|
||||
email: () => (value: Value): ErrorMessage =>
|
||||
!!value && !/.+@.+\..+/.test(value) && 'Must be a valid email',
|
||||
|
||||
url: () => (value: Value): ErrorMessage =>
|
||||
!!value &&
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
!/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/.test(value) &&
|
||||
'Must be a valid URL',
|
||||
};
|
||||
|
||||
const isNilOrEmptyString = (value: Value): boolean =>
|
||||
value === undefined || value === null || value === '';
|
||||
|
||||
export const generateErrors = (
|
||||
fieldValues: FieldValues,
|
||||
fieldValidators: FieldValidators,
|
||||
): FieldErrors => {
|
||||
const fieldErrors: FieldErrors = {};
|
||||
|
||||
Object.entries(fieldValidators).forEach(([fieldName, validators]) => {
|
||||
[validators].flat().forEach(validator => {
|
||||
const errorMessage = validator(fieldValues[fieldName], fieldValues);
|
||||
if (errorMessage !== false && !fieldErrors[fieldName]) {
|
||||
fieldErrors[fieldName] = errorMessage;
|
||||
}
|
||||
});
|
||||
});
|
||||
return fieldErrors;
|
||||
};
|
||||
|
||||
export default is;
|
||||
68
api/tsconfig.json
Normal file
68
api/tsconfig.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"lib": ["dom", "es6", "es2017", "es2019", "esnext.asynciterable"], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./build", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
"removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"strictNullChecks": true, /* Enable strict null checks. */
|
||||
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
|
||||
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": false, /* Report error when not all code paths in function return a value. */
|
||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": "./src",
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["./src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user