Implemented basic entities, routes, seed scripts
This commit is contained in:
@@ -21,9 +21,13 @@
|
|||||||
"radix": 0,
|
"radix": 0,
|
||||||
"no-restricted-syntax": 0,
|
"no-restricted-syntax": 0,
|
||||||
"no-await-in-loop": 0,
|
"no-await-in-loop": 0,
|
||||||
|
"no-console": 0,
|
||||||
|
"consistent-return": 0,
|
||||||
"@typescript-eslint/no-unused-vars": 0,
|
"@typescript-eslint/no-unused-vars": 0,
|
||||||
"@typescript-eslint/no-use-before-define": 0,
|
"@typescript-eslint/no-use-before-define": 0,
|
||||||
"import/prefer-default-export": 0
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
|
"import/prefer-default-export": 0,
|
||||||
|
"import/no-cycle": 0
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
// Allows us to use absolute imports within codebase
|
// Allows us to use absolute imports within codebase
|
||||||
|
|||||||
1473
package-lock.json
generated
1473
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -5,12 +5,33 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"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": {
|
"dependencies": {
|
||||||
"module-alias": "^2.2.2"
|
"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": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^2.8.0",
|
"@types/cors": "^2.8.6",
|
||||||
"@typescript-eslint/parser": "^2.8.0",
|
"@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": "^6.1.0",
|
||||||
"eslint-config-airbnb-base": "^14.0.0",
|
"eslint-config-airbnb-base": "^14.0.0",
|
||||||
"eslint-config-prettier": "^6.7.0",
|
"eslint-config-prettier": "^6.7.0",
|
||||||
@@ -23,9 +44,6 @@
|
|||||||
"ts-node": "^8.5.2",
|
"ts-node": "^8.5.2",
|
||||||
"typescript": "^3.7.2"
|
"typescript": "^3.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"start": "nodemon --exec ts-node --files src/index.ts"
|
|
||||||
},
|
|
||||||
"_moduleDirectories": [
|
"_moduleDirectories": [
|
||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
|
|||||||
20
src/constants/issues.ts
Normal file
20
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
src/constants/projects.ts
Normal file
5
src/constants/projects.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum ProjectCategory {
|
||||||
|
SOFTWARE = 'software',
|
||||||
|
MARKETING = 'marketing',
|
||||||
|
BUSINESS = 'business',
|
||||||
|
}
|
||||||
41
src/controllers/issues.ts
Normal file
41
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
src/controllers/projects.ts
Normal file
51
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;
|
||||||
22
src/controllers/users.ts
Normal file
22
src/controllers/users.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(
|
||||||
|
'/users/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;
|
||||||
16
src/database/connection.ts
Normal file
16
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
src/database/seeds/development/comment.ts
Normal file
10
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
src/database/seeds/development/index.ts
Normal file
68
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
src/database/seeds/development/issue.ts
Normal file
17
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
src/database/seeds/development/project.ts
Normal file
15
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
src/database/seeds/development/user.ts
Normal file
11
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
src/database/seeds/guestUser.ts
Normal file
106
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): Promise<Comment> =>
|
||||||
|
createEntity(Comment, {
|
||||||
|
body: "Be nice to each other! Don't be mean to each other!",
|
||||||
|
issue,
|
||||||
|
user: issue.users[0],
|
||||||
|
});
|
||||||
|
|
||||||
|
const seedGuestUserEntities = async (user: User): Promise<void> => {
|
||||||
|
const project = await seedProject(user);
|
||||||
|
const issues = await seedIssues(project);
|
||||||
|
await seedComments(issues[issues.length - 1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default seedGuestUserEntities;
|
||||||
49
src/entities/Comment.ts
Normal file
49
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;
|
||||||
82
src/entities/Issue.ts
Normal file
82
src/entities/Issue.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
@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
src/entities/Project.ts
Normal file
61
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
src/entities/User.ts
Normal file
56
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
src/entities/index.ts
Normal file
4
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
src/errors/asyncCatch.ts
Normal file
11
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
src/errors/customErrors.ts
Normal file
38
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
src/errors/errorHandler.ts
Normal file
21
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
src/errors/index.ts
Normal file
2
src/errors/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './customErrors';
|
||||||
|
export { catchErrors } from './asyncCatch';
|
||||||
52
src/index.ts
52
src/index.ts
@@ -1,3 +1,53 @@
|
|||||||
import 'module-alias/register';
|
import 'module-alias/register';
|
||||||
|
import 'dotenv/config';
|
||||||
|
import 'reflect-metadata';
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
console.log('Hey!');
|
import createDatabaseConnection from 'database/connection';
|
||||||
|
import { authenticateUser } from 'middleware/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('/', 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
src/middleware/authentication.ts
Normal file
25
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
src/types/env.d.ts
vendored
Normal file
10
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
src/types/express.d.ts
vendored
Normal file
8
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
src/utils/authToken.ts
Normal file
23
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
src/utils/typeorm.ts
Normal file
62
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';
|
||||||
|
|
||||||
|
export type EntityConstructor = typeof Project | typeof User | typeof Issue | typeof Comment;
|
||||||
|
export type EntityInstance = Project | User | Issue | Comment;
|
||||||
|
|
||||||
|
export 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
src/utils/validation.ts
Normal file
59
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;
|
||||||
@@ -28,14 +28,14 @@
|
|||||||
"strictNullChecks": true, /* Enable strict null checks. */
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
|
||||||
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
"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. */
|
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
/* Additional Checks */
|
/* Additional Checks */
|
||||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
"noImplicitReturns": false, /* Report error when not all code paths in function return a value. */
|
||||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
|
|||||||
Reference in New Issue
Block a user