diff --git a/.env b/.env index 94e0ef4960f6ecef603b2ffccd7e884be6c75400..435ebf9c230036e5c8c82538d43775d4e3253441 100644 --- a/.env +++ b/.env @@ -1,8 +1,18 @@ -PORT=3009 -BAP_ID=ps-bap-network.becknprotocol.io -BAP_URI=https://ps-bap-network.becknprotocol.io -BAP_CLIENT_URI=https://ps-bap-client.becknprotocol.io -CITY_NAME=Bangalore -CITY_CODE=std:080 -COUNTRY_NAME=India -COUNTRY_CODE=IND \ No newline at end of file +APP_NAME="Generic Client Layer" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_PORT=3002 +APP_URL=http://localhost + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +PS_BASE_URI=https://ps-bap-client.becknprotocol.io +PS_BAP_ID=ps-bap-network.becknprotocol.io +PS_BAP_URI=https://ps-bap-network.becknprotocol.io +PS_CITY_NAME=Bangalore +PS_CITY_CODE=std:080 +PS_COUNTRY_NAME=India +PS_COUNTRY_CODE=IND diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..2a3ccdae9971baadfbff1a76faf0461ee2dd4b2f --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_PORT=3002 +APP_URL=http://localhost + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +PS_BASE_URI=https://ps-bap-client.becknprotocol.io +PS_BAP_ID=ps-bap-network.becknprotocol.io +PS_BAP_URI=https://ps-bap-network.becknprotocol.io +PS_CITY_NAME=Bangalore +PS_CITY_CODE=std:080 +PS_COUNTRY_NAME=India +PS_COUNTRY_CODE=IND diff --git a/.gitignore b/.gitignore index 4db21bf294b243ddfc06d9c2de73ba990ca24a8d..036d692a1b36458a1a0bbf24981a9728541b3810 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ package-lock.json .DS_Store coverage/ +logs/ diff --git a/mappings/context.jsonata b/mappings/context.jsonata new file mode 100644 index 0000000000000000000000000000000000000000..bbceac814d641bda0a25093a60ce4bf2366b8343 --- /dev/null +++ b/mappings/context.jsonata @@ -0,0 +1,24 @@ +{ + "domain": domain, + "bpp_id": bpp_id, + "bpp_uri": bpp_uri, + "bap_id": $env.PS_BAP_ID, + "action": $action, + "bap_uri": $env.PS_BAP_URI, + "version": "1.1.0", + "transaction_id": transaction_id ? transaction_id : $uuid(), + "message_id": message_id ? message_id : $uuid(), + "location": { + "country": { + "name": $env.PS_COUNTRY_NAME, + "code": $env.PS_COUNTRY_CODE + }, + "city": { + "name": $env.PS_CITY_NAME, + "code": $env.PS_CITY_CODE + } + }, + "ttl": "PT10M", + "key": key, + "timestamp": $moment().toISOString() +} diff --git a/mappings/init.jsonata b/mappings/init.jsonata new file mode 100644 index 0000000000000000000000000000000000000000..64fe7ea89bfa2b7380761d50354d7ca2d43acd8c --- /dev/null +++ b/mappings/init.jsonata @@ -0,0 +1,6 @@ +$.data.message.orders.{ + "context": $context(%.%.context, $action), + "message": { + "order": $ + } +}[] diff --git a/mappings/on_init.jsonata b/mappings/on_init.jsonata new file mode 100644 index 0000000000000000000000000000000000000000..7d8f731b948bb51a0cfa99f6c435bd53ac29f4e9 --- /dev/null +++ b/mappings/on_init.jsonata @@ -0,0 +1,61 @@ +$.responses.{ + "context":context, + "message": { + "order": { + "type": message.order.type, + "provider": { + "id": message.order.provider.id, + "name": message.order.provider.descriptor.name, + "short_desc": message.order.provider.descriptor.short_desc, + "long_desc": message.order.provider.descriptor.long_desc, + "rating": message.order.provider.rating, + "images": message.order.provider.descriptor.images.{ + "url": url, + "size_type": size_type + }, + "media": message.order.provider.descriptor.media.{ + "url": url + } + }, + "items": message.order.items.{ + "id": id, + "name": descriptor.name, + "short_desc": descriptor.short_desc, + "long_desc": descriptor.long_desc, + "price": price, + "rating": rating, + "rateable": rateable, + "time": time, + "quantity": quantity, + "categories": $map( + $filter(%.provider.categories, function($category) { $boolean($category.id in category_ids)}), + function($category) { + { "id": $category.id, "name": $category.descriptor.name, "code": $category.descriptor.code } + } + )[], + "locations": $map( + $filter(%.provider.locations, function($location) { $boolean($location.id in location_ids)}), + function($location) { + { + "id": $location.id, + "city": $location.city.name, + "state": $location.state.name, + "country": $location.country.name + } + } + )[], + "tags": tags.{ + "code": descriptor.code, + "name": descriptor.name, + "display": display, + "list": list.{ "code": descriptor.code, "name": descriptor.name, "value": value }[] + }[] + }, + "fulfillments": message.order.fulfillments, + "quote": message.order.quote, + "billing": message.order.billing, + "payments": message.order.payments, + "cancellation_terms": message.order.cancellation_terms + } + } +}[] diff --git a/mappings/on_select.jsonata b/mappings/on_select.jsonata index 97ced2b8b6d5870fdbb1093c0684b4e08106479e..ddd202454d9aa934226c69269a4d2b26b8826af3 100644 --- a/mappings/on_select.jsonata +++ b/mappings/on_select.jsonata @@ -1,5 +1,6 @@ -{ - "data": responses.{ +$.responses.{ + "context": context, + "message": { "order": { "type": message.order.type, "quote": message.order.quote, @@ -52,5 +53,5 @@ }[] } } - }[] -} + } +}[] diff --git a/mappings/search.jsonata b/mappings/search.jsonata index a9db61437d64bd3074a7a4c8c13536c12f79d6ec..711b24f083721fe3c5571aa318ab80b7bdc8ebf1 100644 --- a/mappings/search.jsonata +++ b/mappings/search.jsonata @@ -1,5 +1,5 @@ { - "context":context, + "context": $context(context, $action), "message":{ "intent":{ "item":{ @@ -26,7 +26,8 @@ "name":provider.providerName }, "locations":$map( provider.providerCity, function($location) { - {"city":{ + { + "city":{ "name":$location }} })[], @@ -51,4 +52,4 @@ } } } -} \ No newline at end of file +} diff --git a/mappings/select.jsonata b/mappings/select.jsonata index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..64fe7ea89bfa2b7380761d50354d7ca2d43acd8c 100644 --- a/mappings/select.jsonata +++ b/mappings/select.jsonata @@ -0,0 +1,6 @@ +$.data.message.orders.{ + "context": $context(%.%.context, $action), + "message": { + "order": $ + } +}[] diff --git a/package.json b/package.json index 7a05548947f4cd75019196698988ac76f0fc05cc..d455fbd8d7c12e2b71e29331b7eb956aac5b51db 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,34 @@ { - "name": "generic-client-layer", + "name": "gcl", "version": "1.0.0", - "description": "Beckn Generic Client Layer", - "main": "index.ts", + "description": "", + "main": "index.js", "scripts": { - "test": "node dist/index.js", - "start": "node dist/index.js", - "start:dev": "concurrently \"npx tsc --watch\" \"nodemon --delay 1000ms -q dist/index.js\"", - "dev": "nodemon src/index.ts", - "build": "npx tsc" + "test": "echo \"Error: no test specified\" && exit 1", + "start": "nodemon src/app.ts" }, - "repository": { - "type": "git", - "url": "git+https://github.com/beckn/generic-client-layer.git" - }, - "author": "beckn", + "keywords": [], + "author": "", "license": "ISC", - "bugs": { - "url": "https://github.com/beckn/generic-client-layer/issues" - }, - "homepage": "https://github.com/beckn/generic-client-layer#readme", "dependencies": { "app-root-path": "^3.1.0", - "axios": "^1.6.1", - "class-transformer": "^0.5.1", - "concurrently": "^8.2.2", - "cors": "^2.8.5", + "axios": "^1.6.2", "dotenv": "^16.3.1", "express": "^4.18.2", - "joi": "^17.11.0", + "ini": "^4.1.1", + "inversify": "^6.0.2", + "inversify-express-utils": "^6.4.6", "jsonata": "^2.0.3", - "lodash": "^4.17.21", "moment": "^2.29.4", "reflect-metadata": "^0.1.13", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "winston": "^3.11.0", + "winston-express": "^0.1.1" }, "devDependencies": { - "@types/cors": "^2.8.15", - "@types/express": "^4.17.20", - "@types/lodash": "^4.14.200", - "@types/node": "^20.8.7", - "@types/uuid": "^9.0.6", - "nodemon": "^3.0.1", - "typescript": "^5.2.2" + "@types/express": "^4.17.21", + "@types/ini": "^1.3.33", + "@types/uuid": "^9.0.7", + "nodemon": "^3.0.1" } } diff --git a/src/App.ts b/src/App.ts new file mode 100644 index 0000000000000000000000000000000000000000..b594afaf3f27004c8e8d80a10faa846fc7ffd984 --- /dev/null +++ b/src/App.ts @@ -0,0 +1,39 @@ +import express, { Application } from 'express'; +import { container, server } from './inversify/inversify.config'; +import './gcl/gcl.controller'; +import { ConfigService } from './config/config.service'; +import { AppLogger } from './app/app.logger'; +import { ErrorHandlerMiddleware } from './middleware/errorhandler.middleware'; + +class App { + public app: Application; + + constructor() { + this.app = express(); + this.config(); + this.setupMiddlewares(); + } + + private config(): void { + server.setConfig((app) => { + app.use(express.json()); + app.use(express.urlencoded({ extended: true })) + }); + this.app.use(server.build()); + } + + private setupMiddlewares() { + const errorHandlerMiddleware = container.get<ErrorHandlerMiddleware>(ErrorHandlerMiddleware); + this.app.use(errorHandlerMiddleware.handleError.bind(errorHandlerMiddleware)); + } +} + +const configService = container.resolve<ConfigService>(ConfigService); +const logger = container.resolve<AppLogger>(AppLogger); + +const app = new App().app; +const port = configService.getAppPort(); + +app.listen(port, () => { + logger.info(`Server is running on http://localhost:${port}`); +}); diff --git a/src/app/app.logger.ts b/src/app/app.logger.ts new file mode 100644 index 0000000000000000000000000000000000000000..888ce871835982285a923a2cdaf8a28f733faed3 --- /dev/null +++ b/src/app/app.logger.ts @@ -0,0 +1,39 @@ +import { injectable } from "inversify"; +import winston, { Logger, format } from "winston"; +import { format as printf } from 'util'; + +@injectable() +export class AppLogger { + private logger: Logger; + + constructor() { + const logFormat = format.printf(info => `\n${info.timestamp} [${info.level}]: ${info.message}`) + this.logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), // Add timestamp to the log message + winston.format.simple() + ), + transports: [ + new winston.transports.Console({ + format: format.combine(format.colorize(), logFormat, format.splat()), + }), + new winston.transports.File({ filename: 'logs/error.log', level: 'error', format: format.combine(format.colorize(), format.json()) }), + new winston.transports.File({ filename: 'logs/combined.log', format: format.combine(format.colorize(), format.json()) }), + ], + exitOnError: false + }); + } + + public info(message: string, ...args: any[]): void { + this.logger.info(this.formatMessage(message, args)); + } + + public error(message: string, ...args: any[]): void { + this.logger.error(this.formatMessage(message, args)); + } + + private formatMessage(message: string, args: any[]): string { + return args.length > 0 ? printf(message, ...args) : message; + } +} diff --git a/src/app/app.ts b/src/app/app.ts deleted file mode 100644 index 68823c965eb37b2bb5b92eb06029e91139bde1c1..0000000000000000000000000000000000000000 --- a/src/app/app.ts +++ /dev/null @@ -1,45 +0,0 @@ -import express, { Express, Router, Request, Response } from "express"; -import cors from "cors"; -import dotenv from "dotenv"; -import { clientLayerRoutes } from "./routes"; - -interface InitAppParams { - app: Express; -} - -const initApp = ({ app }: InitAppParams) => { - const router: Router = express.Router(); - dotenv.config(); - app.options( - "*", - cors<Request>({ - origin: process.env.NODE_ENV === "*", - optionsSuccessStatus: 200, - credentials: true, - methods: ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"] - }) - ); - - app.use( - cors({ - origin: process.env.NODE_ENV === "*", - methods: ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"] - }) - ); - - app.set("trust proxy", true); - app.use(express.urlencoded({ extended: true, limit: "200mb" })); - app.use(express.json({ limit: "200mb" })); - app.use(router); - - router.use("/ping", (req: Request, res: Response) => { - res.json({ - status: 200, - message: "Generic Client Layer Started" - }); - }); - router.use(clientLayerRoutes()); - return app; -}; - -export { initApp }; diff --git a/src/app/index.ts b/src/app/index.ts deleted file mode 100644 index 9bf9b1b68aa2e1dd6e9b33eefc7fd18cce761d4d..0000000000000000000000000000000000000000 --- a/src/app/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./routes"; diff --git a/src/app/routes.ts b/src/app/routes.ts deleted file mode 100644 index a588241a92298056e7c15ef36b9747269f779c2b..0000000000000000000000000000000000000000 --- a/src/app/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import express, { Router } from "express"; -import { validateRequest } from "../common"; -import { searchController } from "../modules/search/controller"; -import { selectController } from "../modules/select/controller"; -const router: Router = express.Router(); - -export const clientLayerRoutes = () => { - router.post("/search", validateRequest, searchController); - router.post("/select", validateRequest, selectController); - router.post("/init", validateRequest, () => {}); - router.post("/confirm", validateRequest, () => {}); - router.post("/update", validateRequest, () => {}); - router.post("/status", validateRequest, () => {}); - router.post("/cancel", validateRequest, () => {}); - router.post("/track", validateRequest, () => {}); - router.post("/support", validateRequest, () => {}); - router.post("/rating", validateRequest, () => {}); - - return router; -}; diff --git a/src/app/server.ts b/src/app/server.ts deleted file mode 100644 index ca01a2611d7291fe6a362b2ba4ace425a9106951..0000000000000000000000000000000000000000 --- a/src/app/server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { initApp } from "./app"; -import { Express } from "express"; -export const startServer = async (app: Express) => { - const serverApp = initApp({ app }); - const PORT: string = process.env.PORT || "3009"; - - serverApp.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); - }); -}; diff --git a/src/common/context.ts b/src/common/context.ts deleted file mode 100644 index 4e11d94e2c2236aac013078ff22cd379c13431f0..0000000000000000000000000000000000000000 --- a/src/common/context.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { IRequestContext, requestContextSchema } from "./schemaValidator"; -import { v4 as uuid } from "uuid"; -import moment from "moment"; -export interface IRequestContextStructure_ver_1_1_0 { - domain: string; - location: { - id?: string; - descriptor?: { - name?: string; - code?: string; - short_desc?: string; - long_desc?: string; - additional_desc?: { - url?: string; - content_type?: "text/plain"; - }; - media?: [ - { - mimetype?: string; - url?: string; - signature?: string; - dsa?: string; - } - ]; - images?: [ - { - url?: string; - size_type?: string; - width?: string; - height?: string; - } - ]; - }; - map_url?: string; - gps?: string; - address?: string; - city?: { - name?: string; - code?: string; - }; - district?: string; - state?: { - name?: string; - code?: string; - }; - country?: { - name?: string; - code?: string; - }; - area_code?: string; - circle?: { - gps?: string; - radius?: { - type?: string; - value?: string; - estimated_value?: string; - computed_value?: string; - range?: { - min?: string; - max?: string; - }; - unit?: string; - }; - }; - polygon?: string; - rating?: string; - }; - action: string; - version: string; - bap_id: string; - bap_uri: string; - bpp_id?: string; - bpp_uri?: string; - transaction_id: string; - message_id: string; - timestamp?: string; - key?: string; - ttl?: string; -} - -interface IResponseContextStructure_ver_1_1_0 { - bapId?: string; - messageId?: string; - transactionId?: string; - bapUri?: string; - bppId?: string; - bppUri?: string; - domain?: string; -} - -export const buildRequestContextVer1_1_0 = ( - input: IRequestContext, - action: string -) => { - const { - bppId, - bppUri, - domain, - messageId, - transactionId, - bapId, - bapUri, - key - } = input; - const context: IRequestContextStructure_ver_1_1_0 = { - domain: domain, - bpp_id: bppId, - bpp_uri: bppUri, - bap_id: bapId || `${process.env.BAP_ID}`, - action: action, - bap_uri: bapUri || `${process.env.BAP_URI}`, - version: "1.1.0", - transaction_id: transactionId || uuid(), - message_id: messageId || uuid(), - location: { - country: { - name: `${process.env.COUNTRY_NAME}`, - code: `${process.env.COUNTRY_CODE}` - }, - city: { - name: `${process.env.CITY_NAME}`, - code: `${process.env.CITY_CODE}` - } - }, - ttl: "PT10M", - key: key, - timestamp: moment().toISOString() - }; - return context; -}; - -export const responseContextBuilderVer1_1_0 = ( - context: IRequestContextStructure_ver_1_1_0 -): IResponseContextStructure_ver_1_1_0 => { - return { - messageId: context?.message_id, - transactionId: context?.transaction_id, - bppId: context?.bpp_id, - bppUri: context?.bpp_uri, - domain: context?.domain - }; -}; diff --git a/src/common/index.ts b/src/common/index.ts deleted file mode 100644 index 69d98a97d487e0f6141cc34bb96b4451893ccfaf..0000000000000000000000000000000000000000 --- a/src/common/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./context"; -export * from "./schemaValidator"; -export * from "./schemaObjectGenerator"; diff --git a/src/common/mapper.service.ts b/src/common/mapper.service.ts deleted file mode 100644 index 26d34b4931462636038e0e4723069bc655975328..0000000000000000000000000000000000000000 --- a/src/common/mapper.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import fs from "fs"; -import path from "path"; -import appRootPath from 'app-root-path'; -import jsonata from 'jsonata'; -import { removeEmptyObjectKeys } from './utils' - -export const map = async (data: any, action?: string) => { - const expression = jsonata(fs.readFileSync(path.join(appRootPath.toString(), `/mappings/${action}.jsonata`), "utf8")); - - let mapped = await expression.evaluate(data); - mapped = removeEmptyObjectKeys(mapped); - return mapped; -}; diff --git a/src/common/mappingJsons/request/searchJson.json b/src/common/mappingJsons/request/searchJson.json deleted file mode 100644 index f7fc8f155d2d115834826711a027ed15ab859267..0000000000000000000000000000000000000000 --- a/src/common/mappingJsons/request/searchJson.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "searchString": { - "path": "message.intent.item.descriptor.name" - }, - "itemId": { "path": "message.intent.item.id" }, - "fulfillment.agentName": { - "path": "message.intent.fulfillment.agent.person.name" - }, - "fulfillment.customerGender": { - "path": "message.intent.fulfillment.customer.person.gender" - }, - - "provider.providerName": { - "path": "message.intent.provider.descriptor.name" - }, - "provider.providerCity": { - "path": "message.intent.provider.locations[0].city.name" - }, - "provider.providerId": { "path": "message.intent.provider.id" }, - "category.categoryCode": { - "path": "message.intent.category.descriptor.code" - }, - "category.categoryName": { - "path": "message.intent.category.descriptor.name" - }, - "category.categoryId": { - "path": "message.intent.category.id" - }, - "location": { "path": "message.intent.location.circle.gps" } -} diff --git a/src/common/mappingJsons/response/on_search.json b/src/common/mappingJsons/response/on_search.json deleted file mode 100644 index 62bbba3f690865e6e9be7991ef2692b8b8baa2a2..0000000000000000000000000000000000000000 --- a/src/common/mappingJsons/response/on_search.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "context": { - "path": "context" - }, - "message.catalog.descriptor.name": { - "path": "title" - } -} diff --git a/src/common/protocol-server.service.ts b/src/common/protocol-server.service.ts deleted file mode 100644 index eb98b925e423252dcbad33ce6562a13bb33cb3f4..0000000000000000000000000000000000000000 --- a/src/common/protocol-server.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IRequestContextStructure_ver_1_1_0 } from "../common/context"; -import axios from "axios"; - -export class ProtocolServer { - bap_client_URL = process.env.BAP_CLIENT_URI as string; - context: IRequestContextStructure_ver_1_1_0; - message: any; - constructor(payload: { - context: IRequestContextStructure_ver_1_1_0; - message: any; - }) { - this.context = payload.context; - this.message = payload.message; - } - async call() { - const payload = { - context: this.context, - message: this.message - }; - return axios.post(`${this.bap_client_URL}/${this.context.action}`, payload); - } -} diff --git a/src/common/schemaObjectGenerator.ts b/src/common/schemaObjectGenerator.ts deleted file mode 100644 index 41df5692d49eac133f31ea7bb6b41237d0cedb7f..0000000000000000000000000000000000000000 --- a/src/common/schemaObjectGenerator.ts +++ /dev/null @@ -1,79 +0,0 @@ -import _ from "lodash"; -export interface IDescriptorOutput { - name?: string; - code?: string; - short_desc?: string; - long_desc?: string; - additional_desc?: { - url?: string; - content_type?: "text/plain" | "text/html" | "application/json"; - }; - media?: { - mimetype?: string; - url?: string; - signature?: string; - dsa?: string; - }[]; - images?: { - url?: string; - size_type?: "xs" | "sm" | "md" | "lg" | "xl" | "custom"; - width?: string; - height?: string; - }[]; -} -interface DescriptorGeneratorInput { - descriptionName?: string; - descriptionCode?: string; - shortDesc?: string; - longDesc?: string; - media?: any[]; - images?: any[]; - descriptionUrl?: string; - descriptionUrlContentType?: string; -} - -export const descriptorGenerator = ( - input: DescriptorGeneratorInput -): IDescriptorOutput => { - let obj: any = { - ...(input?.descriptionName && { name: input?.descriptionName }), - ...(input?.descriptionCode && { code: input?.descriptionCode }), - ...(input?.shortDesc && { short_desc: input?.shortDesc }), - ...(input?.longDesc && { long_desc: input?.longDesc }), - ...(() => { - if (input?.media?.length) { - return { - media: input?.media?.map((medi: any) => ({ - mimetype: medi?.mimetype, - url: medi?.url, - signature: medi?.signature, - dsa: medi?.dsa - })) - }; - } - })(), - ...(() => { - if (input?.images?.length) { - return { - images: input?.images?.map((image: any) => ({ - url: image?.url, - size_type: image?.sizeType, - width: image?.width, - height: image?.height - })) - }; - } - })(), - ...(() => { - if (input?.descriptionUrl) - return { - additional_desc: { - url: input?.descriptionUrl, - content_type: input?.descriptionUrlContentType - } - }; - })() - }; - - return obj; -}; diff --git a/src/common/schemaValidator.ts b/src/common/schemaValidator.ts deleted file mode 100644 index 847c6d67a0a0f44b4f3547e69c7c3b2241f842f0..0000000000000000000000000000000000000000 --- a/src/common/schemaValidator.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Joi from "joi"; -import { Request, Response, NextFunction } from "express"; - -export interface IRequestContext { - domain: string; - bppId?: string; - bppUri?: string; - transactionId: string; - messageId: string; - bapId?: string; - bapUri?: string; - key?: string; -} - -export const requestContextSchema = Joi.object({ - domain: Joi.string().required(), - bppId: Joi.string().optional(), - bppUri: Joi.string().optional(), - transactionId: Joi.string().optional(), - messageId: Joi.string().optional(), - key: Joi.string().optional() -}); - -export const requestSchema = Joi.object({ - context: requestContextSchema.required() -}); - -export const validateRequest = ( - req: Request, - res: Response, - next: NextFunction -) => { - const { error } = requestSchema.validate(req.body, { allowUnknown: true }); - if (error) { - return res.status(400).send({ - message: "Invalid Request", - data: error.message, - success: false - }); - } - return next(); -}; diff --git a/src/common/utils.ts b/src/common/utils.ts deleted file mode 100644 index 1e4863712044b49931145d23915c7bfcfe3eeb72..0000000000000000000000000000000000000000 --- a/src/common/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function removeEmptyObjectKeys(obj: any) { - for (const key in obj) { - if (typeof obj[key] === 'object' && obj[key] !== null) { - removeEmptyObjectKeys(obj[key]); - if (Object.keys(obj[key]).length === 0) { - delete obj[key]; - } - } - } - return obj; -} diff --git a/src/config/config.service.ts b/src/config/config.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..4dfff5094b0e9efe0b88a0e33cbf108820bb6bb7 --- /dev/null +++ b/src/config/config.service.ts @@ -0,0 +1,100 @@ +import { injectable } from 'inversify'; +import 'reflect-metadata'; +import * as dotenv from 'dotenv'; + +@injectable() +export class ConfigService { + private readonly apiKey: string; + private readonly appName; + private readonly appEnv; + private readonly appKey; + private readonly appDebug; + private readonly appPort; + private readonly appUrl; + private readonly psBaseUri; + private readonly psBapId; + private readonly psBapUri; + private readonly psCityName; + private readonly psCityCode; + private readonly psCountryName; + private readonly psCountryCode; + + + constructor() { + // Load environment variables from .env file + dotenv.config(); + + // Retrieve the API_KEY from environment variables + this.apiKey = process.env.APP_KEY || ''; + this.appName = process.env.APP_NAME || ''; + this.appEnv = process.env.APP_ENV || ''; + this.appKey = process.env.APP_KEY || ''; + this.appDebug = process.env.APP_DEBUG || ''; + this.appPort = process.env.APP_PORT || ''; + this.appUrl = process.env.APP_URL || ''; + this.psBaseUri = process.env.PS_BASE_URI || ''; + this.psBapId = process.env.PS_BAP_ID || ''; + this.psBapUri = process.env.PS_BAP_URI || ''; + this.psCityName = process.env.PS_CITY_NAME || ''; + this.psCityCode = process.env.PS_CITY_CODE || ''; + this.psCountryName = process.env.PS_COUNTRY_NAME || ''; + this.psCountryCode = process.env.PS_COUNTRY_CODE || ''; + } + + getApiKey(): string { + return this.apiKey; + } + + getAppName(): string { + return this.appName; + }; + + getAppEnv(): string { + return this.appEnv; + }; + + getAppKey(): string { + return this.appKey; + }; + + getAppDebug(): string { + return this.appDebug; + }; + + getAppPort(): string { + return this.appPort; + }; + + getAppUrl(): string { + return this.appUrl; + }; + + getPsBaseUri(): string { + return this.psBaseUri; + }; + + getPsBapId(): string { + return this.psBapId; + }; + + getPsBapUri(): string { + return this.psBapUri; + }; + + getPsCityName(): string { + return this.psCityName; + }; + + getPsCityCode(): string { + return this.psCityCode; + }; + + getPsCountryName(): string { + return this.psCountryName; + }; + + getPsCountryCode(): string { + return this.psCountryCode; + }; + +} diff --git a/src/gcl/gcl.controller.ts b/src/gcl/gcl.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6e9bfa46f94a69075cb2a8977693faa4a482e9d --- /dev/null +++ b/src/gcl/gcl.controller.ts @@ -0,0 +1,39 @@ +import { inject } from "inversify"; +import { controller, httpPost, requestBody } from "inversify-express-utils"; +import { GCLService } from "./gcl.service"; + +@controller('/') +export class GCLController { + + constructor(@inject(GCLService) private service: GCLService) { } + + @httpPost('search') + public async search(@requestBody() body: any): Promise<any> { + const searchResult = await this.service.search(body); + return searchResult; + } + + @httpPost('select') + public async select(@requestBody() body: any): Promise<any> { + const selectResult = await this.service.select(body); + return selectResult; + } + + @httpPost('init') + public async init(@requestBody() body: any): Promise<any> { + const initResult = await this.service.init(body); + return initResult; + } + + @httpPost('confirm') + public async confirm(@requestBody() body: any): Promise<any> { + const confirmResult = await this.service.confirm(body); + return confirmResult; + } + + @httpPost('status') + public async status(@requestBody() body: any): Promise<any> { + const statusResult = await this.service.status(body); + return statusResult; + } +} diff --git a/src/gcl/gcl.service.ts b/src/gcl/gcl.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a490664626e6085f2730c6d96bd60d3dbd54b396 --- /dev/null +++ b/src/gcl/gcl.service.ts @@ -0,0 +1,55 @@ +import { inject, injectable } from "inversify"; +import { TLService } from "../tl/tl.service"; +import { PSClientService } from "../psclient/psclient.service"; + +@injectable() +export class GCLService { + constructor( + @inject(TLService) private tlService: TLService, + @inject(PSClientService) private psClientService: PSClientService + ) { } + + async search(body: any) { + const payload = await this.tlService.transform(body, "search"); + const psResponse = await this.psClientService.post(payload); + const response = await this.tlService.transform(psResponse, "on_search"); + + return response; + } + + async select(body: any) { + const payload = await this.tlService.transform(body, "select"); + const psResponse = await this.psClientService.postMany(payload); + const response = await this.tlService.transform(psResponse, "on_select"); + + return response; + } + + async init(body: any) { + const payload = await this.tlService.transform(body, "init"); + const psResponse = await this.psClientService.postMany(payload); + const response = await this.tlService.transform(psResponse, "on_init"); + + return response; + } + + async confirm(body: any) { + // const payload = await this.tlService.transform(body, "select"); + // const psResponse = await this.psClientService.postMany(payload); + // const response = await this.tlService.transform(psResponse, "on_select"); + + // return response; + + return "In Progress"; + } + + async status(body: any) { + // const payload = await this.tlService.transform(body, "select"); + // const psResponse = await this.psClientService.postMany(payload); + // const response = await this.tlService.transform(psResponse, "on_select"); + + // return response; + + return "In Progress"; + } +} diff --git a/src/httpclient/http.service.ts b/src/httpclient/http.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..266567786e9200d8ada701233e8de0aef317c64e --- /dev/null +++ b/src/httpclient/http.service.ts @@ -0,0 +1,60 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; +import { inject, injectable } from 'inversify'; +import { AppLogger } from '../app/app.logger'; + +@injectable() +class HttpClient { + private readonly client: AxiosInstance; + + constructor(@inject(AppLogger) private logger: AppLogger) { + this.client = axios.create({ + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Add request interceptor + this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => { + // Modify the request config if needed + logger.info("Making network request: \n%s %s\nHEADERS %s \nDATA %o", config.method?.toUpperCase(), config.url, JSON.stringify(config.headers), JSON.stringify(config.data)); + return config; + }, (error: any) => { + // Handle request error + logger.error("Request error: \n%o", error.data); + return Promise.reject(error); + }); + + // Add response interceptor + this.client.interceptors.response.use((response: AxiosResponse) => { + // Modify the response data if needed + logger.info("Response received: %o\n", JSON.stringify(response.data)); + return response; + }, (error: any) => { + // Handle response error + logger.error("Response error: %o", error.message); + return Promise.reject(error); + }); + } + + async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> { + const response: AxiosResponse<T> = await this.client.get(url, config); + return response.data; + } + + async post<T>(url: string, data: any, config?: AxiosRequestConfig): Promise<T> { + const response: AxiosResponse<T> = await this.client.post(url, data, config); + return response.data; + } + + async put<T>(url: string, data: any, config?: AxiosRequestConfig): Promise<T> { + const response: AxiosResponse<T> = await this.client.put(url, data, config); + return response.data; + } + + async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> { + const response: AxiosResponse<T> = await this.client.delete(url, config); + return response.data; + } +} + +export default HttpClient; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index b0bbbe55b73d2a18ccc46774ba3c41fc35ad0d6a..0000000000000000000000000000000000000000 --- a/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import express, { Express } from "express"; -import { startServer } from "./app/server"; -import dotenv from "dotenv"; -const app: Express = express(); - -dotenv.config(); - -startServer(app); diff --git a/src/inversify/inversify.config.ts b/src/inversify/inversify.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6e05b460ede0d1d096f1dda6836d8de68f2c446 --- /dev/null +++ b/src/inversify/inversify.config.ts @@ -0,0 +1,23 @@ +import { Container } from 'inversify'; +import "reflect-metadata"; +import { ConfigService } from '../config/config.service'; +import { GCLService } from '../gcl/gcl.service'; +import { TLService } from '../tl/tl.service'; +import { InversifyExpressServer } from 'inversify-express-utils'; +import { PSClientService } from '../psclient/psclient.service'; +import { AppLogger } from '../app/app.logger'; +import HttpClient from '../httpclient/http.service'; +import { ErrorHandlerMiddleware } from '../middleware/errorhandler.middleware'; + +const container = new Container(); +const server = new InversifyExpressServer(container); + +container.bind<ConfigService>(ConfigService).to(ConfigService); +container.bind<GCLService>(GCLService).to(GCLService); +container.bind<TLService>(TLService).to(TLService); +container.bind<PSClientService>(PSClientService).to(PSClientService); +container.bind<AppLogger>(AppLogger).to(AppLogger); +container.bind<HttpClient>(HttpClient).to(HttpClient); +container.bind<ErrorHandlerMiddleware>(ErrorHandlerMiddleware).toSelf(); + +export { server, container }; diff --git a/src/middleware/errorhandler.middleware.ts b/src/middleware/errorhandler.middleware.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed854f39a9b822b3f36eb6fae4580a5821cfb4ec --- /dev/null +++ b/src/middleware/errorhandler.middleware.ts @@ -0,0 +1,18 @@ +import { inject, injectable } from 'inversify'; +import { Request, Response, NextFunction } from 'express'; +import { AppLogger } from '../app/app.logger'; + +@injectable() +export class ErrorHandlerMiddleware { + constructor(@inject(AppLogger) private logger: AppLogger) { } + + public handleError(error: Error, req: Request, res: Response, next: NextFunction): void { + this.logger.error("%s", error.stack); + const statusCode = res.statusCode === 200 ? 500 : res.statusCode; + res.status(statusCode).json({ + error: { + message: error.message + }, + }); + } +} diff --git a/src/modules/cancel/controller.ts b/src/modules/cancel/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/cancel/schemaHelper.ts b/src/modules/cancel/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/cancel/service.ts b/src/modules/cancel/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/confirm/controller.ts b/src/modules/confirm/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/confirm/schemaHelper.ts b/src/modules/confirm/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/confirm/service.ts b/src/modules/confirm/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/init/controller.ts b/src/modules/init/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/init/schemaHelper.ts b/src/modules/init/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/init/service.ts b/src/modules/init/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/rating/controller.ts b/src/modules/rating/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/rating/schemaHelper.ts b/src/modules/rating/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/rating/service.ts b/src/modules/rating/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/search/controller.ts b/src/modules/search/controller.ts deleted file mode 100644 index 3a9cdad63f2664d18614a90a32ce18a93b90489f..0000000000000000000000000000000000000000 --- a/src/modules/search/controller.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { searchService } from "./service"; - -export const searchController = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const payloadForBAP = await searchService(req?.body); - return res.status(200).send(payloadForBAP); - } catch (error: any) { - return res.status(400).json({ success: false, message: error.message }); - } -}; diff --git a/src/modules/search/service.ts b/src/modules/search/service.ts deleted file mode 100644 index 3b1d449f7bd17e63b03894be68ee0f5bf7a3015b..0000000000000000000000000000000000000000 --- a/src/modules/search/service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { buildRequestContextVer1_1_0 } from "../../common"; -import * as mapperService from "../../common/mapper.service"; -import { ProtocolServer } from "../../common/protocol-server.service"; - -export const searchService = async (body: any) => { - try { - const context = buildRequestContextVer1_1_0(body?.context, "search"); - const request = await mapperService.map({ ...body, context }, "search"); - console.log(JSON.stringify(request)); - const response = await new ProtocolServer(request).call(); - - return await mapperService.map(response.data, "on_search"); - } catch (error: any) { - console.log(error.response.data.error.data.errors); - } -}; diff --git a/src/modules/select/controller.ts b/src/modules/select/controller.ts deleted file mode 100644 index 9140b1d62e1b8f3864388c2ca5e92d67f32f82c8..0000000000000000000000000000000000000000 --- a/src/modules/select/controller.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { selectService } from "./service"; - -export const selectController = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { - const payloadForBAP = await selectService(req?.body); - return res.status(200).send(payloadForBAP); - } catch (error: any) { - return res.status(400).json({ success: false, message: error.message }); - } -}; diff --git a/src/modules/select/schemaHelper.ts b/src/modules/select/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/select/service.ts b/src/modules/select/service.ts deleted file mode 100644 index bf8c3ad4dfa87d868facf3a02e6eaeddf8ef39b1..0000000000000000000000000000000000000000 --- a/src/modules/select/service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { buildRequestContextVer1_1_0 } from "../../common"; -import * as mapperService from "../../common/mapper.service"; -import { ProtocolServer } from "../../common/protocol-server.service"; - -export const selectService = async (body: any) => { - try { - return await mapperService.map(response.data, "on_select"); // Response will hold response from PS - } catch (error: any) { - console.log(error.response.data.error.data.errors); - } -}; diff --git a/src/modules/status/controller.ts b/src/modules/status/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/status/schemaHelper.ts b/src/modules/status/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/status/service.ts b/src/modules/status/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/support/controller.ts b/src/modules/support/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/support/schemaHelper.ts b/src/modules/support/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/support/service.ts b/src/modules/support/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/track/controller.ts b/src/modules/track/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/track/schemaHelper.ts b/src/modules/track/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/track/service.ts b/src/modules/track/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/update/controller.ts b/src/modules/update/controller.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/update/schemaHelper.ts b/src/modules/update/schemaHelper.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/modules/update/service.ts b/src/modules/update/service.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/psclient/psclient.service.ts b/src/psclient/psclient.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef5d300d79fa804cf1b698d529d062096a72825c --- /dev/null +++ b/src/psclient/psclient.service.ts @@ -0,0 +1,34 @@ +import { inject, injectable } from "inversify"; +import { ConfigService } from "../config/config.service"; +import HttpClient from "../httpclient/http.service"; + +@injectable() +export class PSClientService { + private psBaseUri: string; + + constructor( + @inject(ConfigService) private config: ConfigService, + @inject(HttpClient) private httpClient: HttpClient + ) { + this.psBaseUri = this.config.getPsBaseUri() + } + + private buildUri(action: string) { + return `${this.psBaseUri}/${action}`; + } + + private postPromise(payload: any): Promise<any> { + return this.httpClient.post(this.buildUri(payload.context.action), payload) + } + + async post(payload: any): Promise<any> { + const response = await this.postPromise(payload); + return response; + } + + async postMany(payloads: any[]): Promise<any> { + return await Promise.all(payloads.map((payload: any) => + this.postPromise(payload) + )); + } +} diff --git a/src/tl/tl.helper.ts b/src/tl/tl.helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..470481644bf949073c7bdbce970ec617b108de06 --- /dev/null +++ b/src/tl/tl.helper.ts @@ -0,0 +1,11 @@ +import jsonata from "jsonata"; +import path from 'path'; +import fs from 'fs'; +import appRootPath from 'app-root-path'; +import moment from 'moment'; +import { v4 as uuid } from 'uuid'; + +export const context = async (data: any, action: string) => { + const expression = jsonata(fs.readFileSync(path.join(appRootPath.toString(), `/mappings/context.jsonata`), "utf8")); + return await expression.evaluate(data, { env: process.env, moment, uuid, action }); +} diff --git a/src/tl/tl.service.ts b/src/tl/tl.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..728d19d9c77a84fb15b0bc30c67899a82ee496f5 --- /dev/null +++ b/src/tl/tl.service.ts @@ -0,0 +1,23 @@ +import { inject, injectable } from "inversify"; +import jsonata from "jsonata"; +import path from 'path'; +import appRootPath from "app-root-path"; +import fs from 'fs'; +import { ObjectUtil } from "../util/object.util"; +import * as tlHelpers from './tl.helper' +import { AppLogger } from "../app/app.logger"; + +@injectable() +export class TLService { + constructor(@inject(AppLogger) private logger: AppLogger) { } + async transform(data: any, action: string) { + const expression = jsonata(fs.readFileSync(path.join(appRootPath.toString(), `/mappings/${action}.jsonata`), "utf8")); + + this.logger.info("Transforming %s data: \n%o", action, JSON.stringify(data)); + let transformed = await expression.evaluate(data, { action, ...tlHelpers }); + transformed = ObjectUtil.removeEmptyObjectKeys(transformed) + this.logger.info("Transformed %s data: \n%o", action, JSON.stringify(transformed)); + + return transformed; + } +} diff --git a/src/util/object.util.ts b/src/util/object.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9dd1294a99a2283eb66ef74f1f00cd3102a491e --- /dev/null +++ b/src/util/object.util.ts @@ -0,0 +1,13 @@ +export class ObjectUtil { + + static removeEmptyObjectKeys(obj: any) { + for (const key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + this.removeEmptyObjectKeys(obj[key]); + if (Object.keys(obj[key]).length === 0) delete obj[key]; + } + } + return obj; + } + +} diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tsconfig.json b/tsconfig.json index 05fd2ca086c11389a4844d79c1c1f676c733006b..7c78d7f221fd6d9f9d355543e9844f495b9f4ec0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -100,6 +100,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "experimentalDecorators": true } }