Merge branch 'develop' of Software_Engineering_I/greenvironment-server into master

pull/5/head
Trivernis 5 years ago committed by Gitea
commit 086430f0ba

@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- event and eventCount to UserData gql interface - event and eventCount to UserData gql interface
- joined field to Event gql type - joined field to Event gql type
- joined field to Group gql type - joined field to Group gql type
- rate limits with defaults of 10/min for `/upload` and 30/min for `/graphql`
- complexity limits for graphql queries that can be configured with the `api.maxQueryComplexity` option
### Removed ### Removed
@ -39,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- default findUser param limit to 20 - default findUser param limit to 20
- only group admins can create group events - only group admins can create group events
- config behaviour to use all files that reside in the ./config directory with the .toml format - config behaviour to use all files that reside in the ./config directory with the .toml format
- default response timeout from 2 minutes to 30 seconds
### Fixed ### Fixed

@ -3,17 +3,22 @@
# A connection uri string to the database # A connection uri string to the database
connectionUri = "sqlite://greenvironment.db" connectionUri = "sqlite://greenvironment.db"
# Configuration for the redis connection # Configuration for the redis connection
[redis] [redis]
# A connection uri string to the redis server # A connection uri string to the redis server
connectionUri = "redis://localhost:6379" connectionUri = "redis://localhost:6379"
# Configuration of the http server # Configuration of the http server
[server] [server]
# The port the server is running on # The port the server is running on
port = 8080 port = 8080
# Allow cross origin requests # Allow cross origin requests
cors = false cors = false
# The timeout for a server response
timeout = 30000
# Configuration for the sessions # Configuration for the sessions
[session] [session]
@ -22,19 +27,50 @@ secret = "REPLACE WITH SAFE RANDOM GENERATED SECRET"
# The maximum age of a cookie. The age is reset with every request of the client # The maximum age of a cookie. The age is reset with every request of the client
cookieMaxAge = 6048e+5 # 7 days cookieMaxAge = 6048e+5 # 7 days
# Configuration for markdown rendering # Configuration for markdown rendering
[markdown] [markdown]
# The plugins used in the markdown parser # The plugins used in the markdown parser
plugins = ["markdown-it-emoji"] plugins = ["markdown-it-emoji"]
# Configuration for logging # Configuration for logging
[logging] [logging]
# The loglevel # The loglevel
level = "info" level = "info"
# Configuration for the frontend files # Configuration for the frontend files
[frontend] [frontend]
# The path to the angular index file. Its relative to the public path. # The path to the angular index file. Its relative to the public path.
angularIndex = "index.html" angularIndex = "index.html"
# The path to the public files # The path to the public files
publicPath = "./public" publicPath = "./public"
# Configuration for the api
[api]
# if graphiql should be enabled
graphiql = true
# The maximum complexity of queries
maxQueryComplexity = 5000
# Configuration for the api rate limit
[api.rateLimit]
# rate limit of /upload
[api.rateLimit.upload]
# The time in milliseconds before the rate limit is reset
expire = 60000
# The total number of calls allowed before rate limiting
total = 10
# rate limit of /graphql
[api.rateLimit.graphql]
# The time in milliseconds before the rate limit is reset
expire = 60000
# The total number of calls allowed before rate limiting
total = 30

