Skip to content
This repository has been archived by the owner on Sep 21, 2023. It is now read-only.

Commit

Permalink
fixed linting, acceptance tests, added user types
Browse files Browse the repository at this point in the history
  • Loading branch information
Frederic Charette authored and Frederic Charette committed Sep 23, 2021
1 parent 143e5d6 commit c82ed36
Show file tree
Hide file tree
Showing 17 changed files with 129 additions and 124 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"error",
{
"devDependencies": [
"**/tests/**/*.ts",
"**/test/**/*.ts"
]
}
Expand Down Expand Up @@ -152,7 +153,7 @@
]
},
"env": {
"browser": true,
"node": true,
"jest": true
}
}
8 changes: 4 additions & 4 deletions middleware/context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { randomUUID } from 'crypto';

export default function context(req, res, next) {
const requestId = req.headers['x-request-id'] || randomUUID();
req.id = requestId;
res.setHeader('x-request-id', requestId);
const requestId = req.headers['x-request-id'] || randomUUID();
req.id = requestId;
res.setHeader('x-request-id', requestId);

next();
next();
}
14 changes: 7 additions & 7 deletions middleware/security.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export default function security(req, res, next) {
res.removeHeader('X-Powered-By');
if (decodeURIComponent(req.url).includes('<script>')) {
return res.status(406).end('Illegal component in URI');
}
next();
res.removeHeader('X-Powered-By');

if (decodeURIComponent(req.url).includes('<script>')) {
return res.status(406).end('Illegal component in URI');
}

next();
}
4 changes: 2 additions & 2 deletions packages/domains/user/data/db-user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { query } from '@nc/utils/db';

export function readUser(userId) {
return query(`SELECT * FROM users WHERE id = $1`, [userId])
.then(response => response.rows?.[0]);
return query('SELECT * FROM users WHERE id = $1', [userId])
.then((response) => response.rows?.[0]);
}
26 changes: 14 additions & 12 deletions packages/domains/user/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { User } from './types';

const publicFields = ['first_name', 'last_name', 'company_name'];

export function capitalize(word) {
const str = `${word}`;
return str[0].toUpperCase() + str.slice(1);
const str = `${word}`;
return str[0].toUpperCase() + str.slice(1);
}

