I'm trying out NestJS + GraphQL using Apollo underneath. When I set the Apollo 'debug' option to be true, I can see the stacktrace in the response but I cannot find a way to log this stacktrace in our application logs.
I would like to have it in the log to troubleshoot issues in production. Is there a way to do this?
Here's the ApolloServerPlugin I use
import { Plugin } from '#nestjs/apollo';
import { Logger } from '#nestjs/common';
import {
ApolloServerPlugin,
GraphQLRequestListener,
} from 'apollo-server-plugin-base';
import {
BaseContext,
GraphQLRequestContext,
GraphQLRequestContextWillSendResponse,
} from 'apollo-server-types';
import * as util from 'util';
#Plugin()
export class LoggingPlugin implements ApolloServerPlugin {
constructor(private readonly logger: Logger) {}
async requestDidStart(
requestContext: GraphQLRequestContext,
): Promise<GraphQLRequestListener> {
const thatLogger = this.logger;
if (requestContext.request.operationName !== 'IntrospectionQuery') {
thatLogger.log(
`request query: ${requestContext.request.query || 'undefined'}`,
);
}
return {
async willSendResponse(
requestContextWillSendResponse: GraphQLRequestContextWillSendResponse<BaseContext>,
): Promise<void> {
if (
requestContextWillSendResponse.request.operationName !==
'IntrospectionQuery'
) {
if (!requestContextWillSendResponse.errors) {
thatLogger.log(`response without any errors`);
} else {
const errors = requestContextWillSendResponse.errors.concat();
const responseErrors =
requestContextWillSendResponse.response.errors?.concat();
if (errors && responseErrors) {
for (let i = 0; i < errors.length; i++) {
const result = {
...responseErrors[i],
stack: errors[i].stack,
};
if (result.extensions) {
delete result.extensions.exception;
}
if (
result.extensions &&
result.extensions.code !== 'INTERNAL_SERVER_ERROR'
) {
thatLogger.warn(
`response with errors: ${util.inspect(result, {
depth: 4,
})}`,
);
} else {
thatLogger.error(
`response with errors: ${util.inspect(result, {
depth: 4,
})}`,
);
}
}
}
}
}
},
};
}
}
I was able to do this using ApolloServerPlugin.
Related
I have been following https://github.com/goldbergyoni/nodebestpractices to learn more about nodejs best practices.
I have the following middleware that I implemented:
import { NextFunction, Request, Response } from "express";
import { BAD_REQUEST_ERROR_TYPES } from "../constants";
import { BadRequestError } from "./../error-handling";
const isAccountActive = (req: Request, res: Response, next: NextFunction) => {
if (req.session?.user?.isEmailVerified) {
next();
} else {
next(new BadRequestError(BAD_REQUEST_ERROR_TYPES.ACCOUNT_NOT_ACTIVE));
}
};
export default isAccountActive;
This is the test that I wrote for it:
describe("isAccountActive Middleware", () => {
describe("Recieving a request", () => {
test("When the request has a userUUID set in the session, it calls the next function without throwing a Bad Request Account Not Active error", async () => {
// Arrange
const req = {
method: "GET",
url: "/user/42",
session: {
user: {
userUUID: "some-string",
},
},
} as unknown as Request;
const res = jest.fn as unknown as Response;
const next = jest.fn;
// Act
await isAccountActive(req, res, next);
// Assert
expect(next).toBeCalledTimes(1);
expect(next).toBeCalledWith(
new BadRequestError(BAD_REQUEST_ERROR_TYPES.ACCOUNT_NOT_ACTIVE)
);
});
});
});
That is implementation number 3 for that test. I also tried using sinon, and node-mocks-http.
When I run the test command, I get the following error regardless of any implementation:
My app builds and runs fine; so I am not quite sure why jest would be throwing this error when the actuall server code itself is being compiled and run without any issues.
For reference, my config.ts:
import { isFullRedisURL } from "./helpers";
import { z } from "zod";
import { REDIS_URL_ERROR } from "./constants";
import { StartupError } from "./error-handling";
const input = {
environment: process.env.NODE_ENV,
basePort: process.env.BASE_PORT,
redisUrl: process.env.REDIS_URL,
redisPassword: process.env.REDIS_PASSWORD,
databaseUrl: process.env.DATABASE_URL,
sessionSecret: process.env.SESSION_SECRET,
};
const configSchema = z.object({
environment: z.string(),
basePort: z.coerce.number().positive().int(),
redisUrl: z
.string()
.refine((val) => isFullRedisURL(val), { message: REDIS_URL_ERROR }),
redisPassword: z.string(),
databaseUrl: z.string(),
sessionSecret: z.string().min(8),
});
let parsedInput;
try {
parsedInput = configSchema.parse(input);
} catch (e) {
throw new StartupError("Config validation error", e);
}
export const config = parsedInput;
export type Config = z.infer<typeof configSchema>;
my error-handling/error-handling-middleware.ts
import { COMMON_ERRORS, STATUS_CODES } from "../constants";
import { NextFunction, Request, Response } from "express";
import errorHandler from "./errorHandler";
import { config } from "../config";
const errorHandlingMiddleware = async (
// eslint-disable-next-line #typescript-eslint/no-explicit-any
error: any,
req: Request,
res: Response,
next: NextFunction
) => {
if (error && typeof error === "object") {
if (error.isTrusted === undefined || error.isTrusted === null) {
error.isTrusted = true; // Error during a specific request is usually not fatal and should not lead to process exit
}
}
errorHandler.handleError(error);
const { environment } = config;
const result = {
status: error?.httpStatus || STATUS_CODES.InternalServerError,
name: error?.name || COMMON_ERRORS.InternalServerError,
message: error?.message || "Sorry, something went wrong.",
details: error?.details,
stacktrace: environment === "development" ? error?.stacktrace : undefined,
};
res
.status(error?.httpStatus || STATUS_CODES.InternalServerError)
.send(result);
};
export default errorHandlingMiddleware;
the StartupError class:
import { FieldError } from "__shared/types";
import {
COMMON_ERRORS,
BAD_REQUEST_ERROR_MESSAGES,
BAD_REQUEST_ERROR_TYPES,
STATUS_CODES,
} from "../constants";
export class ApplicationError extends Error {
constructor(
public name: string,
public message: string,
public httpStatus: STATUS_CODES = STATUS_CODES.InternalServerError,
public isTrusted: boolean = true,
public isOperational: boolean = true,
public details?: FieldError[],
public stacktrace?: unknown
) {
super(message); // 'Error' breaks prototype chain here
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
this.name = name;
this.httpStatus = httpStatus;
this.isOperational = isOperational;
this.isTrusted = isTrusted;
this.details = details;
this.stacktrace = stacktrace;
Error.captureStackTrace(this, this.constructor);
}
}
export class BadRequestError extends ApplicationError {
constructor(type: keyof typeof BAD_REQUEST_ERROR_TYPES) {
super(
COMMON_ERRORS.BadRequestError,
BAD_REQUEST_ERROR_MESSAGES[type],
STATUS_CODES.BadRequest,
true,
true
);
}
}
export class StartupError extends ApplicationError {
constructor(reason: string, error: unknown) {
super(
COMMON_ERRORS.StartupError,
`Start up failed: (${reason}) `,
STATUS_CODES.InternalServerError,
false,
true,
undefined,
error
);
}
}
I am currently in the following situation,
I'm testing the Controllers with Jest and TS
When I run the application it works without problems.
When I run the tests I get this failure, which I have no idea what it could be
Below are the files that involve the problem:
Error message
FAIL src/modules/users/application/controllers/create-user-controller.spec.ts
● Test suite failed to run
TypeError: Class extends value undefined is not a constructor or null
5 | import { HttpRequest, HttpResponse } from '#/shared/interfaces/http';
6 |
> 7 | export class CreateSessionController extends BaseController {
| ^
8 | constructor(private createSessionUseCase: CreateSessionUseCase) {
9 | super();
10 | }
at Object.<anonymous> (src/modules/sessions/application/controllers/create-session-controller.ts:7:46)
at Object.<anonymous> (src/main/di/controllers.ts:3:1)
base-controller.ts
He is responsible for doing the Global error handling and executing the function that will be implemented in perform.
import * as fs from 'fs';
import { ApplicationErrors } from '#/shared/application/application-error';
import { DomainError } from '#/shared/domain/domain-error';
import { HttpRequest, HttpResponse } from '#/shared/interfaces/http';
import { IValidator } from './validations/validator-interface';
import { ILoggerGateway } from '../infra/gateways/logger-gateway/logger-gateway-interface';
import { container } from '#/main/di/container';
export abstract class BaseController {
private loggerGateway: ILoggerGateway;
constructor() {
this.loggerGateway = container.resolve('loggerGateway');
}
abstract perform(request: HttpRequest): Promise<HttpResponse>;
async handle(request: HttpRequest): Promise<HttpResponse> {
try {
const error = this.validate(request);
if (error) {
return error;
}
if (request.file && fs.existsSync(request.file.path)) {
fs.unlinkSync(request.file.path);
}
const response = await this.perform(request);
return response;
} catch (error) {
const err = error as Error;
switch (err.constructor) {
case DomainError:
return { data: { error: err.message }, statusCode: 400 };
case ApplicationErrors.UnauthorizedError:
return { data: { error: err.message }, statusCode: 401 };
case ApplicationErrors.ForbiddenError:
return { data: { error: err.message }, statusCode: 403 };
case ApplicationErrors.NotFoundError:
return { data: { error: err.message }, statusCode: 404 };
case ApplicationErrors.ConflictError:
return { data: { error: err.message }, statusCode: 409 };
default:
this.loggerGateway.error(err.message);
return { data: { error: 'Internal Server Error' }, statusCode: 500 };
}
}
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
buildValidators(request: HttpRequest): IValidator[] {
return [];
}
private validate(request: HttpRequest): HttpResponse | undefined {
const validators = this.buildValidators(request);
const errors: string[] = [];
validators.forEach((validator) => {
const error = validator.validate();
if (error) {
errors.push(error.message);
}
});
if (errors.length > 0) {
return { data: { errors }, statusCode: 400 };
}
return undefined;
}
}
create-user-controller.ts
import { RequiredFieldValidator } from '#/shared/application/validations/required-field-validator';
import { IValidator } from '#/shared/application/validations/validator-interface';
import { HttpRequest, HttpResponse } from '#/shared/interfaces/http';
import { BaseController } from '../../../../shared/application/base-controller';
import { UserSteps } from '../../domain/enums/user-steps';
import { ICreateUserUseCase } from '../usecases/interfaces/create-user-usecase-interface';
export class CreateUserController extends BaseController {
constructor(private createUserUseCase: ICreateUserUseCase) {
super();
}
async perform(request: HttpRequest): Promise<HttpResponse> {
const response = await this.createUserUseCase.execute({
...request.body,
stepId: UserSteps.UserCreated,
});
return { data: response, statusCode: 201 };
}
override buildValidators(request: HttpRequest): IValidator[] {
return [
new RequiredFieldValidator(request.body.email, 'email'),
new RequiredFieldValidator(request.body.phone, 'phone'),
new RequiredFieldValidator(request.body.password, 'password'),
new RequiredFieldValidator(request.body.roleId, 'roleId'),
];
}
}
create-user-controller.spec.ts
The test I'm running
import { ICreateUserUseCase } from '../usecases/interfaces/create-user-usecase-interface';
import { CreateUserUseCaseStub } from '../usecases/stubs/create-user-usecase-stub';
import { CreateUserController } from './create-user-controller';
describe('CreateUserController', () => {
let sut: CreateUserController;
let createUserUseCase: ICreateUserUseCase;
beforeEach(() => {
createUserUseCase = new CreateUserUseCaseStub();
sut = new CreateUserController(createUserUseCase);
});
it('Should be able to execute CreateUserController', async () => {
const response = await sut.handle({
body: {},
file: {},
headers: {},
method: {},
params: {},
query: {},
url: {},
user: {},
});
expect(response.statusCode).toBe(201);
});
});
create-user-usecases-stub.spec.ts
The stub i try to run on test
import { userMockData } from '../../../domain/mocks/user-mock';
import { UserMapper } from '../../mappers/user-mapper';
import { ICreateUserDTO } from '../dtos/create-user-dto';
import { IOutputUserDTO } from '../dtos/output-user-dto';
import { ICreateUserUseCase } from '../interfaces/create-user-usecase-interface';
export class CreateUserUseCaseStub implements ICreateUserUseCase {
async execute(_: ICreateUserDTO): Promise<IOutputUserDTO> {
const user = UserMapper.toDomain(userMockData);
return UserMapper.toOutputDTO(user);
}
}
I'm getting this error when running a test, however I have wrapped the component I am testing in a <BrowserRouter> component:
● Axios › gets a response
Invariant failed: You should not use <Link> outside a <Router>
91 |
92 | if (RootComponent) {
> 93 | const component = mountWithCustomWrappers(<Wrapper store={store}><RootComponent
{...props} />, rootWrappers);
nock.integration.test
import React from 'react';
import axios from 'axios';
const core = require('tidee-life-core');
import httpAdapter from 'axios/lib/adapters/http';
import doFetch from '../../../helpers/doFetch.js';
import ComponentBuilder from "../component-builder";
import LoginPage from "../../../scenes/login/login-page";
const host = 'http://example.com';
process.env.API_URL = host;
axios.defaults.host = host;
axios.defaults.adapter = httpAdapter;
const makeRequest = () => {
return doFetch({
url: core.urls.auth.login(),
queryParams: { foo: 'bar' },
})
.then(res => res.data)
.catch(error => console.log(error));
};
describe('Axios', () => {
let component;
let componentBuilder;
beforeEach(() => {
componentBuilder = new ComponentBuilder();
});
test('gets a response', async () => {
componentBuilder.includeInterceptor('login');
component = await componentBuilder.build({
RootComponent: LoginPage,
selector: 'LoginForm',
});
return makeRequest()
.then(response => {
expect(typeof response).toEqual('object');
expect(response.data.token).toEqual('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhNDY3MGE3YWI3ZWM0ZjQ2MzM4ODdkMzJkNzRkNTY5OSIsImlhdCI6MTU1MjA5MDI1NX0.vsKLXJEqSUZK-Y6IU9PumfZdW7t1SLM28jzJL89lcrA');
});
});
});
login-page.jsx:
import React, { Component } from 'react';
import PropTypes from "prop-types";
import { withRouter } from 'react-router-dom';
import { injectIntl, intlShape } from 'react-intl';
import queryString from 'query-string';
import { Link } from 'tidee-life-ui';
import LoginForm from './login-form.jsx';
import { doLogin } from '../../api/auth/auth-api';
import Auth from '../../modules/Auth';
import messages from '../../messages';
const { urls } = require('tidee-life-core');
class LoginPage extends Component {
constructor(props) {
super(props);
const { intl } = props;
if (Auth.isUserAuthenticated()) {
props.history.replace({ pathname: urls.pages.pathBoxes() });
}
this.messages = {
'account.activation.error.expired': intl.formatMessage(messages['account.activation.error.expired']),
'account.activation.required': intl.formatMessage(messages['account.activation.required']),
'common.click': intl.formatMessage(messages['common.click']),
'common.here': intl.formatMessage(messages['common.here']),
'error.500.msg': intl.formatMessage(messages['error.500.msg']),
'forgot.success': intl.formatMessage(messages['forgot.success']),
'login.account.needs.activating.partial': intl.formatMessage(messages['login.account.needs.activating.partial']),
'login.error.account.credentials': intl.formatMessage(messages['login.error.account.credentials']),
'login.validation.email': intl.formatMessage(messages['login.validation.email']),
'login.validation.password': intl.formatMessage(messages['login.validation.password']),
'signup.account.created': intl.formatMessage(messages['signup.account.created'])
};
let alertMessage;
let alertMessageType;
const query = queryString.parse(props.location.search);
if ('signup-success' in query) {
alertMessage = this.messages['signup.account.created'];
alertMessageType = 'success';
} else if ('forgot-success' in query) {
alertMessage = this.messages['forgot.success'];
alertMessageType = 'success';
}
this.state = {
alert: {
type: alertMessageType ? alertMessageType : '',
msg: alertMessage ? alertMessage : '',
},
user: {
email: '',
password: ''
}
};
this.changeUser = this.changeUser.bind(this);
this.clearAlert = this.clearAlert.bind(this);
this.processForm = this.processForm.bind(this);
}
clearAlert() {
this.setState({ alert: {
type: '',
msg: '',
}});
}
processForm(e) {
e.preventDefault();
return doLogin({
email: this.state.user.email,
password: this.state.user.password,
}).then((response) => {
Auth.authenticateUser(response.data.token);
this.props.history.replace({ pathname: urls.pages.pathBoxes() });
}).catch((error) => {
const msg = error.message && this.messages[error.message] ? [this.messages[error.message]] : [this.messages['error.500.msg']];
if (error.message === 'account.activation.error.expired' || error.message === 'account.activation.required') {
const to = urls.pages.pathResendLink(error.data.confirmHash);
msg.push(` ${this.messages['common.click']} `);
msg.push(<Link underline color="inherit" key="email" to={to}>{this.messages['common.here']}</Link>);
msg.push(` ${this.messages['login.account.needs.activating.partial']}`);
}
this.setState({
alert: {
type: 'error',
msg,
}
});
});
}
changeUser(event) {
const { name, value } = event.target;
this.setState((currentState) => ({
user: {
...currentState.user,
[name]: value,
}
}));
}
render() {
return (
<LoginForm
data-test="login-form"
alert={this.state.alert}
onSubmit={this.processForm}
onChange={this.changeUser}
user={this.state.user}
/>
);
}
}
LoginPage.propTypes = {
history: PropTypes.object,
intl: intlShape.isRequired,
location: PropTypes.object.isRequired,
};
export default injectIntl(withRouter(LoginPage));
component-builder.js
import React from "react";
import nock from 'nock';
import cloneDeep from 'lodash.clonedeep';
import { mountWithCustomWrappers } from 'enzyme-custom-wrappers';
import { waitForStoreState } from './wait/wait-for-store-state';
import Wrapper from './wrapper.jsx';
import waitForComponentPredicate from './wait-for-component-predicate/wait-for-component-predicate';
import waitForComponentSelector from './wait-for-component-selector/wait-for-component-selector';
import { startAllNockServiceIntercepts } from './nock/nock-manager';
import nockServices from './nock/services';
import store from "../../store/store";
import wrappers from './wrappers';
const rootWrappers = component => wrappers(component);
class ComponentBuilder {
constructor() {
this.nockInterceptors = [];
this.storePreparers = [];
}
includeInterceptor( interceptorName, nockConfigOverride = null ) {
// Maybe need to do a clone deep here if things start breaking!
const clonedNockService = cloneDeep(nockServices[interceptorName]().nockConfig);
const nockService = {
[interceptorName]: {
...clonedNockService,
...(nockConfigOverride || {}),
}
};
this.nockInterceptors.push(nockService);
}
prepareStore( storePreparer ) {
this.storePreparers.push(storePreparer);
}
async waitForStoreToUpdate() {
const promises = this.storePreparers
.map(service => waitForStoreState(service.redux.storeStateToWaitFor, store));
await Promise.all(promises);
}
async runStorePreparers() {
nock.cleanAll();
const interceptors = [];
this.storePreparers.forEach((service) => {
const interceptorName = service.http.interceptor;
const clonedNockService = service.http.interceptor && cloneDeep(nockServices[interceptorName]().nockConfig);
interceptors.push({
[interceptorName]: {
...clonedNockService,
}
});
});
startAllNockServiceIntercepts(interceptors);
this.storePreparers.forEach(service => service.redux.actionToDispatch && store.dispatch(service.redux.actionToDispatch()));
return await this.waitForStoreToUpdate();
}
/**
* Build a component to be tested.
* #param RootComponent
* #param selector {string} - A selector to wait for. CSS selector or name of component.
* #param props {object}
* #param store {object}
* #param predicate {function} - A function that returns true if a condition is met.
* #param predicateMaxTime {number}
* #param predicateInterval {number}
* #returns {Promise<*>}
*/
async build({
RootComponent = null,
selector = '',
props = {},
predicate = null,
predicateMaxTime = 2000,
predicateInterval = 10,
} = {}) {
try {
await this.runStorePreparers();
startAllNockServiceIntercepts(this.nockInterceptors);
if (RootComponent) {
const component = mountWithCustomWrappers(<Wrapper store={store}><RootComponent {...props} /></Wrapper>, rootWrappers);
if (selector) {
await waitForComponentSelector({ selector, rootComponent: component, store });
}
if (predicate) {
await waitForComponentPredicate({
predicate,
rootComponent: component,
store,
maxTime: predicateMaxTime,
interval: predicateInterval,
});
}
return component;
}
} catch(err) {
throw err;
}
}
}
export default ComponentBuilder;
wrapper.jsx
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { BrowserRouter } from "react-router-dom";
import { Provider } from 'react-redux';
import ThemeProvider from "../../theme/Theme.jsx";
import LocaleProviderWrapper from "./locale-provider-wrapper.jsx";
const propTypes = {
children: PropTypes.element.isRequired,
store: PropTypes.object.isRequired,
};
class Wrapper extends Component {
getStore() {
return this.props.store;
}
render() {
return (
<Provider store={this.props.store}>
<LocaleProviderWrapper>
<ThemeProvider>
<BrowserRouter>{this.props.children}</BrowserRouter>
</ThemeProvider>
</LocaleProviderWrapper>
</Provider>
);
}
}
Wrapper.propTypes = propTypes;
export default Wrapper;
Exactly as message says, you cannot use Link which doesn't have any parent of type Router. In you processForm function you are building a messaage with Link component which is worng.
if (error.message === 'account.activation.error.expired' || error.message === 'account.activation.required') {
const to = urls.pages.pathResendLink(error.data.confirmHash);
msg.push(` ${this.messages['common.click']} `);
msg.push(<Link underline color="inherit" key="email" to={to}>{this.messages['common.here']}</Link>);
msg.push(` ${this.messages['login.account.needs.activating.partial']}`);
}
You should use a tag to build dynamic link. May be something like:
msg.push(`${this.messages['common.here']}`);
Currently I have the following class:
import * as winston from 'winston';
const { combine, timestamp, printf, label, json} = winston.format;
import {isEmpty, isNil} from 'lodash';
import {Log} from './Log';
export class LoggingService {
public static initializeKeys() {
this.keys = {tag: 'tag'};
}
public static intialize() {
this.initializeKeys();
const maskFormat = winston.format((meta) => {
meta[this.keys.tag] = 'WebProxyConsumer';
return meta;
})();
const jsonLog = printf((info) => {
return JSON.stringify(info);
});
this.logger = winston.createLogger({
level: 'info',
format: combine(
timestamp(),
jsonLog
),
transports: [
new winston.transports.Console(),
new winston.transports.File( { filename: 'error.log', level: 'error', maxsize: 10000000})
],
exceptionHandlers: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'exceptions.log', maxsize: 10000000 })
]
});
}
public static getDefaultLogger() {
return this.logger;
}
public static error(error, label, data) {
if (isNil(this.logger)) {
LoggingService.intialize();
}
let logObj = new Log(null, null);
logObj.level = 'error';
if (!isNil(label)) {
logObj.label = label;
}
if (!isNil(data)) {
if (typeof data === 'string') {
logObj.message = data;
} else {
Object.keys(data).forEach((key) => {
const value = data[key];
if (logObj.hasOwnProperty(key)) {
logObj[key] = value;
} else {
logObj.data[key] = value;
}
});
}
}
if (error instanceof Error) {
if (error.hasOwnProperty('message')) {
logObj.message += ' Error Message: ' + error.message;
}
if (error.hasOwnProperty('stack')) {
logObj.error_stack = error.stack;
}
}
if (typeof error === 'string') {
logObj.message += ' Error Message: ' + error;
}
if (isNil(logObj.device_id)) {
delete logObj.device_id;
}
if (isNil(logObj.data) || isEmpty(logObj.data)) {
delete logObj.data;
}
this.logger.log(logObj);
}
public static info(data, label) {
if (isNil(this.logger)) {
LoggingService.intialize();
}
let logObj = new Log(null, null);
if (!isNil(label)) {
logObj.label = label;
}
if (!isNil(data)) {
if (typeof data === 'string') {
logObj.message = data;
} else {
Object.keys(data).forEach((key) => {
const value = data[key];
if (logObj.hasOwnProperty(key)) {
logObj[key] = value;
} else {
logObj.data[key] = value;
}
});
}
if (isNil(logObj.device_id)) {
delete logObj.device_id;
}
if (isNil(logObj.data) || isEmpty(logObj.data)) {
delete logObj.data;
}
this.logger.log(logObj);
}
}
private static logger: winston.Logger;
private static keys: any;
}
I'm using mocha for unit testing and so far this is my unit test for the class:
describe('LoggingService Tests', () => {
const loggingService = new LoggingService();
const loggingServiceProto = Object.getPrototypeOf(loggingService);
it('Checking LoggingService Initialization', () => {
expect(loggingServiceProto).to.not.be.null;
expect(loggingServiceProto.combine).to.not.be.null;
expect(loggingServiceProto.timestamp).to.not.be.null;
expect(loggingServiceProto.printf).to.not.be.null;
expect(loggingServiceProto.logger).to.not.be.null;
expect(loggingServiceProto.keys).to.not.be.null;
})
it('Checking initializeKeys', () => {
expect(loggingServiceProto.initializeKeys()).to.not.be.null;
})
it('Checking initialize', () => {
expect(loggingServiceProto.intialize()).to.not.be.null;
expect(loggingServiceProto.logger).to.not.be.null;
})
it('Checking getDefaultLogger', () => {
expect(loggingServiceProto.getDefaultLogger()).to.not.be.null;
})
})
The importing for mocha is correct and for my first test 'Checking LoggingService Initialization', I'm successfully passing. That's to say I'm able to initialize my class without a problem. The problem is with the rest of the tests I'm running. For those, I get the following errors:
TypeError: loggingServiceProto.initializeKeys is not a function
TypeError: loggingServiceProto.intialize is not a function
TypeError: loggingServiceProto.getDefaultLogger is not a function
Would anyone know why this is happening? These functions are defined and I'm not experiencing this issue with any other classes I'm testing using mocha.
Any advice would greatly be appreciated!
something to do with tsconfig. remove tsconfig and see if test passes.
the test command should look like:
mocha -r ts-node/register src/**/*.spec.ts
when I try to push my array I have the following error :
Cannot read property 'push' of null
I tried to solve my problem by initializing the array, but it's not working. Sorry, I'm new to code and I don't understand why it doesn't work.
Thank you for your help.
balade.ts :
export class Balade {
NOM_BALADE: string;
DATE_DEPART: string;
LIEU_RDV: string;
ID_BALADE?: number;
constructor(NOM_BALADE: string, DATE_DEPART: string, LIEU_RDV: string, ID_BALADE?: number) {
this.NOM_BALADE = NOM_BALADE;
this.DATE_DEPART = DATE_DEPART;
this.LIEU_RDV = LIEU_RDV;
this.ID_BALADE = ID_BALADE;
}
}
balade.service.ts :
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse, HttpParams } from '#angular/common/http';
import { Observable, throwError } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Balade } from './balade';
#Injectable({
providedIn: 'root'
})
export class BaladeService {
baseUrl = './htdocs/api';
balades: Balade[] = [];
constructor(private http: HttpClient) { }
getAll(): Observable<Balade[]> {
return this.http.get(`${this.baseUrl}/list.php`).pipe(
map((res) => {
this.balades = res['data'];
return this.balades;
}),
catchError(this.handleError));
}
store(balade: Balade): Observable<Balade[]> {
return this.http.post(`${this.baseUrl}/store.php`, { data: balade })
.pipe(map((res) => {
this.balades.push(res['data']);
return this.balades;
}),
catchError(this.handleError));
}
app.component.ts :
import { Component, OnInit } from '#angular/core';
import { Balade } from './balade';
import { BaladeService } from './balade.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
balades: Balade[] = [];
error = '';
success = '';
balade = new Balade('', '','');
constructor(private baladeService: BaladeService) {
}
ngOnInit() {
this.getBalades();
}
getBalades(): void {
this.baladeService.getAll().subscribe(
(res: Balade[]) => {
this.balades = res;
},
(err) => {
this.error = err;
}
);
}
addBalade(f) {
this.resetErrors();
this.baladeService.store(this.balade)
.subscribe(
(res: Balade[]) => {
// Update the list of balades
this.balades = res;
// Inform the user
this.success = 'Created successfully';
// Reset the form
f.reset();
},
(err) => this.error = err
);
}
Here is the error :
TypeError: Cannot read property 'push' of null
at t.project (main.6f2a73c37c9b189cf02f.js:1)
at t._next (main.6f2a73c37c9b189cf02f.js:1)
at t.next (main.6f2a73c37c9b189cf02f.js:1)
at t._next (main.6f2a73c37c9b189cf02f.js:1)
at t.next (main.6f2a73c37c9b189cf02f.js:1)
at t._next (main.6f2a73c37c9b189cf02f.js:1)
at t.next (main.6f2a73c37c9b189cf02f.js:1)
at t.notifyNext (main.6f2a73c37c9b189cf02f.js:1)
at t._next (main.6f2a73c37c9b189cf02f.js:1)
at t.next (main.6f2a73c37c9b189cf02f.js:1)
There is something wrong with balades which is not working as an array as a result push property of array is not functioning.
You can make it an array by assigning an empty array.
For example
this.arrayName = this.arrayName || []; //assign an empty array
In balade.service.ts
store(balade: Balade): Observable<Balade[]> {
return this.http.post(`${this.baseUrl}/store.php`, { data: balade })
.pipe(map((res) => {
this.balades = this.balades || [];
this.balades.push(res['data']);
return this.balades;
}),
catchError(this.handleError));
}
You didn't use an arrow function to define your store method, so this is scoped to the function, not the class.
getBalades = (): void => {
this.baladeService.getAll().subscribe(
(res: Balade[]) => {
this.balades = res;
},
(err) => {
this.error = err;
}
);
}
Try this:
store(balade: Balade): Observable<Balade[]> {
var ref = this; // Store the this into one variable and then use that variable to access a global scope
return this.http.post(`${this.baseUrl}/store.php`, { data: balade })
.pipe(map((res) => {
ref.balades.push(res['data']);
return this.balades;
}),
catchError(this.handleError));
}
EDIT:
In service:
balades: Balade[] = [];
let create = document.getElementById("create");
create.addEventListener("click", function(e) {
let addTxt = document.getElementById("addTxt");
let notes = localStorage.getItem("notes");
if (notes == null) {
notesObj = [];
} else {
notesObj = JSON.parse(notes);
}
notesObj.push(addTxt.value);
localStorage.setItem("notes", JSON.stringify(notesObj));
addTxt.value = "";
// console.log(notesObj);
showNotes();
});