@ -23,6 +23,7 @@
"@types/bluebird": "^3.5.27", "@types/bluebird": "^3.5.27",
"@types/chai": "^4.2.7", "@types/chai": "^4.2.7",
"@types/compression": "^1.0.1", "@types/compression": "^1.0.1",
"@types/config": "^0.0.36",
"@types/connect-pg-simple": "^4.2.0", "@types/connect-pg-simple": "^4.2.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.6", "@types/cors": "^2.8.6",
@ -32,12 +33,14 @@
"@types/express-session": "^1.15.14", "@types/express-session": "^1.15.14",
"@types/express-socket.io-session": "^1.3.2", "@types/express-socket.io-session": "^1.3.2",
"@types/fs-extra": "^8.0.0", "@types/fs-extra": "^8.0.0",
"@types/graphql-query-complexity": "^0.2.1",
"@types/http-status": "^0.2.30", "@types/http-status": "^0.2.30",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/markdown-it": "0.0.9", "@types/markdown-it": "0.0.9",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/node": "^12.7.12", "@types/node": "^12.7.12",
"@types/pg": "^7.11.0", "@types/pg": "^7.11.0",
"@types/redis": "^2.8.14",
"@types/sequelize": "^4.28.5", "@types/sequelize": "^4.28.5",
"@types/sharp": "^0.23.1", "@types/sharp": "^0.23.1",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
@ -56,7 +59,6 @@
"typescript": "^3.7.2" "typescript": "^3.7.2"
}, },
"dependencies": { "dependencies": {
"@types/config": "^0.0.36",
"compression": "^1.7.4", "compression": "^1.7.4",
"config": "^3.2.4", "config": "^3.2.4",
"connect-session-sequelize": "^6.0.0", "connect-session-sequelize": "^6.0.0",
@ -65,11 +67,13 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.1.6", "express-fileupload": "^1.1.6",
"express-graphql": "^0.9.0", "express-graphql": "^0.9.0",
"express-limiter": "^1.6.1",
"express-session": "^1.16.2", "express-session": "^1.16.2",
"express-socket.io-session": "^1.3.5", "express-socket.io-session": "^1.3.5",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"graphql": "^14.4.2", "graphql": "^14.4.2",
"graphql-import": "^0.7.1", "graphql-import": "^0.7.1",
"graphql-query-complexity": "^0.4.1",
"http-status": "^1.3.2", "http-status": "^1.3.2",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"markdown-it": "^10.0.0", "markdown-it": "^10.0.0",

@ -10,12 +10,17 @@ import sharedsession = require("express-socket.io-session");
import * as fsx from "fs-extra"; import * as fsx from "fs-extra";
import {buildSchema} from "graphql"; import {buildSchema} from "graphql";
import {importSchema} from "graphql-import"; import {importSchema} from "graphql-import";
import queryComplexity, {directiveEstimator, simpleEstimator} from "graphql-query-complexity";
import {IncomingMessage, ServerResponse} from "http";
import * as http from "http"; import * as http from "http";
import * as httpStatus from "http-status"; import * as httpStatus from "http-status";
import * as path from "path"; import * as path from "path";
import {RedisClient} from "redis";
import * as redis from "redis";
import {Sequelize} from "sequelize-typescript"; import {Sequelize} from "sequelize-typescript";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import * as socketIoRedis from "socket.io-redis"; import * as socketIoRedis from "socket.io-redis";
import {query} from "winston";
import {resolver} from "./graphql/resolvers"; import {resolver} from "./graphql/resolvers";
import dataaccess from "./lib/dataAccess"; import dataaccess from "./lib/dataAccess";
import globals from "./lib/globals"; import globals from "./lib/globals";
@ -23,6 +28,7 @@ import HomeRoute from "./routes/HomeRoute";
import {UploadRoute} from "./routes/UploadRoute"; import {UploadRoute} from "./routes/UploadRoute";
const SequelizeStore = require("connect-session-sequelize")(session.Store); const SequelizeStore = require("connect-session-sequelize")(session.Store);
const createLimiter: (...args: any) => any = require("express-limiter");
const logger = globals.logger; const logger = globals.logger;
/** /**
@ -40,6 +46,16 @@ class App {
*/ */
public io: socketIo.Server; public io: socketIo.Server;
/**
* The instance of the redis client
*/
public redisClient: RedisClient;
/**
* The limiter for api requests
*/
public limiter: any;
/** /**
* An instance of the http server where the site is served * An instance of the http server where the site is served
*/ */
@ -63,6 +79,8 @@ class App {
constructor(id?: number) { constructor(id?: number) {
this.id = id; this.id = id;
this.app = express(); this.app = express();
this.redisClient = redis.createClient(null, null, {url: config.get("redis.connectionUri")});
this.limiter = createLimiter(this.app, this.redisClient);
this.server = new http.Server(this.app); this.server = new http.Server(this.app);
this.io = socketIo(this.server); this.io = socketIo(this.server);
this.sequelize = new Sequelize(config.get("database.connectionUri")); this.sequelize = new Sequelize(config.get("database.connectionUri"));
@ -102,6 +120,7 @@ class App {
this.io.use(sharedsession(appSession, {autoSave: true})); this.io.use(sharedsession(appSession, {autoSave: true}));
logger.info("Configuring express app."); logger.info("Configuring express app.");
this.server.setTimeout(config.get("server.timeout"));
this.app.set("views", path.join(__dirname, "views")); this.app.set("views", path.join(__dirname, "views"));
this.app.set("view engine", "pug"); this.app.set("view engine", "pug");
this.app.set("trust proxy", 1); this.app.set("trust proxy", 1);
@ -146,17 +165,54 @@ class App {
await homeRoute.init(this.io); await homeRoute.init(this.io);
this.app.use("/home", homeRoute.router); this.app.use("/home", homeRoute.router);
this.limiter({
expire: config.get("api.rateLimit.upload.expire"),
lookup: ["connection.remoteAddress"],
method: "all",
onRateLimited: (req: IncomingMessage, res: any) => {
res.status(httpStatus.TOO_MANY_REQUESTS);
res.json({error: "Rate Limit Exceeded"});
},
path: "/upload",
total: config.get("api.rateLimit.upload.total"),
});
this.app.use("/upload", uploadRoute.router); this.app.use("/upload", uploadRoute.router);
// listen for graphql requests // listen for graphql requests
this.app.use("/graphql", graphqlHTTP((request, response) => { this.limiter({
expire: config.get("api.rateLimit.graphql.expire"),
lookup: ["connection.remoteAddress"],
method: "all",
onRateLimited: (req: IncomingMessage, res: any) => {
res.status(httpStatus.TOO_MANY_REQUESTS);
res.json({errors: [{message: "Rate Limit Exceeded"}]});
},
path: "/graphql",
total: config.get("api.rateLimit.graphql.total"),
});
// @ts-ignore
this.app.use("/graphql", graphqlHTTP(async (request, response, {variables}) => {
return { return {
// @ts-ignore all // @ts-ignore all
context: {session: request.session}, context: {session: request.session},
graphiql: true, graphiql: config.get("api.graphiql"),
rootValue: resolver(request, response), rootValue: resolver(request, response),
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
validationRules: [
queryComplexity({
estimators: [
directiveEstimator(),
simpleEstimator(),
],
maximumComplexity: config.get("api.maxQueryComplexity"),
onComplete: (complexity: number) => {
logger.debug(`QueryComplexity: ${complexity}`);
},
variables,
}),
],
}; };
})); }));
// allow access to cluster information // allow access to cluster information

@ -1,3 +1,12 @@
"a directive to assign a complexity to a query field"
directive @complexity(
"The complexity value for the field"
value: Int!,
"Optional multipliers"
multipliers: [String!]
) on FIELD_DEFINITION
type Query { type Query {
"returns the user object for a given user id or a handle (only one required)" "returns the user object for a given user id or a handle (only one required)"
getUser(userId: ID, handle: String): User getUser(userId: ID, handle: String): User
@ -18,10 +27,10 @@ type Query {
getRequest(requestId: ID!): Request getRequest(requestId: ID!): Request
"searches for users, groups, events, posts and returns a search result" "searches for users, groups, events, posts and returns a search result"
search(query: String!, first: Int = 20, offset: Int = 0): SearchResult! search(query: String!, first: Int = 20, offset: Int = 0): SearchResult! @complexity(value: 1, multipliers: ["first"])
"returns the post filtered by the sort type with pagination." "returns the post filtered by the sort type with pagination."
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post!]! getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post!]! @complexity(value: 1, multipliers: ["first"])
"returns all activities" "returns all activities"
getActivities: [Activity] getActivities: [Activity]
@ -121,25 +130,25 @@ interface UserData {
postCount: Int! postCount: Int!
"returns a given number of posts of a user" "returns a given number of posts of a user"
posts(first: Int=10, offset: Int=0): [Post!]! posts(first: Int=10, offset: Int=0): [Post!]! @complexity(value: 1, multipliers: ["first"])
"creation date of the user account" "creation date of the user account"
joinedAt: String! joinedAt: String!
"all friends of the user" "all friends of the user"
friends(first: Int=10, offset: Int=0): [User!]! friends(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"The number of friends the user has" "The number of friends the user has"
friendCount: Int! friendCount: Int!
"The groups the user has joined" "The groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group!]! groups(first: Int=10, offset: Int=0): [Group!]! @complexity(value: 1, multipliers: ["first"])
"The number of groups the user has joined" "The number of groups the user has joined"
groupCount: Int! groupCount: Int!
"The events the user is participating in" "The events the user is participating in"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"The number of events the user is participating in" "The number of events the user is participating in"
eventCount: Int! eventCount: Int!
@ -169,7 +178,7 @@ type User implements UserData{
numberOfPosts: Int! numberOfPosts: Int!
"returns a given number of posts of a user" "returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post!]! posts(first: Int=10, offset: Int): [Post!]! @complexity(value: 1, multipliers: ["first"])
"the number of posts the user has created" "the number of posts the user has created"
postCount: Int! postCount: Int!
@ -178,7 +187,7 @@ type User implements UserData{
joinedAt: String! joinedAt: String!
"all friends of the user" "all friends of the user"
friends(first: Int=10, offset: Int=0): [User!]! friends(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"The number of friends the user has" "The number of friends the user has"
friendCount: Int! friendCount: Int!
@ -187,13 +196,13 @@ type User implements UserData{
points: Int! points: Int!
"the groups the user has joined" "the groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group!]! groups(first: Int=10, offset: Int=0): [Group!]! @complexity(value: 1, multipliers: ["first"])
"The numbef of groups the user has joined" "The numbef of groups the user has joined"
groupCount: Int! groupCount: Int!
"The events the user is participating in" "The events the user is participating in"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"The number of events the user is participating in" "The number of events the user is participating in"
eventCount: Int! eventCount: Int!
@ -213,7 +222,7 @@ type Profile implements UserData {
email: String! email: String!
"returns the chatrooms the user joined." "returns the chatrooms the user joined."
chats(first: Int=10, offset: Int): [ChatRoom] chats(first: Int=10, offset: Int): [ChatRoom] @complexity(value: 1, multipliers: ["first"])
"the count of the users chats" "the count of the users chats"
chatCount: Int! chatCount: Int!
@ -231,13 +240,13 @@ type Profile implements UserData {
postCount: Int! postCount: Int!
"returns a given number of posts of a user" "returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post!]! posts(first: Int=10, offset: Int): [Post!]! @complexity(value: 1, multipliers: ["first"])
"creation date of the user account" "creation date of the user account"
joinedAt: String! joinedAt: String!
"all friends of the user" "all friends of the user"
friends(first: Int=10, offset: Int=0): [User!]! friends(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"The number of friends the user has" "The number of friends the user has"
friendCount: Int! friendCount: Int!
@ -255,13 +264,13 @@ type Profile implements UserData {
createdGroups: [Group!]! createdGroups: [Group!]!
"all groups the user has joined" "all groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group!]! groups(first: Int=10, offset: Int=0): [Group!]! @complexity(value: 1, multipliers: ["first"])
"The numbef of groups the user has joined" "The numbef of groups the user has joined"
groupCount: Int! groupCount: Int!
"The events the user is participating in" "The events the user is participating in"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"The number of events the user is participating in" "The number of events the user is participating in"
eventCount: Int! eventCount: Int!
@ -335,10 +344,10 @@ type ChatRoom {
namespace: String namespace: String
"the members of the chatroom" "the members of the chatroom"
members(first: Int=10, offset: Int=0): [User!]! members(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"return a specfic range of messages posted in the chat" "return a specfic range of messages posted in the chat"
messages(first: Int = 10, offset: Int): [ChatMessage!]! messages(first: Int = 10, offset: Int): [ChatMessage!]! @complexity(value: 1, multipliers: ["first"])
"id of the chat" "id of the chat"
id: ID! id: ID!
@ -375,16 +384,16 @@ type Group {
creator: User! creator: User!
"all admins of the group" "all admins of the group"
admins(first: Int=10, offset: Int=0): [User!]! admins(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"the members of the group with pagination" "the members of the group with pagination"
members(first: Int = 10, offset: Int = 0): [User!]! members(first: Int = 10, offset: Int = 0): [User!]! @complexity(value: 1, multipliers: ["first"])
"the groups chat" "the groups chat"
chat: ChatRoom! chat: ChatRoom!
"the events of the group" "the events of the group"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"If the user with the specified id has joined the group" "If the user with the specified id has joined the group"
joined(userId: Int): Boolean! joined(userId: Int): Boolean!
@ -404,7 +413,7 @@ type Event {
group: Group! group: Group!
"The participants of the event." "The participants of the event."
participants(first: Int=10, offset: Int=0): [User!]! participants(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"Returns if the user with the specified id has joined the event" "Returns if the user with the specified id has joined the event"
joined(userId: Int): Boolean joined(userId: Int): Boolean

File diff suppressed because it is too large Load Diff

@ -152,6 +152,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/graphql-query-complexity@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@types/graphql-query-complexity/-/graphql-query-complexity-0.2.1.tgz#5166c7f32b6cd0a24f1aad5e00ca513b82b7f0e0"
integrity sha512-PxYhF92UFagAl9UIep8seEUd9j18JardL9ZM9tOfP02fWot9ZlkBYYGFwSZ7fRE6HTva/Yr4BQem7b4P/TgDPA==
dependencies:
graphql-query-complexity "*"
"@types/http-status@^0.2.30": "@types/http-status@^0.2.30":
version "0.2.30" version "0.2.30"
resolved "https://registry.yarnpkg.com/@types/http-status/-/http-status-0.2.30.tgz#b43a1e1673b6ed9b5a28e8647862b51b6473634d" resolved "https://registry.yarnpkg.com/@types/http-status/-/http-status-0.2.30.tgz#b43a1e1673b6ed9b5a28e8647862b51b6473634d"
@ -212,6 +219,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/redis@^2.8.14":
version "2.8.14"
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.14.tgz#2ed46d0f923f7ccd63fbe73a46a1241e606cf716"
integrity sha512-255dzsOLJdXFHBio9/aMHGozNkoiBUgc+g2nlNjbTSp5qcAlmpm4Z6Xs3pKOBLNIKdZbA2BkUxWvYSIwKra0Yw==
dependencies:
"@types/node" "*"
"@types/sequelize@^4.28.5": "@types/sequelize@^4.28.5":
version "4.28.6" version "4.28.6"
resolved "https://registry.yarnpkg.com/@types/sequelize/-/sequelize-4.28.6.tgz#01d2f1d3781cc34448cd63c2fd97bdb0612b15de" resolved "https://registry.yarnpkg.com/@types/sequelize/-/sequelize-4.28.6.tgz#01d2f1d3781cc34448cd63c2fd97bdb0612b15de"
@ -1745,6 +1759,11 @@ express-graphql@^0.9.0:
http-errors "^1.7.3" http-errors "^1.7.3"
raw-body "^2.4.1" raw-body "^2.4.1"
express-limiter@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/express-limiter/-/express-limiter-1.6.1.tgz#70ede144f9e875e3c7e120644018a6f7784bda71"
integrity sha512-w/Xz/FIHuAOIVIUeHSe6g2rSYTqCSKA9WFLO2CxX15BzEAK+avp7HoYd7pu/M2tEp5E/to253+4x8vQ6WcTJkQ==
express-session@^1.16.2: express-session@^1.16.2:
version "1.17.0" version "1.17.0"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.0.tgz#9b50dbb5e8a03c3537368138f072736150b7f9b3" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.0.tgz#9b50dbb5e8a03c3537368138f072736150b7f9b3"
@ -2261,6 +2280,13 @@ graphql-import@^0.7.1:
lodash "^4.17.4" lodash "^4.17.4"
resolve-from "^4.0.0" resolve-from "^4.0.0"
graphql-query-complexity@*, graphql-query-complexity@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/graphql-query-complexity/-/graphql-query-complexity-0.4.1.tgz#06ad49de617da0d74c8196fb4a641349f104552d"
integrity sha512-Uo87hNlnJ5jwoWBkVYITbJpTrlCVwgfG5Wrfel0K1/42G+3xvud31CpsprAwiSpFIP+gCqttAx7OVmw4eTqLQQ==
dependencies:
lodash.get "^4.4.2"
graphql@^14.4.2, graphql@^14.5.3: graphql@^14.4.2, graphql@^14.5.3:
version "14.5.8" version "14.5.8"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c"
@ -3028,6 +3054,11 @@ locate-path@^3.0.0:
p-locate "^3.0.0" p-locate "^3.0.0"
path-exists "^3.0.0" path-exists "^3.0.0"
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4: lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4:
version "4.17.15" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"

Loading…
Cancel
Save