@@ -0,0 +1,3 @@ | |||
node_modules/ | |||
*.js | |||
dist/ |
@@ -0,0 +1,162 @@ | |||
# Product REST API | |||
A REST API designed for a backend code test for Mission Labs. The background in this fake world is a Mission Labs merch store with a product database for the web store. API has all 4 CRUD methods for its products. Authentication protects any write methods with the X-Token header. All methods below are documents and the code base has comments and documentation throughout. | |||
## Setup | |||
Requires node.js 14+, npm, and mongodb | |||
### Installation | |||
```sh | |||
npm install | |||
``` | |||
### MongoDb setup | |||
Make sure you have properly setup a MongoDB instance and its running on the default port. Then run these two commands to insert our test data and set a field as an index. | |||
```sh | |||
mongoimport -d ml-test -c products --jsonArray products.json | |||
mongo localhost/ml-test --eval "db.products.createIndex( { identifier: 1 }, { unique: true } )" | |||
``` | |||
## Usage | |||
To run the server: | |||
```sh | |||
npm run prod | |||
``` | |||
You should see this output | |||
```sh | |||
> ml-crud_api@1.0.0 prod /home/roxie/Projects/ts/mission-labs/code-test | |||
> tsc && node ./dist/index.js | |||
API running @ http://127.0.0.1:8081 | |||
``` | |||
Great! You are now running the REST API server. It is accessible at http://127.0.0.1:8081. For this demo, the secret key token for authentication is `SECRET_KEY`. | |||
## API Docs | |||
The product schema will be as follows... | |||
```json | |||
{ | |||
"identifier": "url-safe-identifier", // REQUIRED - CAN NOT be empty, needs to be url-safe and lowercase | |||
"name": "Example Product", // REQUIRED - CAN NOT be empty | |||
"price": 9000, // REQUIRED - Price is in pence, CAN NOT be negative | |||
"category": "Examples", // REQUIRED - CAN NOT be empty | |||
"sizes": ["Example Sized"] // OPTIONAL - Can be empty | |||
} | |||
``` | |||
Searching is done with identifiers instead of ObjectID ID's to make API more human-readable and accessible. | |||
Items in curly braces are parameters. | |||
### List all products | |||
`GET /api/products` | |||
This endpoint accepts two parameters to filter the output: | |||
- `priceFrom`: Define a minimum price of all products in the search | |||
- `priceTo`: Define a maximum price of all products in the search | |||
All prices are listed as pence for ease of use. | |||
#### Responses | |||
- `200 OK` on success | |||
```json | |||
[ | |||
{ | |||
"identifier": "url-safe-identifier", | |||
"name": "Example Product", | |||
"price": 9000, // Price is in pence | |||
"category": "Examples", | |||
"sizes": ["Example Sized"] | |||
} | |||
] | |||
``` | |||
### Get one product | |||
`GET /api/products/{IDENTIFIER}` | |||
#### Responses | |||
- `200 OK` on success. | |||
- `404 Not Found` if product with identifier doesn't exist. | |||
```json | |||
{ | |||
"identifier": "url-safe-identifier", | |||
"name": "Example Product", | |||
"price": 9000, // Price is in pence | |||
"category": "Examples", | |||
"sizes": ["Example Sized"] | |||
} | |||
``` | |||
### Create new product | |||
`POST /api/products/` | |||
#### Responses | |||
- `200 OK` on success/ | |||
- `400 Bad Request` when trying to create an object with the same identifier as an existing object. | |||
- `401 Unauthorized` on request without header `X-Token` being set to an authorized key. | |||
Posted data should be a raw body with the header `Content-Type` set to `application/json`. Example of the posted JSON would look like this | |||
```json | |||
{ | |||
"identifier": "ml-cool-logo-cup", | |||
"name": "Brand New Mugs", | |||
"price": 500, // Price is in pence | |||
"category": "Liquid Holders" | |||
} | |||
``` | |||
### Update product | |||
`PATCH /api/products/{IDENTIFIER}` | |||
Edits part of a product object (or all) with the data received. | |||
#### Responses | |||
- `200 OK` on success. | |||
- `400 Bad Request` if unable to parse some or all of the data received for updating. | |||
- `401 Unauthorized` on request without header `X-Token` being set to an authorized key. | |||
- `404 Not Found` if product with identifier doesn't exist. | |||
Posted data should be a raw body with the header `Content-Type` set to `application/json`. `identifier` must be an identifier to an already existing product or else response will be `404 Not Found`. Partial updates are allowed when using PATCH. Example of the posted JSON would look like this | |||
```json | |||
// /api/products/mug_1 | |||
{ | |||
"name": "Mugs One!", | |||
} | |||
``` | |||
This would change the name of the item. | |||
### Delete product | |||
`DELETE /api/products/{IDENTIFIER}` | |||
#### Responses | |||
- `204 No Content` on delete success. | |||
- `401 Unauthorized` on request without header `X-Token` being set to an authorized key. | |||
- `404 Not Found` if product with identifier doesn't exist. |
@@ -0,0 +1,24 @@ | |||
{ | |||
"name": "ml-crud_api", | |||
"version": "1.0.0", | |||
"description": "Code test for Mission Labs", | |||
"main": "dist/index.js", | |||
"scripts": { | |||
"dev": "ts-node ./src/index.ts", | |||
"prod": "tsc && node ./dist/index.js" | |||
}, | |||
"author": "Roxie Gibson", | |||
"license": "ISC", | |||
"devDependencies": { | |||
"ts-node": "^8.10.1", | |||
"tslint": "^6.1.2" | |||
}, | |||
"dependencies": { | |||
"@typegoose/typegoose": "^7.1.3", | |||
"@types/express": "^4.17.6", | |||
"@types/mongoose": "^5.7.21", | |||
"express": "^4.17.1", | |||
"mongoose": "^5.9.16", | |||
"typescript": "^3.9.3" | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
[ | |||
{ | |||
"identifier": "ml_t-shirt", | |||
"name": "Mission Labs T-Shirt", | |||
"price": 2000, | |||
"sizes": ["XS", "S", "M", "L", "XL"], | |||
"category": "Merch" | |||
}, | |||
{ | |||
"identifier": "ml_beanie", | |||
"name": "Mission Labs Beanie", | |||
"price": 999, | |||
"sizes": ["OSFA"], | |||
"category": "Merch" | |||
} | |||
] |
@@ -0,0 +1,265 @@ | |||
import express from "express"; | |||
import { Application, NextFunction, Request, Response, json as jsonBodyParser } from "express"; | |||
import { Product, ProductModel } from "./db/models"; | |||
import { QueryOperators, connect as connectDB } from "./db/connection"; | |||
import { post } from "@typegoose/typegoose"; | |||
/** | |||
* Middleware for the server to have a simple authentication system. | |||
* Checks headers to see if "X-Token" is set to "SECRET_KEY". If it isn't, then the server will return a 401 due to lack of authentication. | |||
* @param req - request object | |||
* @param res - response object | |||
* @param next - next function in the chain | |||
*/ | |||
function checkAuthorization(req: Request, res: Response, next: NextFunction) { | |||
const token: string | undefined = req.get("X-Token"); | |||
if (token === "SECRET_KEY") { | |||
next(); | |||
} else { | |||
res.status(401).end(); // Return 401 Unauthorized due to no authentication | |||
} | |||
} | |||
/** | |||
* Converts the integer parameters in the API requests | |||
* @param param - the string parameter passed by the user | |||
* @throws Error if the string cannot be converted into a positive integer and thus can't be used to filter the database query. | |||
*/ | |||
function parseParamInt(param: string): number { | |||
const int = parseInt(param, 10); | |||
if ( isNaN(int) || int < 0 ) { | |||
throw new Error("Param cannot be processed into a valid number"); | |||
} | |||
return int; | |||
} | |||
/** | |||
* Gets all products in the database | |||
* | |||
* This endpoint accepts two parameters to filter the output: | |||
* - `priceFrom`: Define a minimum price of all products in the search | |||
* - `priceTo`: Define a maximum price of all products in the search | |||
* | |||
* ~~Responses~~ | |||
* `200 OK` on success | |||
* @param req - request object | |||
* @param res - response object | |||
*/ | |||
async function getProductsEndpoint(req: Request, res: Response): Promise<void> { | |||
const priceFromStr = req.query.priceFrom; | |||
const priceToStr = req.query.priceTo; | |||
let priceFrom: number = -1; | |||
let priceTo: number = -1; | |||
try { | |||
if (typeof priceFromStr === "string") { | |||
priceFrom = parseParamInt(priceFromStr); | |||
} | |||
if (typeof priceToStr === "string") { | |||
priceTo = parseParamInt(priceToStr); | |||
} | |||
} catch(error) { | |||
// Error processing numbers | |||
return res.status(400).end("One or more parameters could not be processed."); | |||
} | |||
// If these variables are below 0, then they are still the original number set before parsing | |||
const priceFromValid = priceFrom >= 0; | |||
const priceToValid = priceTo >= 0; | |||
if (priceFromValid && priceToValid) { | |||
if (priceFrom > priceTo) { | |||
return res.status(400).end("priceFrom parameter is bigger than priceTo parameter."); | |||
} | |||
} | |||
let options = {}; | |||
if (priceFromValid || priceToValid) { | |||
// If at least one parameter is parseable | |||
const queryOperators: QueryOperators = {}; | |||
if (priceFromValid) { | |||
queryOperators.$gte = priceFrom; | |||
} | |||
if (priceToValid) { | |||
queryOperators.$lte = priceTo; | |||
} | |||
options = {price: queryOperators}; | |||
} | |||
const products = await ProductModel.find(options) as Product[]; | |||
// Filter product objects in query to remove non-useful data | |||
const filteredProducts = products.map(product => { return Product.isValid(product); }); | |||
res.json(filteredProducts); | |||
} | |||
/** | |||
* Gets one product in the database using the unique identifier | |||
* | |||
* ~~Responses~~ | |||
* `200 OK` on success | |||
* `404 Not Found` if product with identifier doesn't exist. | |||
* @param req - request object | |||
* @param res - response object | |||
*/ | |||
async function getProductEndpoint(req: Request, res: Response): Promise<void> { | |||
const product = await ProductModel.findOne({identifier: req.params.identifier}); | |||
if (!product) { | |||
return res.status(404).end(`Product with identifier ${req.params.identifier} could not be found.`); | |||
} | |||
// Run DB output through validator to strip any database specific keys (ObjectID's and version ID's) | |||
const validProduct: Product = Product.isValid(product); | |||
res.json(validProduct); | |||
} | |||
/** | |||
* Creates a product. req.body is processed from middleware ran before this function. Parsed from raw data to a json object. | |||
* | |||
* ~~Responses~~ | |||
* `200 OK` on success | |||
* `400 Bad Request` when trying to create an object with the same identifier as an existing object. | |||
* `401 Unauthorized` on request without header `X-Token` being set to an authorized key. See checkAuthorization middleware function. | |||
* @param req - request object | |||
* @param res - response object | |||
*/ | |||
async function createProductEndpoint(req: Request, res: Response): Promise<void> { | |||
let postData: Product; | |||
try { | |||
postData = Product.isValid(req.body); | |||
} catch (error) { | |||
// Thrown error if data received is not valid and cannot be inserted into DB | |||
return res.status(400).end("Request has invalid data. Cannot be added to database."); | |||
} | |||
const identifierAlreadyExists = await ProductModel.findOne({ | |||
identifier: postData.identifier | |||
}); | |||
if (identifierAlreadyExists) { | |||
return res.status(400).end("Supplied identifier already exists in database of products."); | |||
} | |||
try { | |||
ProductModel.create(postData); | |||
return res.status(200).end("Successfully added product to database."); | |||
} catch (error) { | |||
// Generic error handler for unexpected issue | |||
return res.status(500).end("Something unexpected happened and the server was unable to process your request."); | |||
} | |||
} | |||
// TODO: COMMENT | |||
async function editProductEndpoint(req: Request, res: Response): Promise<void> { | |||
const appendObject = (object1: object, object2: object) => { | |||
return {...object1, ...object2}; | |||
}; | |||
const postData = req.body; | |||
let updateData = {}; | |||
let failedValidation: boolean = false; | |||
const keys: Record<string, any> = { | |||
"identifier": Product.stringIsUrlSafe, | |||
"name": Product.stringIsValid, | |||
"price": Product.numberIsValid, | |||
"category": Product.stringIsValid, | |||
"sizes": Product.arrayIsValid | |||
}; | |||
Object.keys(postData).forEach((key) => { | |||
if (keys.hasOwnProperty(key)) { | |||
// Execute validation function tied to the key in the above object | |||
const value = postData[key]; | |||
const validationFunction = (keys)[key]; | |||
if (validationFunction(value)) { | |||
const newData: Record<string, any> = {}; | |||
newData[key] = value; | |||
updateData = appendObject(updateData, newData); | |||
} else { | |||
// One of the fields could not be validated | |||
failedValidation = true; | |||
} | |||
} | |||
}); | |||
if (failedValidation){ | |||
return res.status(400).end("Could not update product as non-valid data was provided."); | |||
} | |||
if (updateData) { | |||
const updatedProduct = await ProductModel.updateOne({identifier: req.params.identifier}, updateData); | |||
if (updatedProduct) { | |||
res.status(200).end(`${req.params.identifier} was successfully updated.`); | |||
} else { | |||
// No product was found in the database, nothing was updated | |||
res.status(404).end(`${req.params.identifier} could not be found.`); | |||
} | |||
} else { | |||
return res.status(400).end("Could not update product as no valid fields were given."); | |||
} | |||
} | |||
/** | |||
* Deletes the product with the identifier given. | |||
* | |||
* ~~Responses~~ | |||
* `204 No Content` on delete success. | |||
* `401 Unauthorized` on request without header `X-Token` being set to an authorized key. See checkAuthorization middleware function. | |||
* `404 Not Found` if product with identifier doesn't exist. | |||
* @param req - request object | |||
* @param res - response object | |||
*/ | |||
async function deleteProductEndpoint(req: Request, res: Response): Promise<void> { | |||
const product = await ProductModel.findOneAndDelete({identifier: req.params.identifier}); | |||
if (!product) { | |||
// If something was found and deleted, it returns the deleted object | |||
// If no return, then the object wasn't in the database! | |||
return res.status(404).end(`Product with identifier ${req.params.identifier} could not be found.`); | |||
} else { | |||
return res.status(204).end(`${req.params.identifier} has been deleted.`); | |||
} | |||
} | |||
/** | |||
* Defines all endpoints with middleware | |||
* @param server - express Application to register endpoints on. | |||
*/ | |||
function defineEndpoints(server: Application): void { | |||
// REGISTER API ROUTES | |||
const apiBaseEndpoint = "/api/products"; | |||
const specifiedProductEndpoint = `${apiBaseEndpoint}/:identifier`; | |||
// Middleware to parse json in raw body requests | |||
const jsonParser = jsonBodyParser(); | |||
// GET all products | |||
server.get(apiBaseEndpoint, getProductsEndpoint); | |||
// GET specified product | |||
server.get(specifiedProductEndpoint, getProductEndpoint); | |||
// POST create new product | |||
// middleware - Check if client is authorized, Parse JSON from body, then try and create product | |||
server.post(apiBaseEndpoint, [checkAuthorization, jsonParser, createProductEndpoint]); | |||
// PATCH edit specified product | |||
// middleware - Check if client is authorized, Parse JSON from body, then try and edit product | |||
server.patch(specifiedProductEndpoint, [checkAuthorization, jsonParser, editProductEndpoint]); | |||
// DELETE specified product | |||
// middleware - Check if client is authorized, then delete product | |||
server.delete(specifiedProductEndpoint, [checkAuthorization, deleteProductEndpoint]); | |||
} | |||
/** | |||
* Runs REST API server. Makes sure the connection to the mongodb is established. | |||
*/ | |||
export function runServer(): void { | |||
const server = express(); | |||
connectDB(); | |||
defineEndpoints(server); | |||
// Run Server | |||
server.listen(8081, () => { | |||
console.log("API running @ http://127.0.0.1:8081"); | |||
}); | |||
} |
@@ -0,0 +1,40 @@ | |||
import "mongoose"; | |||
import { mongoose } from "@typegoose/typegoose"; | |||
/** | |||
* Connects to the MongoDB database running on the default port. | |||
*/ | |||
export function connect(): void { | |||
const url = "mongodb://localhost:27017/"; | |||
const db = mongoose.connection; | |||
// Exit on error since program relies on db connection | |||
db.on('error', (err) => { | |||
// One weird issue with this is the connection takes a while to timeout | |||
console.error(err); | |||
process.exit(2); | |||
}); | |||
mongoose.connect(url, { | |||
useNewUrlParser: true, | |||
useUnifiedTopology: true, | |||
useCreateIndex: true, | |||
dbName: "ml-test" | |||
}); | |||
} | |||
/** | |||
* Defines the query operators for querying the mongodb. This is to make this a lot easier to pass the object to filter a value. | |||
* More here about it https://docs.mongodb.com/manual/reference/operator/query-logical/ | |||
*/ | |||
export interface QueryOperators { | |||
$eq?: any; | |||
$gt?: any; | |||
$gte?: any; | |||
$in?: any[]; | |||
$lt?: any; | |||
$lte?: any; | |||
$ne?: any; | |||
$nin?: any[]; | |||
} |
@@ -0,0 +1,120 @@ | |||
import { arrayProp, prop, getModelForClass } from '@typegoose/typegoose'; | |||
/** | |||
* @class A interface for the Product schema. The model is derived from the properties and their decorators. | |||
* The class also has a lot of static methods to validate itself and its properties. | |||
*/ | |||
export class Product { | |||
@prop({ required: true, unique: true, dropDupes: true }) | |||
public identifier!: string; | |||
@prop({ required: true }) | |||
public name!: string; | |||
@prop({ required: true }) | |||
public price!: number; | |||
@arrayProp({ items: String }) | |||
public sizes?: string[]; | |||
@prop({ required: true }) | |||
public category!: string; | |||
/** | |||
* Validates sizes array, making sure it either is undefined, or defined. All items have to be strings. | |||
* @param possibleArray - Object to run tests against. Can be anything. | |||
* @returns true or false to whether the object is a valid array | |||
*/ | |||
public static arrayIsValid(possibleArray: any): boolean { | |||
if (Array.isArray(possibleArray)) { | |||
const allItemsStrings = possibleArray.every( (value) => { return typeof value === "string"; }); | |||
if (!allItemsStrings) { | |||
return false; | |||
} else { | |||
return true; | |||
} | |||
} else if (typeof possibleArray === "undefined") { | |||
// No array provided is valid | |||
return true; | |||
} else { | |||
// sizes is not array but also not undefined | |||
return false; | |||
} | |||
} | |||
/** | |||
* Validates strings. Checks if they are a string and that they are not empty. | |||
* @param possibleArray - Object to run tests against. Can be anything. | |||
* @returns true or false to whether the object is a valid string | |||
*/ | |||
public static stringIsValid(possibleString: any): boolean { | |||
if (typeof possibleString === "string") { | |||
// Aka if not empty | |||
if (possibleString.replace(" ", "")) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
/** | |||
* Takes a string and checks if it only contains alphanumeric characters | |||
* @param str - string to check if url safe. Could be anything, hence the check during the function. | |||
* @returns boolean of if string is alphanumeric. If not a string, returns false. | |||
*/ | |||
public static stringIsUrlSafe(str: any): boolean { | |||
if (this.stringIsValid(str)) { | |||
const newString = str.replace(/[^a-z0-9\-\_]/gi, '_').toLowerCase(); | |||
if (newString === str) { return true; } | |||
} | |||
return false; | |||
} | |||
/** | |||
* Takes a string and checks if it only contains alphanumeric characters | |||
* @param str - string to check if url safe. Could be anything, hence the check during the function. | |||
* @returns boolean of if string is alphanumeric. If not a string, returns false. | |||
*/ | |||
public static numberIsValid(int: any): boolean { | |||
if (typeof int === "number") { | |||
if (int >= 0) { return true; } | |||
} | |||
// For simplicity, we will reject strings that can be converted to numbers for now | |||
return false; | |||
} | |||
/** | |||
* Validates object if it can be cast to product interface. | |||
* Checks if all required fields are provided and that the data meets the guidelines. | |||
* If extra fields given, we will just ignore them. Possible issue here because we are failing quietly. | |||
* @param jsonInput - Object to be casted to Product interface | |||
* @returns Product interface with valid fields and data | |||
*/ | |||
public static isValid(jsonInput: Product): Product { | |||
// Check if keys exist and types are correct | |||
const isValid = | |||
this.stringIsUrlSafe(jsonInput.identifier) && | |||
this.stringIsValid(jsonInput.name) && | |||
this.numberIsValid(jsonInput.price) && | |||
this.stringIsValid(jsonInput.category); | |||
const arrayIsValid = this.arrayIsValid(jsonInput.sizes); | |||
if (!isValid || !arrayIsValid) { | |||
throw new Error("POST data is missing required keys or data provided is not valid to the schema."); | |||
} | |||
// Build object to return | |||
const validatedProduct: Product = { | |||
identifier: jsonInput.identifier, | |||
name: jsonInput.name, | |||
price: jsonInput.price, | |||
category: jsonInput.category, | |||
}; | |||
if (arrayIsValid) { | |||
validatedProduct.sizes = jsonInput.sizes; | |||
} | |||
return validatedProduct; | |||
} | |||
} | |||
export const ProductModel = getModelForClass(Product); |
@@ -0,0 +1,4 @@ | |||
import { runServer } from "./api"; | |||
// TODO: Add logging to endpoints and general running for sysadmin | |||
runServer(); |
@@ -0,0 +1,66 @@ | |||
{ | |||
"compilerOptions": { | |||
/* Basic Options */ | |||
// "incremental": true, /* Enable incremental compilation */ | |||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ | |||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ | |||
// "lib": [], /* Specify library files to be included in the compilation. */ | |||
// "allowJs": true, /* Allow javascript files to be compiled. */ | |||
// "checkJs": true, /* Report errors in .js files. */ | |||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ | |||
// "declaration": true, /* Generates corresponding '.d.ts' file. */ | |||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ | |||
// "sourceMap": true, /* Generates corresponding '.map' file. */ | |||
// "outFile": "./", /* Concatenate and emit output to single file. */ | |||
"outDir": "./dist", /* Redirect output structure to the directory. */ | |||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ | |||
// "composite": true, /* Enable project compilation */ | |||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ | |||
// "removeComments": true, /* Do not emit comments to output. */ | |||
// "noEmit": true, /* Do not emit outputs. */ | |||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ | |||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ | |||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ | |||
/* Strict Type-Checking Options */ | |||
"strict": true, /* Enable all strict type-checking options. */ | |||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ | |||
// "strictNullChecks": true, /* Enable strict null checks. */ | |||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ | |||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ | |||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ | |||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ | |||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ | |||
/* Additional Checks */ | |||
// "noUnusedLocals": true, /* Report errors on unused locals. */ | |||
// "noUnusedParameters": true, /* Report errors on unused parameters. */ | |||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ | |||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ | |||
/* Module Resolution Options */ | |||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ | |||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ | |||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ | |||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ | |||
// "typeRoots": [], /* List of folders to include type definitions from. */ | |||
// "types": [], /* Type declaration files to be included in compilation. */ | |||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ | |||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ | |||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ | |||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ | |||
/* Source Map Options */ | |||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ | |||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ | |||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ | |||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ | |||
/* Experimental Options */ | |||
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ | |||
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ | |||
/* Advanced Options */ | |||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ | |||
} | |||
} |
@@ -0,0 +1,12 @@ | |||
{ | |||
"defaultSeverity": "error", | |||
"extends": [ | |||
"tslint:recommended" | |||
], | |||
"jsRules": {}, | |||
"rules": { | |||
"no-console": false, | |||
"semicolon": true | |||
}, | |||
"rulesDirectory": [] | |||
} |