├── .gitignore ├── README.md ├── api.Dockerfile ├── api.entrypoint.sh ├── doc ├── ER_diagram.png └── table_design.png ├── docker-compose.yml ├── package.json ├── src ├── application │ ├── comment-app-service.ts │ ├── follow-app-service.ts │ ├── serializer.ts │ ├── tweet-app-service.ts │ └── user-app-service.ts ├── config.ts ├── domain │ ├── comment.ts │ ├── follow.ts │ ├── id.ts │ ├── repository.ts │ ├── time.ts │ ├── tweet.ts │ └── user.ts ├── infrastructure │ ├── aws.ts │ ├── comment-repository.ts │ ├── follow-repository.ts │ ├── tweet-repository.ts │ └── user-repository.ts ├── logger.ts ├── presentation │ └── api-server.ts └── tasks │ ├── db-init.ts │ ├── db-seed.ts │ └── run-api-server.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /logs 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scalable-twitter 2 | 3 | A highly scalable API server that can withstand over 10 million users. This API server is an SNS like Twitter. The following functions are implemented. 4 | 5 | - Post a tweet 6 | - Post a comment on tweet 7 | - Follow a user 8 | - Show tweets of followees on the timeline 9 | 10 | ER Diagram is following. 11 | 12 | ![ER Diagram](./doc/ER_diagram.png) 13 | 14 | Details are written in [this article](https://dev.to/koukikitamura/server-can-handle-10-million-users-10h8). 15 | 16 | ## Requirements 17 | 18 | - Docker 19 | 20 | ## API Server Dependencies 21 | 22 | - Node 16.14 23 | - Express 4.17.3 24 | - DynamoDB 2012-08-10 25 | 26 | ## Usage 27 | 28 | The following command starts the server and database. 29 | 30 | ``` 31 | docker-compose up 32 | ``` 33 | 34 | ## Software Design 35 | 36 | This project is designed based on DDD(Domain Driven Development). 37 | 38 | | Directory Name | DDD Layer | Components | 39 | | ------------------ | -------------------- | -------------------------------------------- | 40 | | src/domain | Domain Layer | Entity / Value Object / Repository Interface | 41 | | src/application | Application Layer | Application Service / Serializer | 42 | | src/infrastructure | Infrastructure Layer | Repository / AWS Config | 43 | | src/presentation | Presentation Layer | API Server | 44 | 45 | ## DynamoDB Design 46 | 47 | The design of the table and index is as follows. 48 | 49 | ![Table Design](./doc//table_design.png) 50 | 51 | The use cases and query conditions are as follows. 52 | 53 | | Entity | UseCase | Parameters | Table / Index | Key Condition | 54 | | ------- | ------------------------ | ---------- | ------------- | ------------------------------------------------------------------- | 55 | | Tweet | getTimelineByUserId | { UserId } | Primary Key | GetItem (ID=UserId AND begins_with(DataType, timeline)) | 56 | | User | getUserByUserName | {Username} | GSI-1 | Query (DataValue=Username AND DataType=usserProfile) | 57 | | Follow | getFolloweesByUserId | {UserId} | Primary key | Query (ID=userId AND begins_with(DataType, followee) | 58 | | Follow | getFollowersByUserId | {UserId} | Primary Key | Query (ID=userId AND begins_with(DataType, follower) | 59 | | Follow | getCountFoloweeByUserId | {UserId} | Primary Key | Select COUNT / Query (ID=userId AND begins_with(DataType, followee) | 60 | | Follow | getcountFollowerByUsreId | {UserId} | Primary Key | Select COUNT / Query (ID=userId AND begins_with(DataType, follower) | 61 | | Tweet | getTweetsByUserId | {UserId} | Primary Key | Query(ID=userId AND begins_with(DataType, tweet) | 62 | | Tweet | getTweetByTweetId | {TweetId} | GSI-1 | Query(DataValue=tweetId AND begins_with(DataType, tweet) | 63 | | Comment | getCommentsByTweetId | {TweetId} | GSI-1 | Query(DataValue=tweetId AND begins_with(DataType, comment) | 64 | -------------------------------------------------------------------------------- /api.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.14 2 | 3 | WORKDIR /app 4 | 5 | ENTRYPOINT [ "./api.entrypoint.sh" ] 6 | -------------------------------------------------------------------------------- /api.entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | yarn install 6 | yarn db:init 7 | yarn db:seed 8 | 9 | exec "$@" 10 | -------------------------------------------------------------------------------- /doc/ER_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koukikitamura/scalable-twitter/26725bcf53912b108ed67533ee3359de89fc4e47/doc/ER_diagram.png -------------------------------------------------------------------------------- /doc/table_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koukikitamura/scalable-twitter/26725bcf53912b108ed67533ee3359de89fc4e47/doc/table_design.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | x-api: &api 4 | build: 5 | context: . 6 | dockerfile: api.Dockerfile 7 | volumes: 8 | - .:/app 9 | environment: 10 | AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-fakeMyKeyId} 11 | AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-fakeSecretAccessKey} 12 | 13 | 14 | services: 15 | api: 16 | <<: *api 17 | ports: 18 | - '7000:7000' 19 | command: ['yarn', 'dev'] 20 | dynamodb-local: 21 | image: amazon/dynamodb-local:latest 22 | ports: 23 | - '8000:8000' 24 | dynamodb-admin: 25 | image: aaronshaf/dynamodb-admin:latest 26 | tty: true 27 | environment: 28 | DYNAMO_ENDPOINT: http://dynamodb-local:8000 29 | AWS_REGION: "ap-northeast-1" 30 | AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-fakeMyKeyId} 31 | AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-fakeSecretAccessKey} 32 | ports: 33 | - '8001:8001' 34 | depends_on: 35 | - dynamodb-local 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scalable-twitter", 3 | "version": "1.0.0", 4 | "repository": "ssh://git@github.com/koukikitamura/scalable-twitter.git", 5 | "author": "koki kitamura ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/tasks/run-api-server.ts", 9 | "db:init": "ts-node -r tsconfig-paths/register ./src/tasks/db-init.ts ", 10 | "db:seed": "ts-node -r tsconfig-paths/register ./src/tasks/db-seed.ts " 11 | }, 12 | "dependencies": { 13 | "aws-sdk": "^2.1111.0", 14 | "express": "^4.17.3", 15 | "morgan": "^1.10.0", 16 | "ts-node": "^10.7.0", 17 | "winston": "^3.7.2" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^4.17.13", 21 | "ts-node-dev": "^1.1.8", 22 | "tsconfig-paths": "^3.14.1", 23 | "typescript": "^4.6.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/application/comment-app-service.ts: -------------------------------------------------------------------------------- 1 | import { postComment } from "@src/domain/comment"; 2 | import { logger } from "@src/logger"; 3 | import { 4 | ICommentRepository, 5 | ITweetRepository, 6 | IUserRepository, 7 | } from "@src/domain/repository"; 8 | import { serializeComment } from "./serializer"; 9 | 10 | export class CommentAppService { 11 | constructor( 12 | private userRepository: IUserRepository, 13 | private tweetRepository: ITweetRepository, 14 | private commentRepository: ICommentRepository 15 | ) {} 16 | 17 | async post(userId: number, tweetId: number, content: string) { 18 | const user = await this.userRepository.findById(userId); 19 | const tweet = await this.tweetRepository.findById(tweetId); 20 | if (!user) { 21 | logger.debug("User is not found"); 22 | return null; 23 | } 24 | 25 | if (!tweet) { 26 | logger.debug("Tweet is not found"); 27 | return null; 28 | } 29 | 30 | const comment = await postComment(user, tweet, content); 31 | if (comment.isInvalid()) { 32 | return null; 33 | } 34 | 35 | await this.commentRepository.create(comment); 36 | 37 | return serializeComment(comment); 38 | } 39 | 40 | async getAll(tweetId: number) { 41 | const comments = await this.commentRepository.findAllByTweetId(tweetId); 42 | 43 | return comments.map((c) => serializeComment(c)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/application/follow-app-service.ts: -------------------------------------------------------------------------------- 1 | import { followUser } from "@src/domain/follow"; 2 | import { logger } from "@src/logger"; 3 | import { IFollowRepository, IUserRepository } from "@src/domain/repository"; 4 | 5 | export class FollowAppService { 6 | constructor( 7 | private userRepository: IUserRepository, 8 | private followRepository: IFollowRepository 9 | ) {} 10 | 11 | async follow(followeeId: number, followerId: number): Promise { 12 | const followee = await this.userRepository.findById(followeeId); 13 | const follower = await this.userRepository.findById(followerId); 14 | 15 | if (!followee) { 16 | logger.debug(`folowee is not found. foloweeId is ${followeeId}`); 17 | return false; 18 | } 19 | if (!follower) { 20 | logger.debug(`folower is not found. folowerId is ${followerId}`); 21 | return false; 22 | } 23 | 24 | const follow = followUser(followee, follower); 25 | await this.followRepository.create(follow); 26 | 27 | return true; 28 | } 29 | 30 | async unfollow(followeeId: number, followerId: number): Promise { 31 | const followee = await this.userRepository.findById(followeeId); 32 | const follower = await this.userRepository.findById(followerId); 33 | 34 | if (!followee) { 35 | logger.debug(`folowee is not found. foloweeId is ${followeeId}`); 36 | return false; 37 | } 38 | if (!follower) { 39 | logger.debug(`folower is not found. folowerId is ${followerId}`); 40 | return false; 41 | } 42 | 43 | const follow = followUser(followee, follower); 44 | await this.followRepository.delete(follow); 45 | 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/application/serializer.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from "@src/domain/comment"; 2 | import { Tweet } from "@src/domain/tweet"; 3 | import { User } from "@src/domain/user"; 4 | 5 | export const serializeUser = (entity: User) => { 6 | return { 7 | id: entity.id, 8 | username: entity.username, 9 | avatarURL: entity.avatarURL, 10 | introduction: entity.introduction, 11 | registerDate: entity.registerDate.toISOString(), 12 | }; 13 | }; 14 | 15 | export const serializeTweet = (entity: Tweet) => { 16 | return { 17 | id: entity.id, 18 | userId: entity.userId, 19 | content: entity.content, 20 | postDate: entity.postDate.toISOString(), 21 | }; 22 | }; 23 | 24 | export const serializeComment = (entity: Comment) => { 25 | return { 26 | id: entity.id, 27 | userId: entity.userId, 28 | tweetId: entity.tweetId, 29 | content: entity.content, 30 | postDate: entity.postDate, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/application/tweet-app-service.ts: -------------------------------------------------------------------------------- 1 | import { postTweet } from "@src/domain/tweet"; 2 | import { logger } from "@src/logger"; 3 | import { ITweetRepository, IUserRepository } from "@src/domain/repository"; 4 | import { serializeTweet } from "./serializer"; 5 | 6 | export class TweetAppService { 7 | constructor( 8 | private userRepository: IUserRepository, 9 | private tweetRepository: ITweetRepository 10 | ) {} 11 | 12 | async post(userId: number, content: string) { 13 | const user = await this.userRepository.findById(userId); 14 | 15 | if (!user) { 16 | logger.debug("User is not found"); 17 | return null; 18 | } 19 | 20 | const tweet = postTweet(user, content); 21 | if (tweet.isInValid()) { 22 | return null; 23 | } 24 | 25 | await this.tweetRepository.create(tweet); 26 | return serializeTweet(tweet); 27 | } 28 | 29 | async getAll(userId: number) { 30 | const tweets = await this.tweetRepository.findAllByUserId(userId); 31 | 32 | return tweets.map((t) => serializeTweet(t)); 33 | } 34 | 35 | async getOne(tweetId: number) { 36 | const tweet = await this.tweetRepository.findById(tweetId); 37 | 38 | if (!tweet) { 39 | return tweet; 40 | } 41 | 42 | return serializeTweet(tweet); 43 | } 44 | 45 | async getTimeline(userId: number) { 46 | const tweets = await this.tweetRepository.getTimeline(userId); 47 | 48 | return tweets.map((t) => serializeTweet(t)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/application/user-app-service.ts: -------------------------------------------------------------------------------- 1 | import { IFollowRepository, IUserRepository } from "@src/domain/repository"; 2 | import { checkDuplication, createUserAccount } from "@src/domain/user"; 3 | import { logger } from "@src/logger"; 4 | import { serializeUser } from "./serializer"; 5 | 6 | export class UserAppService { 7 | constructor( 8 | private userRepository: IUserRepository, 9 | private followRepository: IFollowRepository 10 | ) {} 11 | 12 | async register(username: string, avatarURL: string, introduction: string) { 13 | const user = createUserAccount(username, avatarURL, introduction); 14 | 15 | if (user.isInValid()) { 16 | return null; 17 | } 18 | 19 | if (await checkDuplication(this.userRepository, user)) { 20 | logger.debug("User is duplicated"); 21 | return null; 22 | } 23 | 24 | this.userRepository.create(user); 25 | 26 | return serializeUser(user); 27 | } 28 | 29 | async getUserProfile(username: string) { 30 | const user = await this.userRepository.findByUsername(username); 31 | if (!user) { 32 | return null; 33 | } 34 | 35 | const foloweeCount = await this.followRepository.countFollowee(user.id); 36 | const folowerCount = await this.followRepository.countFollower(user.id); 37 | 38 | return { 39 | ...serializeUser(user), 40 | foloweeCount, 41 | folowerCount, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | DatabaseTableName: `twitter`, 3 | snowflakeWorkerId: 4 | parseInt(process.env["SNOWFLAKE_WORKER_ID"] as string) || 1, 5 | }; 6 | -------------------------------------------------------------------------------- /src/domain/comment.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@src/logger"; 2 | import { IdGenerator } from "./id"; 3 | import { Tweet } from "./tweet"; 4 | import { User } from "./user"; 5 | 6 | export class Comment { 7 | public id: number; 8 | public userId: number; 9 | public tweetId: number; 10 | public content: string; 11 | public postDate: Date; 12 | 13 | constructor( 14 | id: number, 15 | userId: number, 16 | tweetId: number, 17 | content: string, 18 | postDate: Date 19 | ) { 20 | this.id = id; 21 | this.userId = userId; 22 | this.tweetId = tweetId; 23 | this.content = content; 24 | this.postDate = postDate; 25 | } 26 | 27 | public isInvalid() { 28 | let invalid = false; 29 | 30 | if (this.content.length > 140) { 31 | logger.debug("content should be less than or equal to 15 characters"); 32 | invalid = true; 33 | } 34 | 35 | return invalid; 36 | } 37 | } 38 | 39 | const commentIdGenerator = new IdGenerator(); 40 | 41 | export const postComment = (user: User, tweet: Tweet, content: string) => { 42 | return new Comment( 43 | commentIdGenerator.generate(), 44 | user.id, 45 | tweet.id, 46 | content, 47 | new Date() 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/domain/follow.ts: -------------------------------------------------------------------------------- 1 | import { IdGenerator } from "./id"; 2 | import { User } from "./user"; 3 | 4 | export class Follow { 5 | public id: number; 6 | public followee: User; 7 | public follower: User; 8 | 9 | constructor(id: number, followee: User, follower: User) { 10 | this.id = id; 11 | this.followee = followee; 12 | this.follower = follower; 13 | } 14 | } 15 | 16 | const followIdGenerator = new IdGenerator(); 17 | 18 | export const followUser = (followee: User, follower: User): Follow => { 19 | return new Follow(followIdGenerator.generate(), followee, follower); 20 | }; 21 | -------------------------------------------------------------------------------- /src/domain/id.ts: -------------------------------------------------------------------------------- 1 | import { config } from "@src/config"; 2 | import { dateToUnixTime } from "./time"; 3 | 4 | const workerIDBits = 10; 5 | const sequenceBits = 12; 6 | 7 | // Use snowflake 8 | // See: https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake 9 | export class IdGenerator { 10 | private workerId: number; 11 | private lastGenerateAt: number; 12 | private sequence: number; 13 | 14 | constructor(workerId?: number) { 15 | this.workerId = config.snowflakeWorkerId; 16 | this.lastGenerateAt = dateToUnixTime(new Date()); 17 | this.sequence = 0; 18 | } 19 | generate(): number { 20 | const now = dateToUnixTime(new Date()); 21 | 22 | if (now == this.lastGenerateAt) { 23 | this.sequence++; 24 | } else { 25 | this.sequence = 0; 26 | } 27 | this.lastGenerateAt = now; 28 | 29 | // The bit operators ('<<' and '|' ) can handle numbers within 30 | // the range of signed 32 bit integer. 31 | return ( 32 | now * 2 ** (workerIDBits + sequenceBits) + 33 | this.workerId * 2 ** sequenceBits + 34 | this.sequence 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/domain/repository.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from "@src/domain/comment"; 2 | import { Follow } from "@src/domain/follow"; 3 | import { Tweet } from "@src/domain/tweet"; 4 | import { User } from "@src/domain/user"; 5 | 6 | export interface IUserRepository { 7 | create(entity: User): Promise; 8 | findById(id: number): Promise; 9 | findByUsername(username: string): Promise; 10 | } 11 | 12 | export interface IFollowRepository { 13 | create(entity: Follow): Promise; 14 | delete(entity: Follow): Promise; 15 | countFollowee(userId: number): Promise; 16 | countFollower(userId: number): Promise; 17 | } 18 | 19 | export interface ITweetRepository { 20 | create(entity: Tweet): Promise; 21 | getTimeline(userId: number): Promise; 22 | findById(tweetId: number): Promise; 23 | findAllByUserId(userId: number): Promise; 24 | } 25 | 26 | export interface ICommentRepository { 27 | create(entity: Comment): Promise; 28 | findAllByTweetId(tweetId: number): Promise; 29 | } 30 | -------------------------------------------------------------------------------- /src/domain/time.ts: -------------------------------------------------------------------------------- 1 | export const dateToUnixTime = (date: Date) => Math.floor(date.getTime() / 1000); 2 | 3 | export const unixTimeToDate = (unixTime: number) => new Date(unixTime * 1000); 4 | -------------------------------------------------------------------------------- /src/domain/tweet.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@src/logger"; 2 | import { IdGenerator } from "./id"; 3 | import { User } from "./user"; 4 | 5 | export class Tweet { 6 | public id: number; 7 | public userId: number; 8 | public content: string; 9 | public postDate: Date; 10 | 11 | constructor(id: number, userId: number, content: string, postDate: Date) { 12 | this.id = id; 13 | this.userId = userId; 14 | this.content = content; 15 | this.postDate = postDate; 16 | } 17 | 18 | public isInValid() { 19 | let invalid = false; 20 | 21 | if (this.content.length > 140) { 22 | logger.debug("content should be less than or equal to 15 characters"); 23 | invalid = true; 24 | } 25 | 26 | return invalid; 27 | } 28 | } 29 | 30 | const tweetIdGenerator = new IdGenerator(); 31 | 32 | export const postTweet = (user: User, content: string) => { 33 | return new Tweet(tweetIdGenerator.generate(), user.id, content, new Date()); 34 | }; 35 | -------------------------------------------------------------------------------- /src/domain/user.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@src/logger"; 2 | import { IdGenerator } from "./id"; 3 | import { IUserRepository } from "./repository"; 4 | 5 | export class User { 6 | public id: number; 7 | public username: string; 8 | public avatarURL: string; 9 | public introduction: string; 10 | public registerDate: Date; 11 | 12 | constructor( 13 | id: number, 14 | username: string, 15 | avatarURL: string, 16 | introduction: string, 17 | registerDate: Date 18 | ) { 19 | this.id = id; 20 | this.username = username; 21 | this.avatarURL = avatarURL; 22 | this.introduction = introduction; 23 | this.registerDate = registerDate; 24 | } 25 | 26 | public isInValid(): boolean { 27 | let invalid = false; 28 | 29 | if (this.username.length > 15) { 30 | logger.debug("username should be less than or equal to 15 characters"); 31 | invalid = true; 32 | } 33 | 34 | if (this.introduction.length > 160) { 35 | logger.debug( 36 | "introduction should be less than or equal to 160 characters" 37 | ); 38 | invalid = true; 39 | } 40 | 41 | return invalid; 42 | } 43 | } 44 | 45 | const userIdGenerator = new IdGenerator(); 46 | 47 | export const createUserAccount = ( 48 | username: string, 49 | avatarURL: string, 50 | introduction: string 51 | ): User => { 52 | return new User( 53 | userIdGenerator.generate(), 54 | username, 55 | avatarURL, 56 | introduction, 57 | new Date() 58 | ); 59 | }; 60 | 61 | export const checkDuplication = async ( 62 | userRepository: IUserRepository, 63 | user: User 64 | ) => { 65 | const duplicated = !!(await userRepository.findByUsername(user.username)); 66 | 67 | return duplicated; 68 | }; 69 | -------------------------------------------------------------------------------- /src/infrastructure/aws.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | 3 | export const initAWS = () => { 4 | AWS.config.update({ 5 | region: "ap-northeast-1", 6 | dynamodb: { 7 | apiVersion: "2012-08-10", 8 | endpoint: `http://dynamodb-local:8000`, 9 | }, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/infrastructure/comment-repository.ts: -------------------------------------------------------------------------------- 1 | import { ICommentRepository } from "@src/domain/repository"; 2 | import { Comment } from "@src/domain/comment"; 3 | import { dateToUnixTime, unixTimeToDate } from "@src/domain/time"; 4 | import { logger } from "@src/logger"; 5 | import * as AWS from "aws-sdk"; 6 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 7 | import { config } from "@src/config"; 8 | export class CommentRepository implements ICommentRepository { 9 | async create(entity: Comment): Promise { 10 | const ddb = new AWS.DynamoDB.DocumentClient(); 11 | 12 | const params: DocumentClient.PutItemInput = { 13 | TableName: config.DatabaseTableName, 14 | Item: { 15 | ID: entity.userId, 16 | DataType: `comment#${entity.id}`, 17 | DataValue: entity.tweetId.toString(), 18 | commentId: entity.id, 19 | commentContent: entity.content, 20 | commentPostDate: dateToUnixTime(entity.postDate), 21 | }, 22 | }; 23 | 24 | await ddb.put(params).promise(); 25 | logger.debug(`PutItem: ${JSON.stringify(params)}}`); 26 | } 27 | 28 | async findAllByTweetId(tweetId: number): Promise { 29 | const ddb = new AWS.DynamoDB.DocumentClient(); 30 | 31 | const params: DocumentClient.QueryInput = { 32 | TableName: config.DatabaseTableName, 33 | IndexName: "DataValue-index", 34 | KeyConditionExpression: 35 | "DataValue=:tweetId AND begins_with(DataType, :datatypePrefix)", 36 | ExpressionAttributeValues: { 37 | ":tweetId": tweetId.toString(), 38 | ":datatypePrefix": `comment`, 39 | }, 40 | }; 41 | 42 | const result = await ddb.query(params).promise(); 43 | logger.debug(`Query: ${JSON.stringify(params)}}`); 44 | 45 | if (result.Items) { 46 | return result.Items.map( 47 | (i) => 48 | new Comment( 49 | i["CommentId"], 50 | i["ID"], 51 | parseInt(i["DataValue"]), 52 | i["commentContent"], 53 | unixTimeToDate(i["commentPostDate"]) 54 | ) 55 | ); 56 | } else { 57 | return []; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/infrastructure/follow-repository.ts: -------------------------------------------------------------------------------- 1 | import { IFollowRepository } from "@src/domain/repository"; 2 | import { Follow } from "@src/domain/follow"; 3 | import { dateToUnixTime } from "@src/domain/time"; 4 | import { logger } from "@src/logger"; 5 | import * as AWS from "aws-sdk"; 6 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 7 | import { config } from "@src/config"; 8 | 9 | export class FollowRepository implements IFollowRepository { 10 | async create(entity: Follow): Promise { 11 | const ddb = new AWS.DynamoDB.DocumentClient(); 12 | const followee = entity.followee; 13 | const follower = entity.follower; 14 | 15 | const followeePutItem = { 16 | ID: follower.id, 17 | DataType: `followee#${entity.id}`, 18 | DataValue: followee.id.toString(), 19 | Username: followee.username, 20 | AvatarURL: followee.avatarURL, 21 | Introduction: followee.introduction, 22 | RegisterDate: dateToUnixTime(followee.registerDate), 23 | }; 24 | 25 | const followerPutItem = { 26 | ID: followee.id, 27 | DataType: `follower#${entity.id}`, 28 | DataValue: follower.id.toString(), 29 | Username: follower.username, 30 | AvatarURL: follower.avatarURL, 31 | Introduction: follower.introduction, 32 | RegisterDate: dateToUnixTime(follower.registerDate), 33 | }; 34 | 35 | const params: DocumentClient.BatchWriteItemInput = { 36 | RequestItems: { 37 | [config.DatabaseTableName]: [ 38 | { 39 | PutRequest: { 40 | Item: followeePutItem, 41 | }, 42 | }, 43 | { 44 | PutRequest: { 45 | Item: followerPutItem, 46 | }, 47 | }, 48 | ], 49 | }, 50 | }; 51 | 52 | await ddb.batchWrite(params).promise(); 53 | logger.debug(`BatchWrite: ${JSON.stringify(params)}`); 54 | } 55 | 56 | async delete(entity: Follow): Promise { 57 | const ddb = new AWS.DynamoDB.DocumentClient(); 58 | const followee = entity.followee; 59 | const follower = entity.follower; 60 | 61 | const params: DocumentClient.BatchWriteItemInput = { 62 | RequestItems: { 63 | [config.DatabaseTableName]: [ 64 | { 65 | DeleteRequest: { 66 | Key: { 67 | ID: follower.id, 68 | DataType: `followee#${entity.id}`, 69 | }, 70 | }, 71 | }, 72 | { 73 | DeleteRequest: { 74 | Key: { 75 | ID: followee.id, 76 | DataType: `follower#${entity.id}`, 77 | }, 78 | }, 79 | }, 80 | ], 81 | }, 82 | }; 83 | 84 | await ddb.batchWrite(params).promise(); 85 | logger.debug(`BatchWrite: ${JSON.stringify(params)}`); 86 | } 87 | 88 | async countFollowee(userId: number): Promise { 89 | const ddb = new AWS.DynamoDB.DocumentClient(); 90 | 91 | const params: DocumentClient.QueryInput = { 92 | TableName: config.DatabaseTableName, 93 | Select: "COUNT", 94 | KeyConditionExpression: 95 | "ID=:id AND begins_with(DataType, :datatypePrefix)", 96 | ExpressionAttributeValues: { 97 | ":id": userId, 98 | ":datatypePrefix": "followee", 99 | }, 100 | }; 101 | 102 | const result = await ddb.query(params).promise(); 103 | logger.debug(`Query: ${JSON.stringify(params)}`); 104 | 105 | return result.Count || 0; 106 | } 107 | 108 | async countFollower(userId: number): Promise { 109 | const ddb = new AWS.DynamoDB.DocumentClient(); 110 | 111 | const params: DocumentClient.QueryInput = { 112 | TableName: config.DatabaseTableName, 113 | Select: "COUNT", 114 | KeyConditionExpression: 115 | "ID=:id AND begins_with(DataType, :datatypePrefix)", 116 | ExpressionAttributeValues: { 117 | ":id": userId, 118 | ":datatypePrefix": "follower", 119 | }, 120 | }; 121 | 122 | const result = await ddb.query(params).promise(); 123 | logger.debug(`Query: ${JSON.stringify(params)}`); 124 | 125 | return result.Count || 0; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/infrastructure/tweet-repository.ts: -------------------------------------------------------------------------------- 1 | import { ITweetRepository } from "@src/domain/repository"; 2 | import { dateToUnixTime, unixTimeToDate } from "@src/domain/time"; 3 | import { Tweet } from "@src/domain/tweet"; 4 | import { logger } from "@src/logger"; 5 | import * as AWS from "aws-sdk"; 6 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 7 | import { config } from "@src/config"; 8 | 9 | const maxBatchWriteItemCount = 25; 10 | 11 | export class TweetRepository implements ITweetRepository { 12 | async create(entity: Tweet): Promise { 13 | const ddb = new AWS.DynamoDB.DocumentClient(); 14 | 15 | const tweetPutParams: DocumentClient.PutItemInput = { 16 | TableName: config.DatabaseTableName, 17 | Item: { 18 | ID: entity.userId, 19 | DataType: `tweet#${entity.id}`, 20 | DataValue: entity.id.toString(), 21 | TweetContent: entity.content, 22 | TweetPostDate: dateToUnixTime(entity.postDate), 23 | }, 24 | }; 25 | 26 | await ddb.put(tweetPutParams).promise(); 27 | logger.debug(`PutItem: ${JSON.stringify(tweetPutParams)}`); 28 | 29 | // Insert a tweet in your and your followers's timeline 30 | const followerUserIds = await this.getFollowerUserIds(entity.userId); 31 | const timelineUserIds = [entity.userId, ...followerUserIds]; 32 | for ( 33 | let i = 0; 34 | i <= timelineUserIds.length - 1; 35 | i += maxBatchWriteItemCount 36 | ) { 37 | const ids = timelineUserIds.slice(i, maxBatchWriteItemCount); 38 | 39 | if (ids.length > 0) { 40 | const params: DocumentClient.BatchWriteItemInput = { 41 | RequestItems: { 42 | twitter: ids.map((i) => ({ 43 | PutRequest: { 44 | Item: { 45 | ID: i, 46 | DataType: `timeline#${entity.id}`, 47 | DataValue: entity.id.toString(), 48 | TweetContent: entity.content, 49 | TweetPostDate: dateToUnixTime(entity.postDate), 50 | }, 51 | }, 52 | })), 53 | }, 54 | }; 55 | 56 | await ddb.batchWrite(params).promise(); 57 | logger.debug(`BatchWrite: ${JSON.stringify(params)}`); 58 | } 59 | } 60 | } 61 | 62 | async findById(tweetId: number): Promise { 63 | const ddb = new AWS.DynamoDB.DocumentClient(); 64 | 65 | const params: DocumentClient.QueryInput = { 66 | TableName: config.DatabaseTableName, 67 | IndexName: "DataValue-index", 68 | KeyConditionExpression: "DataValue=:tweetId AND DataType=:datatype", 69 | ExpressionAttributeValues: { 70 | ":tweetId": tweetId.toString(), 71 | ":datatype": `tweet#${tweetId}`, 72 | }, 73 | }; 74 | 75 | const result = await ddb.query(params).promise(); 76 | logger.debug(`Query: ${JSON.stringify(params)}`); 77 | 78 | const item = result.Items ? result.Items[0] : null; 79 | if (!item) { 80 | return null; 81 | } else { 82 | return this.itemToTweet(item); 83 | } 84 | } 85 | 86 | async findAllByUserId(userId: number): Promise { 87 | const ddb = new AWS.DynamoDB.DocumentClient(); 88 | 89 | const params: DocumentClient.QueryInput = { 90 | TableName: config.DatabaseTableName, 91 | KeyConditionExpression: 92 | "ID=:id AND begins_with(DataType, :datatypePrefix)", 93 | ExpressionAttributeValues: { 94 | ":id": userId, 95 | ":datatypePrefix": "tweet", 96 | }, 97 | }; 98 | 99 | const result = await ddb.query(params).promise(); 100 | logger.debug(`Query: ${JSON.stringify(params)}`); 101 | 102 | if (result.Items) { 103 | return result.Items.map((i) => this.itemToTweet(i)); 104 | } else { 105 | return []; 106 | } 107 | } 108 | 109 | async getTimeline(userId: number): Promise { 110 | const ddb = new AWS.DynamoDB.DocumentClient(); 111 | 112 | const params: DocumentClient.QueryInput = { 113 | TableName: config.DatabaseTableName, 114 | KeyConditionExpression: 115 | "ID=:id AND begins_with(DataType, :datatypePrefix)", 116 | ExpressionAttributeValues: { 117 | ":id": userId, 118 | ":datatypePrefix": "timeline", 119 | }, 120 | }; 121 | 122 | const result = await ddb.query(params).promise(); 123 | logger.debug(`Query: ${JSON.stringify(params)}`); 124 | 125 | if (result.Items) { 126 | return result.Items.map( 127 | (i) => 128 | new Tweet( 129 | parseInt(i["DataValue"]), 130 | i["ID"], 131 | i["TweetContent"], 132 | unixTimeToDate(i["TweetPostDate"]) 133 | ) 134 | ); 135 | } else { 136 | return []; 137 | } 138 | } 139 | 140 | private getFollowerUserIds = async (userId: number): Promise => { 141 | const ddb = new AWS.DynamoDB.DocumentClient(); 142 | 143 | const params: DocumentClient.QueryInput = { 144 | TableName: config.DatabaseTableName, 145 | KeyConditionExpression: 146 | "ID=:id AND begins_with(DataType, :datatypePrefix)", 147 | ExpressionAttributeValues: { 148 | ":id": userId, 149 | ":datatypePrefix": "follower", 150 | }, 151 | }; 152 | 153 | const result = await ddb.query(params).promise(); 154 | logger.debug(`Query: ${JSON.stringify(params)}`); 155 | 156 | if (result.Items) { 157 | return result.Items.map((i) => parseInt(i["DataValue"])); 158 | } else { 159 | return []; 160 | } 161 | }; 162 | 163 | private itemToTweet = (item: DocumentClient.AttributeMap): Tweet => { 164 | return new Tweet( 165 | parseInt(item["DataValue"]), 166 | item["ID"], 167 | item["TweetContent"], 168 | unixTimeToDate(item["TweetPostDate"]) 169 | ); 170 | }; 171 | } 172 | -------------------------------------------------------------------------------- /src/infrastructure/user-repository.ts: -------------------------------------------------------------------------------- 1 | import { IUserRepository } from "@src/domain/repository"; 2 | import { dateToUnixTime, unixTimeToDate } from "@src/domain/time"; 3 | import { User } from "@src/domain/user"; 4 | import { logger } from "@src/logger"; 5 | import * as AWS from "aws-sdk"; 6 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 7 | import { config } from "@src/config"; 8 | 9 | export class UserRepository implements IUserRepository { 10 | async create(entity: User): Promise { 11 | const ddb = new AWS.DynamoDB.DocumentClient(); 12 | 13 | const params: DocumentClient.PutItemInput = { 14 | TableName: config.DatabaseTableName, 15 | Item: { 16 | ID: entity.id, 17 | DataType: "userProfile", 18 | DataValue: entity.username, 19 | AvatarURL: entity.avatarURL, 20 | Introduction: entity.introduction, 21 | RegisterDate: dateToUnixTime(entity.registerDate), 22 | }, 23 | }; 24 | 25 | await ddb.put(params).promise(); 26 | logger.debug(`PutItem: ${JSON.stringify(params)}`); 27 | } 28 | 29 | async findById(id: number): Promise { 30 | const ddb = new AWS.DynamoDB.DocumentClient(); 31 | 32 | const params: DocumentClient.GetItemInput = { 33 | TableName: config.DatabaseTableName, 34 | Key: { 35 | ID: id, 36 | DataType: "userProfile", 37 | }, 38 | }; 39 | 40 | const result = await ddb.get(params).promise(); 41 | logger.debug(`GetItem: ${JSON.stringify(params)}`); 42 | 43 | const item = result.Item; 44 | if (!item) { 45 | return null; 46 | } 47 | 48 | return this.itemToEntity(item); 49 | } 50 | 51 | async findByUsername(username: string): Promise { 52 | const ddb = new AWS.DynamoDB.DocumentClient(); 53 | 54 | const params: DocumentClient.QueryInput = { 55 | TableName: config.DatabaseTableName, 56 | IndexName: "DataValue-index", 57 | KeyConditionExpression: "DataValue=:username AND DataType=:datatype", 58 | ExpressionAttributeValues: { 59 | ":username": username, 60 | ":datatype": "userProfile", 61 | }, 62 | }; 63 | 64 | const result = await ddb.query(params).promise(); 65 | logger.debug(`Query: ${JSON.stringify(params)}`); 66 | 67 | const item = result.Items ? result.Items[0] : null; 68 | if (!item) { 69 | return null; 70 | } 71 | 72 | return this.itemToEntity(item); 73 | } 74 | 75 | private itemToEntity = (item: DocumentClient.AttributeMap): User => { 76 | return new User( 77 | item["ID"], 78 | item["DataValue"], 79 | item["AvatarURL"], 80 | item["Introduction"], 81 | unixTimeToDate(item["RegisterDate"]) 82 | ); 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from "winston"; 2 | 3 | export const logger = winston.createLogger({ 4 | transports: [ 5 | new winston.transports.Console({ 6 | level: "debug", 7 | }), 8 | ], 9 | }); 10 | -------------------------------------------------------------------------------- /src/presentation/api-server.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import { UserAppService } from "@src/application/user-app-service"; 3 | import { UserRepository } from "@src/infrastructure/user-repository"; 4 | import * as express from "express"; 5 | import * as morgan from "morgan"; 6 | import { logger } from "@src/logger"; 7 | import { FollowAppService } from "@src/application/follow-app-service"; 8 | import { FollowRepository } from "@src/infrastructure/follow-repository"; 9 | import { TweetAppService } from "@src/application/tweet-app-service"; 10 | import { TweetRepository } from "@src/infrastructure/tweet-repository"; 11 | import { CommentAppService } from "@src/application/comment-app-service"; 12 | import { CommentRepository } from "@src/infrastructure/comment-repository"; 13 | 14 | const pingRouter = express.Router(); 15 | pingRouter.get("/", (req, res) => { 16 | res.json({ status: "ok" }); 17 | }); 18 | 19 | const userRouter = express.Router(); 20 | userRouter.post("/register", async (req, res) => { 21 | const username: string = req.body.username; 22 | const avatarURL = req.body.avatarURL; 23 | const introduction = req.body.introduction; 24 | const userAppService = new UserAppService( 25 | new UserRepository(), 26 | new FollowRepository() 27 | ); 28 | 29 | const user = await userAppService.register(username, avatarURL, introduction); 30 | if (!user) { 31 | res.status(400).json(); 32 | } else { 33 | res.json(user); 34 | } 35 | }); 36 | 37 | userRouter.get("/:username", async (req, res) => { 38 | const username = req.params.username; 39 | 40 | const userAppService = new UserAppService( 41 | new UserRepository(), 42 | new FollowRepository() 43 | ); 44 | const profile = await userAppService.getUserProfile(username); 45 | 46 | if (profile) { 47 | res.json(profile); 48 | } else { 49 | res.status(404).json(); 50 | } 51 | }); 52 | 53 | const followRouter = express.Router(); 54 | followRouter.post("/", async (req, res) => { 55 | const followeeId = parseInt(req.body.followeeId); 56 | const followerId = parseInt(req.body.followerId); 57 | 58 | if (isNaN(followeeId)) { 59 | logger.debug("followeeId is NaN"); 60 | res.status(400).json(); 61 | return; 62 | } 63 | 64 | if (isNaN(followerId)) { 65 | logger.debug("followerId is NaN"); 66 | res.status(400).json(); 67 | return; 68 | } 69 | 70 | const followService = new FollowAppService( 71 | new UserRepository(), 72 | new FollowRepository() 73 | ); 74 | 75 | const ok = await followService.follow(followeeId, followerId); 76 | 77 | if (ok) { 78 | res.status(200).json(); 79 | } else { 80 | res.status(400).json(); 81 | } 82 | }); 83 | followRouter.delete("/", async (req, res) => { 84 | const followeeId = parseInt(req.body.followeeId); 85 | const followerId = parseInt(req.body.followerId); 86 | 87 | if (isNaN(followeeId)) { 88 | logger.debug("followeeId is NaN"); 89 | res.status(400).json(); 90 | return; 91 | } 92 | 93 | if (isNaN(followerId)) { 94 | logger.debug("followerId is NaN"); 95 | res.status(400).json(); 96 | return; 97 | } 98 | 99 | const followService = new FollowAppService( 100 | new UserRepository(), 101 | new FollowRepository() 102 | ); 103 | 104 | const ok = await followService.unfollow(followeeId, followerId); 105 | if (ok) { 106 | res.status(200).json(); 107 | } else { 108 | res.status(400).json(); 109 | } 110 | }); 111 | 112 | const tweetRouter = express.Router(); 113 | tweetRouter.post("/", async (req, res) => { 114 | const userId = parseInt(req.body.userId); 115 | const content = req.body.content; 116 | 117 | if (isNaN(userId)) { 118 | logger.debug("userId is NaN"); 119 | res.status(400).json(); 120 | return; 121 | } 122 | 123 | const tweetService = new TweetAppService( 124 | new UserRepository(), 125 | new TweetRepository() 126 | ); 127 | 128 | const tweet = await tweetService.post(userId, content); 129 | 130 | if (tweet) { 131 | res.status(200).json(tweet); 132 | } else { 133 | res.status(400).json(); 134 | } 135 | }); 136 | tweetRouter.get("/timeline", async (req, res) => { 137 | const userId = parseInt(req.body.userId); 138 | 139 | if (isNaN(userId)) { 140 | logger.debug("userId is NaN"); 141 | res.status(400).json(); 142 | return; 143 | } 144 | 145 | const tweetService = new TweetAppService( 146 | new UserRepository(), 147 | new TweetRepository() 148 | ); 149 | const tweets = await tweetService.getTimeline(userId); 150 | 151 | res.status(200).json(tweets); 152 | }); 153 | tweetRouter.get("/:id", async (req, res) => { 154 | const tweetId = parseInt(req.params.id); 155 | 156 | if (isNaN(tweetId)) { 157 | logger.debug("Id is NaN"); 158 | res.status(400).json(); 159 | return; 160 | } 161 | const tweetService = new TweetAppService( 162 | new UserRepository(), 163 | new TweetRepository() 164 | ); 165 | 166 | const tweet = await tweetService.getOne(tweetId); 167 | 168 | if (tweet) { 169 | res.status(200).json(tweet); 170 | } else { 171 | res.status(404).json(); 172 | } 173 | }); 174 | tweetRouter.get("/user/:userId", async (req, res) => { 175 | const userId = parseInt(req.params.userId); 176 | 177 | if (isNaN(userId)) { 178 | logger.debug("userId is NaN"); 179 | res.status(400).json(); 180 | return; 181 | } 182 | const tweetService = new TweetAppService( 183 | new UserRepository(), 184 | new TweetRepository() 185 | ); 186 | 187 | const tweets = await tweetService.getAll(userId); 188 | 189 | res.status(200).json(tweets); 190 | }); 191 | 192 | const commentRouter = express.Router(); 193 | commentRouter.post("/", async (req, res) => { 194 | const userId = parseInt(req.body.userId); 195 | const tweetId = parseInt(req.body.tweetId); 196 | const content = req.body.content; 197 | 198 | if (isNaN(userId)) { 199 | logger.debug("userId is NaN"); 200 | res.status(400).json(); 201 | return; 202 | } 203 | 204 | if (isNaN(tweetId)) { 205 | logger.debug("tweetId is NaN"); 206 | res.status(400).json(); 207 | return; 208 | } 209 | 210 | const commentService = new CommentAppService( 211 | new UserRepository(), 212 | new TweetRepository(), 213 | new CommentRepository() 214 | ); 215 | 216 | const comment = await commentService.post(userId, tweetId, content); 217 | 218 | if (!comment) { 219 | res.status(400).json(); 220 | } else { 221 | res.status(200).json(comment); 222 | } 223 | }); 224 | commentRouter.get("/tweet/:tweetId", async (req, res) => { 225 | const tweetId = parseInt(req.params.tweetId); 226 | 227 | if (isNaN(tweetId)) { 228 | logger.debug("tweetId is NaN"); 229 | res.status(400).json(); 230 | return; 231 | } 232 | 233 | const commentService = new CommentAppService( 234 | new UserRepository(), 235 | new TweetRepository(), 236 | new CommentRepository() 237 | ); 238 | 239 | const comments = await commentService.getAll(tweetId); 240 | 241 | res.status(200).json(comments); 242 | }); 243 | 244 | export class ApiServer { 245 | private port: number; 246 | private server: http.Server; 247 | 248 | constructor(port?: number) { 249 | this.port = port || 7000; 250 | 251 | const app = express(); 252 | app.use(express.json()); 253 | app.use(morgan("combined")); 254 | 255 | app.use("/ping", pingRouter); 256 | app.use("/users", userRouter); 257 | app.use("/follow", followRouter); 258 | app.use("/tweets", tweetRouter); 259 | app.use("/comments", commentRouter); 260 | 261 | this.server = http.createServer(app); 262 | this.server.on("close", () => {}); 263 | } 264 | 265 | start() { 266 | logger.info("Starting server..."); 267 | this.server.listen(this.port); 268 | logger.info(`Listen port: ${this.port}`); 269 | } 270 | 271 | stop() { 272 | this.server.close(); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/tasks/db-init.ts: -------------------------------------------------------------------------------- 1 | import { initAWS } from "@src/infrastructure/aws"; 2 | import * as AWS from "aws-sdk"; 3 | import { CreateTableInput } from "aws-sdk/clients/dynamodb"; 4 | 5 | initAWS(); 6 | 7 | const createTwitterTableInput: CreateTableInput = { 8 | TableName: "twitter", 9 | AttributeDefinitions: [ 10 | { AttributeName: "ID", AttributeType: "N" }, 11 | { AttributeName: "DataType", AttributeType: "S" }, 12 | { AttributeName: "DataValue", AttributeType: "S" }, 13 | ], 14 | KeySchema: [ 15 | { AttributeName: "ID", KeyType: "HASH" }, 16 | { AttributeName: "DataType", KeyType: "RANGE" }, 17 | ], 18 | ProvisionedThroughput: { 19 | ReadCapacityUnits: 10, 20 | WriteCapacityUnits: 10, 21 | }, 22 | GlobalSecondaryIndexes: [ 23 | { 24 | IndexName: "DataValue-index", 25 | Projection: { 26 | ProjectionType: "ALL", 27 | }, 28 | KeySchema: [ 29 | { 30 | AttributeName: "DataValue", 31 | KeyType: "HASH", 32 | }, 33 | { 34 | AttributeName: "DataType", 35 | KeyType: "RANGE", 36 | }, 37 | ], 38 | ProvisionedThroughput: { 39 | ReadCapacityUnits: 10, 40 | WriteCapacityUnits: 10, 41 | }, 42 | }, 43 | ], 44 | }; 45 | 46 | const ddb = new AWS.DynamoDB(); 47 | 48 | ddb.createTable(createTwitterTableInput, (err, data) => { 49 | if (err) { 50 | console.info("Unable to create table."); 51 | console.error(err); 52 | } else { 53 | console.info("Create table."); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /src/tasks/db-seed.ts: -------------------------------------------------------------------------------- 1 | import { UserAppService } from "@src/application/user-app-service"; 2 | import { UserRepository } from "@src/infrastructure/user-repository"; 3 | import { FollowAppService } from "@src/application/follow-app-service"; 4 | import { FollowRepository } from "@src/infrastructure/follow-repository"; 5 | import { initAWS } from "@src/infrastructure/aws"; 6 | import { TweetAppService } from "@src/application/tweet-app-service"; 7 | import { TweetRepository } from "@src/infrastructure/tweet-repository"; 8 | 9 | initAWS(); 10 | 11 | const run = async () => { 12 | const userAppService = new UserAppService( 13 | new UserRepository(), 14 | new FollowRepository() 15 | ); 16 | 17 | const user1 = (await userAppService.register( 18 | "user1", 19 | "https://www.w3schools.com/howto/img_avatar.png", 20 | "I'm engineer" 21 | ))!; 22 | 23 | const user2 = (await userAppService.register( 24 | "user2", 25 | "https://www.w3schools.com/howto/img_avatar.png", 26 | "I'm sales" 27 | ))!; 28 | 29 | const user3 = (await userAppService.register( 30 | "user3", 31 | "https://www.w3schools.com/howto/img_avatar.png", 32 | "I'm marketer" 33 | ))!; 34 | 35 | const user4 = (await userAppService.register( 36 | "user4", 37 | "https://www.w3schools.com/howto/img_avatar.png", 38 | "I'm director" 39 | ))!; 40 | const user5 = (await userAppService.register( 41 | "user5", 42 | "https://www.w3schools.com/howto/img_avatar.png", 43 | "I'm product manager" 44 | ))!; 45 | 46 | const followAppService = new FollowAppService( 47 | new UserRepository(), 48 | new FollowRepository() 49 | ); 50 | 51 | await followAppService.follow(user1.id, user2.id); 52 | await followAppService.follow(user1.id, user3.id); 53 | await followAppService.follow(user1.id, user4.id); 54 | await followAppService.follow(user5.id, user1.id); 55 | 56 | const tweetAppService = new TweetAppService( 57 | new UserRepository(), 58 | new TweetRepository() 59 | ); 60 | 61 | [user1, user2, user3, user4].map(async (u) => { 62 | await tweetAppService.post(u.id, `Hi, I'm ${u.username}`); 63 | }); 64 | [user1, user2, user3, user4].map(async (u) => { 65 | await tweetAppService.post(u.id, "What are you doing?"); 66 | }); 67 | }; 68 | 69 | run(); 70 | -------------------------------------------------------------------------------- /src/tasks/run-api-server.ts: -------------------------------------------------------------------------------- 1 | import { initAWS } from "@src/infrastructure/aws"; 2 | import { ApiServer } from "@src/presentation/api-server"; 3 | 4 | initAWS(); 5 | const server = new ApiServer(); 6 | server.start(); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@src/*": ["src/*"] 6 | }, 7 | "outDir": "./dist/", 8 | "sourceMap": true, 9 | "strictNullChecks": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "experimentalDecorators": true, 13 | "skipLibCheck": true, 14 | "emitDecoratorMetadata": true, 15 | "declaration": true, 16 | "target": "ES2021", 17 | "typeRoots": ["node_modules/@types"], 18 | "lib": ["ES2021"], 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.d.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@colors/colors@1.5.0": 6 | version "1.5.0" 7 | resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" 8 | integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== 9 | 10 | "@cspotcode/source-map-consumer@0.8.0": 11 | version "0.8.0" 12 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" 13 | integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== 14 | 15 | "@cspotcode/source-map-support@0.7.0": 16 | version "0.7.0" 17 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" 18 | integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== 19 | dependencies: 20 | "@cspotcode/source-map-consumer" "0.8.0" 21 | 22 | "@dabh/diagnostics@^2.0.2": 23 | version "2.0.3" 24 | resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" 25 | integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== 26 | dependencies: 27 | colorspace "1.1.x" 28 | enabled "2.0.x" 29 | kuler "^2.0.0" 30 | 31 | "@tsconfig/node10@^1.0.7": 32 | version "1.0.8" 33 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" 34 | integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== 35 | 36 | "@tsconfig/node12@^1.0.7": 37 | version "1.0.9" 38 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" 39 | integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== 40 | 41 | "@tsconfig/node14@^1.0.0": 42 | version "1.0.1" 43 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" 44 | integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== 45 | 46 | "@tsconfig/node16@^1.0.2": 47 | version "1.0.2" 48 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" 49 | integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== 50 | 51 | "@types/body-parser@*": 52 | version "1.19.2" 53 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" 54 | integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== 55 | dependencies: 56 | "@types/connect" "*" 57 | "@types/node" "*" 58 | 59 | "@types/connect@*": 60 | version "3.4.35" 61 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" 62 | integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== 63 | dependencies: 64 | "@types/node" "*" 65 | 66 | "@types/express-serve-static-core@^4.17.18": 67 | version "4.17.28" 68 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" 69 | integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== 70 | dependencies: 71 | "@types/node" "*" 72 | "@types/qs" "*" 73 | "@types/range-parser" "*" 74 | 75 | "@types/express@^4.17.13": 76 | version "4.17.13" 77 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" 78 | integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== 79 | dependencies: 80 | "@types/body-parser" "*" 81 | "@types/express-serve-static-core" "^4.17.18" 82 | "@types/qs" "*" 83 | "@types/serve-static" "*" 84 | 85 | "@types/json5@^0.0.29": 86 | version "0.0.29" 87 | resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" 88 | integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= 89 | 90 | "@types/mime@^1": 91 | version "1.3.2" 92 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" 93 | integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== 94 | 95 | "@types/node@*": 96 | version "17.0.23" 97 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" 98 | integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== 99 | 100 | "@types/qs@*": 101 | version "6.9.7" 102 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" 103 | integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== 104 | 105 | "@types/range-parser@*": 106 | version "1.2.4" 107 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" 108 | integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== 109 | 110 | "@types/serve-static@*": 111 | version "1.13.10" 112 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" 113 | integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== 114 | dependencies: 115 | "@types/mime" "^1" 116 | "@types/node" "*" 117 | 118 | "@types/strip-bom@^3.0.0": 119 | version "3.0.0" 120 | resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" 121 | integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= 122 | 123 | "@types/strip-json-comments@0.0.30": 124 | version "0.0.30" 125 | resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" 126 | integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== 127 | 128 | accepts@~1.3.8: 129 | version "1.3.8" 130 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 131 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 132 | dependencies: 133 | mime-types "~2.1.34" 134 | negotiator "0.6.3" 135 | 136 | acorn-walk@^8.1.1: 137 | version "8.2.0" 138 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 139 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 140 | 141 | acorn@^8.4.1: 142 | version "8.7.0" 143 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" 144 | integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== 145 | 146 | anymatch@~3.1.2: 147 | version "3.1.2" 148 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 149 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 150 | dependencies: 151 | normalize-path "^3.0.0" 152 | picomatch "^2.0.4" 153 | 154 | arg@^4.1.0: 155 | version "4.1.3" 156 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 157 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 158 | 159 | array-flatten@1.1.1: 160 | version "1.1.1" 161 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 162 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 163 | 164 | async@^3.2.3: 165 | version "3.2.3" 166 | resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" 167 | integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== 168 | 169 | aws-sdk@^2.1111.0: 170 | version "2.1111.0" 171 | resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1111.0.tgz#02b1e5c530ef8140235ee7c48c710bb2dbd7dc84" 172 | integrity sha512-WRyNcCckzmu1djTAWfR2r+BuI/PbuLrhG3oa+oH39v4NZ4EecYWFL1CoCPlC2kRUML4maSba5T4zlxjcNl7ELQ== 173 | dependencies: 174 | buffer "4.9.2" 175 | events "1.1.1" 176 | ieee754 "1.1.13" 177 | jmespath "0.16.0" 178 | querystring "0.2.0" 179 | sax "1.2.1" 180 | url "0.10.3" 181 | uuid "3.3.2" 182 | xml2js "0.4.19" 183 | 184 | balanced-match@^1.0.0: 185 | version "1.0.2" 186 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 187 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 188 | 189 | base64-js@^1.0.2: 190 | version "1.5.1" 191 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 192 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 193 | 194 | basic-auth@~2.0.1: 195 | version "2.0.1" 196 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" 197 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 198 | dependencies: 199 | safe-buffer "5.1.2" 200 | 201 | binary-extensions@^2.0.0: 202 | version "2.2.0" 203 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 204 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 205 | 206 | body-parser@1.19.2: 207 | version "1.19.2" 208 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" 209 | integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== 210 | dependencies: 211 | bytes "3.1.2" 212 | content-type "~1.0.4" 213 | debug "2.6.9" 214 | depd "~1.1.2" 215 | http-errors "1.8.1" 216 | iconv-lite "0.4.24" 217 | on-finished "~2.3.0" 218 | qs "6.9.7" 219 | raw-body "2.4.3" 220 | type-is "~1.6.18" 221 | 222 | brace-expansion@^1.1.7: 223 | version "1.1.11" 224 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 225 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 226 | dependencies: 227 | balanced-match "^1.0.0" 228 | concat-map "0.0.1" 229 | 230 | braces@~3.0.2: 231 | version "3.0.2" 232 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 233 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 234 | dependencies: 235 | fill-range "^7.0.1" 236 | 237 | buffer-from@^1.0.0: 238 | version "1.1.2" 239 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 240 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 241 | 242 | buffer@4.9.2: 243 | version "4.9.2" 244 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" 245 | integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== 246 | dependencies: 247 | base64-js "^1.0.2" 248 | ieee754 "^1.1.4" 249 | isarray "^1.0.0" 250 | 251 | bytes@3.1.2: 252 | version "3.1.2" 253 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 254 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 255 | 256 | chokidar@^3.5.1: 257 | version "3.5.3" 258 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 259 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 260 | dependencies: 261 | anymatch "~3.1.2" 262 | braces "~3.0.2" 263 | glob-parent "~5.1.2" 264 | is-binary-path "~2.1.0" 265 | is-glob "~4.0.1" 266 | normalize-path "~3.0.0" 267 | readdirp "~3.6.0" 268 | optionalDependencies: 269 | fsevents "~2.3.2" 270 | 271 | color-convert@^1.9.3: 272 | version "1.9.3" 273 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 274 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 275 | dependencies: 276 | color-name "1.1.3" 277 | 278 | color-name@1.1.3: 279 | version "1.1.3" 280 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 281 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 282 | 283 | color-name@^1.0.0: 284 | version "1.1.4" 285 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 286 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 287 | 288 | color-string@^1.6.0: 289 | version "1.9.0" 290 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" 291 | integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== 292 | dependencies: 293 | color-name "^1.0.0" 294 | simple-swizzle "^0.2.2" 295 | 296 | color@^3.1.3: 297 | version "3.2.1" 298 | resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" 299 | integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== 300 | dependencies: 301 | color-convert "^1.9.3" 302 | color-string "^1.6.0" 303 | 304 | colorspace@1.1.x: 305 | version "1.1.4" 306 | resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" 307 | integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== 308 | dependencies: 309 | color "^3.1.3" 310 | text-hex "1.0.x" 311 | 312 | concat-map@0.0.1: 313 | version "0.0.1" 314 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 315 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 316 | 317 | content-disposition@0.5.4: 318 | version "0.5.4" 319 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 320 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 321 | dependencies: 322 | safe-buffer "5.2.1" 323 | 324 | content-type@~1.0.4: 325 | version "1.0.4" 326 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 327 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 328 | 329 | cookie-signature@1.0.6: 330 | version "1.0.6" 331 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 332 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 333 | 334 | cookie@0.4.2: 335 | version "0.4.2" 336 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" 337 | integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== 338 | 339 | create-require@^1.1.0: 340 | version "1.1.1" 341 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 342 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 343 | 344 | debug@2.6.9: 345 | version "2.6.9" 346 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 347 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 348 | dependencies: 349 | ms "2.0.0" 350 | 351 | depd@~1.1.2: 352 | version "1.1.2" 353 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 354 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 355 | 356 | depd@~2.0.0: 357 | version "2.0.0" 358 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 359 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 360 | 361 | destroy@~1.0.4: 362 | version "1.0.4" 363 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 364 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 365 | 366 | diff@^4.0.1: 367 | version "4.0.2" 368 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 369 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 370 | 371 | dynamic-dedupe@^0.3.0: 372 | version "0.3.0" 373 | resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" 374 | integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= 375 | dependencies: 376 | xtend "^4.0.0" 377 | 378 | ee-first@1.1.1: 379 | version "1.1.1" 380 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 381 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 382 | 383 | enabled@2.0.x: 384 | version "2.0.0" 385 | resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" 386 | integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== 387 | 388 | encodeurl@~1.0.2: 389 | version "1.0.2" 390 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 391 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 392 | 393 | escape-html@~1.0.3: 394 | version "1.0.3" 395 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 396 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 397 | 398 | etag@~1.8.1: 399 | version "1.8.1" 400 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 401 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 402 | 403 | events@1.1.1: 404 | version "1.1.1" 405 | resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" 406 | integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= 407 | 408 | express@^4.17.3: 409 | version "4.17.3" 410 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" 411 | integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== 412 | dependencies: 413 | accepts "~1.3.8" 414 | array-flatten "1.1.1" 415 | body-parser "1.19.2" 416 | content-disposition "0.5.4" 417 | content-type "~1.0.4" 418 | cookie "0.4.2" 419 | cookie-signature "1.0.6" 420 | debug "2.6.9" 421 | depd "~1.1.2" 422 | encodeurl "~1.0.2" 423 | escape-html "~1.0.3" 424 | etag "~1.8.1" 425 | finalhandler "~1.1.2" 426 | fresh "0.5.2" 427 | merge-descriptors "1.0.1" 428 | methods "~1.1.2" 429 | on-finished "~2.3.0" 430 | parseurl "~1.3.3" 431 | path-to-regexp "0.1.7" 432 | proxy-addr "~2.0.7" 433 | qs "6.9.7" 434 | range-parser "~1.2.1" 435 | safe-buffer "5.2.1" 436 | send "0.17.2" 437 | serve-static "1.14.2" 438 | setprototypeof "1.2.0" 439 | statuses "~1.5.0" 440 | type-is "~1.6.18" 441 | utils-merge "1.0.1" 442 | vary "~1.1.2" 443 | 444 | fecha@^4.2.0: 445 | version "4.2.2" 446 | resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.2.tgz#902c69d332b93c69be48992b9a11e41d36c03581" 447 | integrity sha512-5rOQWkBVz3FnYWTi/ELZmq4CoK1Pb+xKNZWuJRsOwo0+8DrP43CrWJtyLVvb5U7z7ggE5llahfDbLjaVNzXVJQ== 448 | 449 | fill-range@^7.0.1: 450 | version "7.0.1" 451 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 452 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 453 | dependencies: 454 | to-regex-range "^5.0.1" 455 | 456 | finalhandler@~1.1.2: 457 | version "1.1.2" 458 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 459 | integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== 460 | dependencies: 461 | debug "2.6.9" 462 | encodeurl "~1.0.2" 463 | escape-html "~1.0.3" 464 | on-finished "~2.3.0" 465 | parseurl "~1.3.3" 466 | statuses "~1.5.0" 467 | unpipe "~1.0.0" 468 | 469 | fn.name@1.x.x: 470 | version "1.1.0" 471 | resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" 472 | integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== 473 | 474 | forwarded@0.2.0: 475 | version "0.2.0" 476 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 477 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 478 | 479 | fresh@0.5.2: 480 | version "0.5.2" 481 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 482 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 483 | 484 | fs.realpath@^1.0.0: 485 | version "1.0.0" 486 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 487 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 488 | 489 | fsevents@~2.3.2: 490 | version "2.3.2" 491 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 492 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 493 | 494 | function-bind@^1.1.1: 495 | version "1.1.1" 496 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 497 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 498 | 499 | glob-parent@~5.1.2: 500 | version "5.1.2" 501 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 502 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 503 | dependencies: 504 | is-glob "^4.0.1" 505 | 506 | glob@^7.1.3: 507 | version "7.2.0" 508 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" 509 | integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== 510 | dependencies: 511 | fs.realpath "^1.0.0" 512 | inflight "^1.0.4" 513 | inherits "2" 514 | minimatch "^3.0.4" 515 | once "^1.3.0" 516 | path-is-absolute "^1.0.0" 517 | 518 | has@^1.0.3: 519 | version "1.0.3" 520 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 521 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 522 | dependencies: 523 | function-bind "^1.1.1" 524 | 525 | http-errors@1.8.1: 526 | version "1.8.1" 527 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" 528 | integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== 529 | dependencies: 530 | depd "~1.1.2" 531 | inherits "2.0.4" 532 | setprototypeof "1.2.0" 533 | statuses ">= 1.5.0 < 2" 534 | toidentifier "1.0.1" 535 | 536 | iconv-lite@0.4.24: 537 | version "0.4.24" 538 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 539 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 540 | dependencies: 541 | safer-buffer ">= 2.1.2 < 3" 542 | 543 | ieee754@1.1.13: 544 | version "1.1.13" 545 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" 546 | integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== 547 | 548 | ieee754@^1.1.4: 549 | version "1.2.1" 550 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 551 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 552 | 553 | inflight@^1.0.4: 554 | version "1.0.6" 555 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 556 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 557 | dependencies: 558 | once "^1.3.0" 559 | wrappy "1" 560 | 561 | inherits@2, inherits@2.0.4, inherits@^2.0.3: 562 | version "2.0.4" 563 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 564 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 565 | 566 | ipaddr.js@1.9.1: 567 | version "1.9.1" 568 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 569 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 570 | 571 | is-arrayish@^0.3.1: 572 | version "0.3.2" 573 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" 574 | integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== 575 | 576 | is-binary-path@~2.1.0: 577 | version "2.1.0" 578 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 579 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 580 | dependencies: 581 | binary-extensions "^2.0.0" 582 | 583 | is-core-module@^2.8.1: 584 | version "2.8.1" 585 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" 586 | integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== 587 | dependencies: 588 | has "^1.0.3" 589 | 590 | is-extglob@^2.1.1: 591 | version "2.1.1" 592 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 593 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 594 | 595 | is-glob@^4.0.1, is-glob@~4.0.1: 596 | version "4.0.3" 597 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 598 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 599 | dependencies: 600 | is-extglob "^2.1.1" 601 | 602 | is-number@^7.0.0: 603 | version "7.0.0" 604 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 605 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 606 | 607 | is-stream@^2.0.0: 608 | version "2.0.1" 609 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" 610 | integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== 611 | 612 | isarray@^1.0.0: 613 | version "1.0.0" 614 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 615 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= 616 | 617 | jmespath@0.16.0: 618 | version "0.16.0" 619 | resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" 620 | integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== 621 | 622 | json5@^1.0.1: 623 | version "1.0.1" 624 | resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" 625 | integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== 626 | dependencies: 627 | minimist "^1.2.0" 628 | 629 | kuler@^2.0.0: 630 | version "2.0.0" 631 | resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" 632 | integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== 633 | 634 | logform@^2.3.2, logform@^2.4.0: 635 | version "2.4.0" 636 | resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" 637 | integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== 638 | dependencies: 639 | "@colors/colors" "1.5.0" 640 | fecha "^4.2.0" 641 | ms "^2.1.1" 642 | safe-stable-stringify "^2.3.1" 643 | triple-beam "^1.3.0" 644 | 645 | make-error@^1.1.1: 646 | version "1.3.6" 647 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 648 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 649 | 650 | media-typer@0.3.0: 651 | version "0.3.0" 652 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 653 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 654 | 655 | merge-descriptors@1.0.1: 656 | version "1.0.1" 657 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 658 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 659 | 660 | methods@~1.1.2: 661 | version "1.1.2" 662 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 663 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 664 | 665 | mime-db@1.52.0: 666 | version "1.52.0" 667 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 668 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 669 | 670 | mime-types@~2.1.24, mime-types@~2.1.34: 671 | version "2.1.35" 672 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 673 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 674 | dependencies: 675 | mime-db "1.52.0" 676 | 677 | mime@1.6.0: 678 | version "1.6.0" 679 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 680 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 681 | 682 | minimatch@^3.0.4: 683 | version "3.1.2" 684 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 685 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 686 | dependencies: 687 | brace-expansion "^1.1.7" 688 | 689 | minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: 690 | version "1.2.6" 691 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" 692 | integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== 693 | 694 | mkdirp@^1.0.4: 695 | version "1.0.4" 696 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" 697 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 698 | 699 | morgan@^1.10.0: 700 | version "1.10.0" 701 | resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" 702 | integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== 703 | dependencies: 704 | basic-auth "~2.0.1" 705 | debug "2.6.9" 706 | depd "~2.0.0" 707 | on-finished "~2.3.0" 708 | on-headers "~1.0.2" 709 | 710 | ms@2.0.0: 711 | version "2.0.0" 712 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 713 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 714 | 715 | ms@2.1.3, ms@^2.1.1: 716 | version "2.1.3" 717 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 718 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 719 | 720 | negotiator@0.6.3: 721 | version "0.6.3" 722 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 723 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 724 | 725 | normalize-path@^3.0.0, normalize-path@~3.0.0: 726 | version "3.0.0" 727 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 728 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 729 | 730 | on-finished@~2.3.0: 731 | version "2.3.0" 732 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 733 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 734 | dependencies: 735 | ee-first "1.1.1" 736 | 737 | on-headers@~1.0.2: 738 | version "1.0.2" 739 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" 740 | integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== 741 | 742 | once@^1.3.0: 743 | version "1.4.0" 744 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 745 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 746 | dependencies: 747 | wrappy "1" 748 | 749 | one-time@^1.0.0: 750 | version "1.0.0" 751 | resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" 752 | integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== 753 | dependencies: 754 | fn.name "1.x.x" 755 | 756 | parseurl@~1.3.3: 757 | version "1.3.3" 758 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 759 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 760 | 761 | path-is-absolute@^1.0.0: 762 | version "1.0.1" 763 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 764 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 765 | 766 | path-parse@^1.0.7: 767 | version "1.0.7" 768 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 769 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 770 | 771 | path-to-regexp@0.1.7: 772 | version "0.1.7" 773 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 774 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 775 | 776 | picomatch@^2.0.4, picomatch@^2.2.1: 777 | version "2.3.1" 778 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 779 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 780 | 781 | proxy-addr@~2.0.7: 782 | version "2.0.7" 783 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 784 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 785 | dependencies: 786 | forwarded "0.2.0" 787 | ipaddr.js "1.9.1" 788 | 789 | punycode@1.3.2: 790 | version "1.3.2" 791 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" 792 | integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= 793 | 794 | qs@6.9.7: 795 | version "6.9.7" 796 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" 797 | integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== 798 | 799 | querystring@0.2.0: 800 | version "0.2.0" 801 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" 802 | integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= 803 | 804 | range-parser@~1.2.1: 805 | version "1.2.1" 806 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 807 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 808 | 809 | raw-body@2.4.3: 810 | version "2.4.3" 811 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" 812 | integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== 813 | dependencies: 814 | bytes "3.1.2" 815 | http-errors "1.8.1" 816 | iconv-lite "0.4.24" 817 | unpipe "1.0.0" 818 | 819 | readable-stream@^3.4.0, readable-stream@^3.6.0: 820 | version "3.6.0" 821 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" 822 | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== 823 | dependencies: 824 | inherits "^2.0.3" 825 | string_decoder "^1.1.1" 826 | util-deprecate "^1.0.1" 827 | 828 | readdirp@~3.6.0: 829 | version "3.6.0" 830 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 831 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 832 | dependencies: 833 | picomatch "^2.2.1" 834 | 835 | resolve@^1.0.0: 836 | version "1.22.0" 837 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" 838 | integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== 839 | dependencies: 840 | is-core-module "^2.8.1" 841 | path-parse "^1.0.7" 842 | supports-preserve-symlinks-flag "^1.0.0" 843 | 844 | rimraf@^2.6.1: 845 | version "2.7.1" 846 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" 847 | integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== 848 | dependencies: 849 | glob "^7.1.3" 850 | 851 | safe-buffer@5.1.2: 852 | version "5.1.2" 853 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 854 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 855 | 856 | safe-buffer@5.2.1, safe-buffer@~5.2.0: 857 | version "5.2.1" 858 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 859 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 860 | 861 | safe-stable-stringify@^2.3.1: 862 | version "2.3.1" 863 | resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" 864 | integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== 865 | 866 | "safer-buffer@>= 2.1.2 < 3": 867 | version "2.1.2" 868 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 869 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 870 | 871 | sax@1.2.1: 872 | version "1.2.1" 873 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" 874 | integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= 875 | 876 | sax@>=0.6.0: 877 | version "1.2.4" 878 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" 879 | integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== 880 | 881 | send@0.17.2: 882 | version "0.17.2" 883 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" 884 | integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== 885 | dependencies: 886 | debug "2.6.9" 887 | depd "~1.1.2" 888 | destroy "~1.0.4" 889 | encodeurl "~1.0.2" 890 | escape-html "~1.0.3" 891 | etag "~1.8.1" 892 | fresh "0.5.2" 893 | http-errors "1.8.1" 894 | mime "1.6.0" 895 | ms "2.1.3" 896 | on-finished "~2.3.0" 897 | range-parser "~1.2.1" 898 | statuses "~1.5.0" 899 | 900 | serve-static@1.14.2: 901 | version "1.14.2" 902 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" 903 | integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== 904 | dependencies: 905 | encodeurl "~1.0.2" 906 | escape-html "~1.0.3" 907 | parseurl "~1.3.3" 908 | send "0.17.2" 909 | 910 | setprototypeof@1.2.0: 911 | version "1.2.0" 912 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 913 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 914 | 915 | simple-swizzle@^0.2.2: 916 | version "0.2.2" 917 | resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" 918 | integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= 919 | dependencies: 920 | is-arrayish "^0.3.1" 921 | 922 | source-map-support@^0.5.12, source-map-support@^0.5.17: 923 | version "0.5.21" 924 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" 925 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 926 | dependencies: 927 | buffer-from "^1.0.0" 928 | source-map "^0.6.0" 929 | 930 | source-map@^0.6.0: 931 | version "0.6.1" 932 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 933 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 934 | 935 | stack-trace@0.0.x: 936 | version "0.0.10" 937 | resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" 938 | integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= 939 | 940 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 941 | version "1.5.0" 942 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 943 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 944 | 945 | string_decoder@^1.1.1: 946 | version "1.3.0" 947 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 948 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 949 | dependencies: 950 | safe-buffer "~5.2.0" 951 | 952 | strip-bom@^3.0.0: 953 | version "3.0.0" 954 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 955 | integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= 956 | 957 | strip-json-comments@^2.0.0: 958 | version "2.0.1" 959 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 960 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 961 | 962 | supports-preserve-symlinks-flag@^1.0.0: 963 | version "1.0.0" 964 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 965 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 966 | 967 | text-hex@1.0.x: 968 | version "1.0.0" 969 | resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" 970 | integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== 971 | 972 | to-regex-range@^5.0.1: 973 | version "5.0.1" 974 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 975 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 976 | dependencies: 977 | is-number "^7.0.0" 978 | 979 | toidentifier@1.0.1: 980 | version "1.0.1" 981 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 982 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 983 | 984 | tree-kill@^1.2.2: 985 | version "1.2.2" 986 | resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" 987 | integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== 988 | 989 | triple-beam@^1.3.0: 990 | version "1.3.0" 991 | resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" 992 | integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== 993 | 994 | ts-node-dev@^1.1.8: 995 | version "1.1.8" 996 | resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.8.tgz#95520d8ab9d45fffa854d6668e2f8f9286241066" 997 | integrity sha512-Q/m3vEwzYwLZKmV6/0VlFxcZzVV/xcgOt+Tx/VjaaRHyiBcFlV0541yrT09QjzzCxlDZ34OzKjrFAynlmtflEg== 998 | dependencies: 999 | chokidar "^3.5.1" 1000 | dynamic-dedupe "^0.3.0" 1001 | minimist "^1.2.5" 1002 | mkdirp "^1.0.4" 1003 | resolve "^1.0.0" 1004 | rimraf "^2.6.1" 1005 | source-map-support "^0.5.12" 1006 | tree-kill "^1.2.2" 1007 | ts-node "^9.0.0" 1008 | tsconfig "^7.0.0" 1009 | 1010 | ts-node@^10.7.0: 1011 | version "10.7.0" 1012 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" 1013 | integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== 1014 | dependencies: 1015 | "@cspotcode/source-map-support" "0.7.0" 1016 | "@tsconfig/node10" "^1.0.7" 1017 | "@tsconfig/node12" "^1.0.7" 1018 | "@tsconfig/node14" "^1.0.0" 1019 | "@tsconfig/node16" "^1.0.2" 1020 | acorn "^8.4.1" 1021 | acorn-walk "^8.1.1" 1022 | arg "^4.1.0" 1023 | create-require "^1.1.0" 1024 | diff "^4.0.1" 1025 | make-error "^1.1.1" 1026 | v8-compile-cache-lib "^3.0.0" 1027 | yn "3.1.1" 1028 | 1029 | ts-node@^9.0.0: 1030 | version "9.1.1" 1031 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" 1032 | integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== 1033 | dependencies: 1034 | arg "^4.1.0" 1035 | create-require "^1.1.0" 1036 | diff "^4.0.1" 1037 | make-error "^1.1.1" 1038 | source-map-support "^0.5.17" 1039 | yn "3.1.1" 1040 | 1041 | tsconfig-paths@^3.14.1: 1042 | version "3.14.1" 1043 | resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" 1044 | integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== 1045 | dependencies: 1046 | "@types/json5" "^0.0.29" 1047 | json5 "^1.0.1" 1048 | minimist "^1.2.6" 1049 | strip-bom "^3.0.0" 1050 | 1051 | tsconfig@^7.0.0: 1052 | version "7.0.0" 1053 | resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" 1054 | integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== 1055 | dependencies: 1056 | "@types/strip-bom" "^3.0.0" 1057 | "@types/strip-json-comments" "0.0.30" 1058 | strip-bom "^3.0.0" 1059 | strip-json-comments "^2.0.0" 1060 | 1061 | type-is@~1.6.18: 1062 | version "1.6.18" 1063 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1064 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1065 | dependencies: 1066 | media-typer "0.3.0" 1067 | mime-types "~2.1.24" 1068 | 1069 | typescript@^4.6.3: 1070 | version "4.6.3" 1071 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" 1072 | integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== 1073 | 1074 | unpipe@1.0.0, unpipe@~1.0.0: 1075 | version "1.0.0" 1076 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1077 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 1078 | 1079 | url@0.10.3: 1080 | version "0.10.3" 1081 | resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" 1082 | integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= 1083 | dependencies: 1084 | punycode "1.3.2" 1085 | querystring "0.2.0" 1086 | 1087 | util-deprecate@^1.0.1: 1088 | version "1.0.2" 1089 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1090 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 1091 | 1092 | utils-merge@1.0.1: 1093 | version "1.0.1" 1094 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1095 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 1096 | 1097 | uuid@3.3.2: 1098 | version "3.3.2" 1099 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" 1100 | integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== 1101 | 1102 | v8-compile-cache-lib@^3.0.0: 1103 | version "3.0.0" 1104 | resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" 1105 | integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== 1106 | 1107 | vary@~1.1.2: 1108 | version "1.1.2" 1109 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1110 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 1111 | 1112 | winston-transport@^4.5.0: 1113 | version "4.5.0" 1114 | resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" 1115 | integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== 1116 | dependencies: 1117 | logform "^2.3.2" 1118 | readable-stream "^3.6.0" 1119 | triple-beam "^1.3.0" 1120 | 1121 | winston@^3.7.2: 1122 | version "3.7.2" 1123 | resolved "https://registry.yarnpkg.com/winston/-/winston-3.7.2.tgz#95b4eeddbec902b3db1424932ac634f887c400b1" 1124 | integrity sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng== 1125 | dependencies: 1126 | "@dabh/diagnostics" "^2.0.2" 1127 | async "^3.2.3" 1128 | is-stream "^2.0.0" 1129 | logform "^2.4.0" 1130 | one-time "^1.0.0" 1131 | readable-stream "^3.4.0" 1132 | safe-stable-stringify "^2.3.1" 1133 | stack-trace "0.0.x" 1134 | triple-beam "^1.3.0" 1135 | winston-transport "^4.5.0" 1136 | 1137 | wrappy@1: 1138 | version "1.0.2" 1139 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1140 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1141 | 1142 | xml2js@0.4.19: 1143 | version "0.4.19" 1144 | resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" 1145 | integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== 1146 | dependencies: 1147 | sax ">=0.6.0" 1148 | xmlbuilder "~9.0.1" 1149 | 1150 | xmlbuilder@~9.0.1: 1151 | version "9.0.7" 1152 | resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" 1153 | integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= 1154 | 1155 | xtend@^4.0.0: 1156 | version "4.0.2" 1157 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 1158 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== 1159 | 1160 | yn@3.1.1: 1161 | version "3.1.1" 1162 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 1163 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 1164 | --------------------------------------------------------------------------------