export function secureTrim(user) {
return JSON.stringify(user, publicFields);
export function secureTrim(user: User): string {
return JSON.stringify(user, publicFields);
}

export function format(rawUser) {
return {
id: rawUser.id,
first_name: capitalize(rawUser.first_name),
last_name: capitalize(rawUser.last_name),
company_name: rawUser.company_name,
ssn: rawUser.ssn,
};
export function format(rawUser): User {
return {
id: rawUser.id,
first_name: capitalize(rawUser.first_name),
last_name: capitalize(rawUser.last_name),
company_name: rawUser.company_name,
ssn: rawUser.ssn,
};
}
29 changes: 15 additions & 14 deletions packages/domains/user/model.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { BadRequest, InternalError, NotFound } from '@nc/utils/errors';
import { format } from './formatter';
import { readUser } from './data/db-user';
import { to } from '@nc/utils/async';
import { format } from './formatter';
import { User } from './types';
import { BadRequest, InternalError, NotFound } from '@nc/utils/errors';

export async function getUserDetails(userId) {
if (!userId) {
throw BadRequest('userId property is missing.');
}
export async function getUserDetails(userId): Promise<User> {
if (!userId) {
throw BadRequest('userId property is missing.');
}

const [dbError, rawUser] = await to(readUser(userId));
const [dbError, rawUser] = await to(readUser(userId));

if (dbError) {
throw InternalError(`Error fetching data from the DB: ${dbError.message}`);
}
if (dbError) {
throw InternalError(`Error fetching data from the DB: ${dbError.message}`);
}

if (!rawUser) {
throw NotFound(`Could not find user with id ${userId}`);
}
if (!rawUser) {
throw NotFound(`Could not find user with id ${userId}`);
}

return format(rawUser);
return format(rawUser);
}
4 changes: 2 additions & 2 deletions packages/domains/user/routes/v1-get-user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from 'express';
import { ApiError } from '@nc/utils/errors';
import { getUserDetails } from '../model';
import { Router } from 'express';
import { secureTrim } from '../formatter';
import { to } from '@nc/utils/async';

Expand All @@ -13,7 +13,7 @@ router.get('/get-user-details', async (req, res, next) => {
return next(new ApiError(userError, userError.status, `Could not get user details: ${userError}`, userError.title, req));
}

if(!userDetails) {
if (!userDetails) {
return res.json({});
}

Expand Down
42 changes: 21 additions & 21 deletions packages/domains/user/tests/formatter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { capitalize, secureTrim, format } from '../formatter';
import { capitalize, format, secureTrim } from '../formatter';

describe('[Packages | User-domain | Formatter] capitalize', () => {
test('capitalize should make the first character as a capital letter', () => {
Expand All @@ -19,32 +19,32 @@ describe('[Packages | User-domain | Formatter] capitalize', () => {
});

describe('[Packages | User-domain | Formatter] secureTrim', () => {
test('secureTrim should remove fields that are not defined in the list of public fields', () => {
return expect(secureTrim({
first_name: 'John',
last_name: 'Smith',
company_name: 'Pleo',
ssn: 1,
})).toEqual(JSON.stringify({
first_name: 'John',
last_name: 'Smith',
company_name: 'Pleo',
}));
});
test('secureTrim should remove fields that are not defined in the list of public fields', () => {
return expect(secureTrim({
first_name: 'John',
last_name: 'Smith',
company_name: 'Pleo',
ssn: 1,
})).toEqual(JSON.stringify({
first_name: 'John',
last_name: 'Smith',
company_name: 'Pleo',
}));
});
});

describe('[Packages | User-domain | Formatter] format', () => {
test('format should return an instance of users that fits the API model, based on the db raw value', () => {
return expect(format({
first_name: 'john',
last_name: 'smith',
company_name: 'Pleo',
ssn: 1,
first_name: 'john',
last_name: 'smith',
company_name: 'Pleo',
ssn: 1,
})).toEqual({
first_name: 'John',
last_name: 'Smith',
company_name: 'Pleo',
ssn: 1,
first_name: 'John',
last_name: 'Smith',
company_name: 'Pleo',
ssn: 1,
});
});
});
2 changes: 1 addition & 1 deletion packages/utils/db.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import config from 'config';
import { Client } from 'pg';
import config from 'config';

let db;

Expand Down
43 changes: 30 additions & 13 deletions packages/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ import Logger from './logging';

const logger = Logger('errors');

export interface ApiErrorType {
code: string
details?: any
message: string
source: {
error: any
request: {
headers: string[]
id: string
url: string
}
}
stack: any
status: number
title: string
}

const isPrintableEnv = (): boolean => {
if (!process.env.NODE_ENV) return true;
return process.env.TEST_MODE !== 'test';
Expand All @@ -16,9 +33,9 @@ const trimSecure = (headers) => {
return headers;
};

export function ApiError(error: ApiError | Error | undefined, statusCode?: string | number, message?: string, title?: string, ctx?: any): void {
export function ApiError(error: ApiErrorType | Error | undefined, statusCode?: string | number, message?: string, title?: string, ctx?: any): void {
// @ts-ignore
if ((error as ApiError)?.source && !ctx) return error as ApiError;
if ((error as ApiErrorType)?.source && !ctx) return error as ApiErrorType;

const printStack = config.debug.stackSize > 0;
if (printStack && error?.stack) this.stack = error.stack;
Expand All @@ -29,40 +46,40 @@ export function ApiError(error: ApiError | Error | undefined, statusCode?: strin
url: ctx?.url,
headers: ctx?.headers && trimSecure(ctx.headers),
},
error: printStack ? (error?.stack || (error as ApiError)?.source?.error) : undefined,
error: printStack ? (error?.stack || (error as ApiErrorType)?.source?.error) : undefined,
};

this.status = Number(statusCode || (error as ApiError).status) || 500;
this.status = Number(statusCode || (error as ApiErrorType).status) || 500;
this.message = message || error.message || error.toString();
this.title = title || (error as ApiError)?.title || (error as Error)?.name;
this.code = (error as ApiError)?.code || 'API-000000';
this.details = (error as ApiError)?.details || '';
this.title = title || (error as ApiErrorType)?.title || (error as Error)?.name;
this.code = (error as ApiErrorType)?.code || 'API-000000';
this.details = (error as ApiErrorType)?.details || '';

if (isPrintableEnv()) logger.error(JSON.stringify(this), ctx);
}

ApiError.prototype = Error.prototype;

export function BadRequest(message: string, context?: any, parentError?: ApiError | Error): ApiError {
export function BadRequest(message: string, context?: any, parentError?: ApiErrorType | Error): ApiErrorType {
return new ApiError(parentError, 400, message, 'Bad Request', context);
}

export function Unauthorized(message: string, context?: any, parentError?: ApiError | Error): ApiError {
export function Unauthorized(message: string, context?: any, parentError?: ApiErrorType | Error): ApiErrorType {
return new ApiError(parentError, 401, message, 'Unauthorized', context);
}

export function Forbidden(message: string, context?: any, parentError?: ApiError | Error): ApiError {
export function Forbidden(message: string, context?: any, parentError?: ApiErrorType | Error): ApiErrorType {
return new ApiError(parentError, 403, message, 'Forbidden', context);
}

export function NotFound(message: string, context?: any, parentError?: ApiError | Error): ApiError {
export function NotFound(message: string, context?: any, parentError?: ApiErrorType | Error): ApiErrorType {
return new ApiError(parentError, 404, message, 'Not Found', context);
}

export function Conflict(message: string, context?: any, parentError?: ApiError | Error): ApiError {
export function Conflict(message: string, context?: any, parentError?: ApiErrorType | Error): ApiErrorType {
return new ApiError(parentError, 409, message, 'Conflict', context);
}

export function InternalError(message: string, context?: any, parentError?: ApiError | Error): ApiError {
export function InternalError(message: string, context?: any, parentError?: ApiErrorType | Error): ApiErrorType {
return new ApiError(parentError, 500, message, 'Internal Server Error', context);
}
6 changes: 3 additions & 3 deletions packages/utils/graceful-shutdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const shutdownRoutine = {
...config.shutdown,
};

function shutdown(server, onShutdown?: (server) => any) {
function shutdown(server, onShutdown?: (_) => any) {
if (!shutdownRoutine.active) {
shutdownRoutine.active = true;
server.ready = false;
Expand All @@ -15,14 +15,14 @@ function shutdown(server, onShutdown?: (server) => any) {
}
}

function shutdownRequest(signal: string, server, onShutdown?: (server) => any) {
function shutdownRequest(signal: string, server, onShutdown?: (_) => any) {
return () => {
process.stderr.write(signal);
shutdown(server, onShutdown);
};
}

export default function gracefulShutdown(server, onShutdown?: (server) => any) {
export default function gracefulShutdown(server, onShutdown?: (_) => any) {
// Modern http compatibility layer
server.close = server.close || server.stop;

Expand Down
4 changes: 2 additions & 2 deletions packages/utils/intl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import config from 'config';
import i18next from 'i18next';
import i18nextBackend from 'i18next-node-fs-backend';
import i18nextBackend from 'i18next-fs-backend';
import { InternalError } from './errors';

i18nextInit();
Expand Down Expand Up @@ -29,4 +29,4 @@ function localize(key, locale) {

export const translation = {
localize,
};
};
22 changes: 12 additions & 10 deletions server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import express from 'express';
import config from 'config';
import context from './middleware/context';
import express from 'express';
import gracefulShutdown from '@nc/utils/graceful-shutdown';
import helmet from 'helmet';
import Logger from '@nc/utils/logging';
import security from './middleware/security';
import { router as userRoutes } from '@nc/domain-user';
import gracefulShutdown from '@nc/utils/graceful-shutdown';
import { createServer as createHTTPServer, Server } from 'http';
import { createServer as createHTTPSServer, Server as SecureServer } from 'https';

const logger = Logger('server');
const app = express();
const server: Server | SecureServer = (config.https.enabled === true) ? createHTTPSServer(config.https, app as any) : createHTTPServer(app as any);
server.ready = false;
Expand All @@ -16,26 +18,26 @@ gracefulShutdown(server);

app.use(helmet());
app.get('/readycheck', function readinessEndpoint(req, res) {
const status = (!!server.ready) ? 200 : 503;
res.status(status).send(status === 200 ? 'OK' : 'NOT OK');
const status = (server.ready) ? 200 : 503;
res.status(status).send(status === 200 ? 'OK' : 'NOT OK');
});

app.get('/healthcheck', function healthcheckEndpoint(req, res) {
res.status(200).send('OK');
res.status(200).send('OK');
});

app.use(context);
app.use(security);

app.use('/user', userRoutes);

app.use(function (err, req, res, next) {
res.status(500).json(err);
app.use(function(err, req, res) {
res.status(500).json(err);
});

server.listen(config.port, () => {
server.ready = true;
console.log(`Server started on port ${config.port}`);
server.ready = true;
logger.log(`Server started on port ${config.port}`);
});

export default server;
Loading

0 comments on commit c82ed36

Please sign in to comment.