From df2f4152f2e061657f8c04fc10fb8b206c7a652b Mon Sep 17 00:00:00 2001 From: WilcoApp Date: Tue, 16 Jan 2024 11:14:02 +0000 Subject: [PATCH] Initial commit --- .github/workflows/wilco-actions.yml | 36 +- backend/.gitignore | 61 +- backend/Dockerfile | 3 +- backend/Dockerfile.aws | 10 - backend/LICENSE | 21 + backend/README.md | 41 +- backend/app.js | 89 - backend/build.gradle | 77 + backend/config/index.js | 3 - backend/config/passport.js | 18 - backend/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + backend/gradlew | 185 + backend/gradlew.bat | 89 + backend/lib/event.js | 25 - backend/models/Comment.js | 22 - backend/models/Item.js | 62 - backend/models/User.js | 130 - backend/package.json | 42 - backend/public/.keep | 0 backend/routes/api/index.js | 23 - backend/routes/api/items.js | 331 -- backend/routes/api/ping.js | 19 - backend/routes/api/profiles.js | 53 - backend/routes/api/tags.js | 12 - backend/routes/api/users.js | 90 - backend/routes/auth.js | 27 - backend/routes/index.js | 13 - backend/scripts/seeds.js | 1 - backend/seeds.sh | 3 - .../io/spring/AnythinkMarketApplication.java | 12 + .../java/io/spring/JacksonCustomizations.java | 44 + .../main/java/io/spring/MyBatisConfig.java | 8 + backend/src/main/java/io/spring/Util.java | 7 + .../main/java/io/spring/api/CommentsApi.java | 94 + .../java/io/spring/api/CurrentUserApi.java | 58 + .../src/main/java/io/spring/api/ItemApi.java | 86 + .../java/io/spring/api/ItemFavoriteApi.java | 59 + .../src/main/java/io/spring/api/ItemsApi.java | 67 + .../src/main/java/io/spring/api/PingApi.java | 21 + .../main/java/io/spring/api/ProfileApi.java | 78 + .../src/main/java/io/spring/api/TagsApi.java | 26 + .../src/main/java/io/spring/api/UsersApi.java | 86 + .../exception/CustomizeExceptionHandler.java | 109 + .../spring/api/exception/ErrorResource.java | 18 + .../exception/ErrorResourceSerializer.java | 42 + .../api/exception/FieldErrorResource.java | 15 + .../InvalidAuthenticationException.java | 8 + .../exception/InvalidRequestException.java | 17 + .../exception/NoAuthorizationException.java | 7 + .../exception/ResourceNotFoundException.java | 7 + .../spring/api/security/JwtTokenFilter.java | 62 + .../api/security/WebSecurityConfig.java | 89 + .../application/CommentQueryService.java | 85 + .../application/CursorPageParameter.java | 40 + .../io/spring/application/CursorPager.java | 44 + .../io/spring/application/DateTimeCursor.java | 23 + .../spring/application/ItemQueryService.java | 185 + .../main/java/io/spring/application/Node.java | 5 + .../main/java/io/spring/application/Page.java | 31 + .../io/spring/application/PageCursor.java | 18 + .../application/ProfileQueryService.java | 35 + .../spring/application/TagsQueryService.java | 16 + .../spring/application/UserQueryService.java | 17 + .../spring/application/data/CommentData.java | 29 + .../io/spring/application/data/ItemData.java | 33 + .../spring/application/data/ItemDataList.java | 20 + .../application/data/ItemFavoriteCount.java | 9 + .../spring/application/data/ProfileData.java | 17 + .../io/spring/application/data/UserData.java | 16 + .../application/data/UserWithToken.java | 20 + .../item/DuplicatedItemConstraint.java | 21 + .../item/DuplicatedItemValidator.java | 17 + .../application/item/ItemCommandService.java | 36 + .../spring/application/item/NewItemParam.java | 27 + .../application/item/UpdateItemParam.java | 16 + .../user/DuplicatedEmailConstraint.java | 16 + .../user/DuplicatedEmailValidator.java | 17 + .../user/DuplicatedUsernameConstraint.java | 16 + .../user/DuplicatedUsernameValidator.java | 17 + .../application/user/RegisterParam.java | 26 + .../application/user/UpdateUserCommand.java | 14 + .../application/user/UpdateUserParam.java | 25 + .../spring/application/user/UserService.java | 106 + .../java/io/spring/core/comment/Comment.java | 26 + .../core/comment/CommentRepository.java | 11 + .../io/spring/core/favorite/ItemFavorite.java | 18 + .../core/favorite/ItemFavoriteRepository.java | 11 + .../main/java/io/spring/core/item/Item.java | 74 + .../io/spring/core/item/ItemRepository.java | 14 + .../main/java/io/spring/core/item/Tag.java | 22 + .../core/service/AuthorizationService.java | 15 + .../io/spring/core/service/JwtService.java | 12 + .../io/spring/core/user/FollowRelation.java | 17 + .../main/java/io/spring/core/user/User.java | 50 + .../io/spring/core/user/UserRepository.java | 21 + .../io/spring/graphql/CommentDatafetcher.java | 122 + .../io/spring/graphql/CommentMutation.java | 66 + .../io/spring/graphql/ItemDatafetcher.java | 378 ++ .../java/io/spring/graphql/ItemMutation.java | 110 + .../java/io/spring/graphql/MeDatafetcher.java | 61 + .../io/spring/graphql/ProfileDatafetcher.java | 71 + .../io/spring/graphql/RelationMutation.java | 65 + .../java/io/spring/graphql/SecurityUtil.java | 19 + .../io/spring/graphql/TagDatafetcher.java | 19 + .../java/io/spring/graphql/UserMutation.java | 93 + .../exception/AuthenticationException.java | 3 + .../GraphQLCustomizeExceptionHandler.java | 114 + .../mybatis/DateTimeHandler.java | 44 + .../mybatis/mapper/CommentMapper.java | 14 + .../mybatis/mapper/ItemFavoriteMapper.java | 14 + .../mybatis/mapper/ItemMapper.java | 29 + .../mybatis/mapper/UserMapper.java | 25 + .../readservice/CommentReadService.java | 18 + .../readservice/ItemFavoritesReadService.java | 19 + .../mybatis/readservice/ItemReadService.java | 42 + .../mybatis/readservice/TagReadService.java | 9 + .../mybatis/readservice/UserReadService.java | 13 + .../UserRelationshipQueryService.java | 16 + .../repository/MyBatisCommentRepository.java | 33 + .../MyBatisItemFavoriteRepository.java | 35 + .../repository/MyBatisItemRepository.java | 59 + .../repository/MyBatisUserRepository.java | 60 + .../service/DefaultJwtService.java | 54 + .../service/SendEventService.java | 77 + .../resources/application-test.properties | 4 + .../src/main/resources/application.properties | 24 + .../db/migration/V1__create_tables.sql | 50 + .../main/resources/mapper/CommentMapper.xml | 35 + .../resources/mapper/CommentReadService.xml | 44 + .../resources/mapper/ItemFavoriteMapper.xml | 21 + .../mapper/ItemFavoritesReadService.xml | 30 + .../src/main/resources/mapper/ItemMapper.xml | 82 + .../main/resources/mapper/ItemReadService.xml | 160 + .../main/resources/mapper/TagReadService.xml | 7 + .../main/resources/mapper/TransferData.xml | 40 + .../src/main/resources/mapper/UserMapper.xml | 61 + .../main/resources/mapper/UserReadService.xml | 10 + .../mapper/UserRelationshipQueryService.xml | 18 + .../src/main/resources/schema/schema.graphqls | 177 + .../AnythinkMarketApplicationTests.java | 13 + .../src/test/java/io/spring/TestHelper.java | 42 + .../java/io/spring/api/CommentsApiTest.java | 141 + .../io/spring/api/CurrentUserApiTest.java | 179 + .../test/java/io/spring/api/ItemApiTest.java | 222 + .../io/spring/api/ItemFavoriteApiTest.java | 103 + .../test/java/io/spring/api/ItemsApiTest.java | 174 + .../java/io/spring/api/ListItemApiTest.java | 72 + .../java/io/spring/api/ProfileApiTest.java | 96 + .../io/spring/api/TestWithCurrentUser.java | 49 + .../test/java/io/spring/api/UsersApiTest.java | 271 ++ .../comment/CommentQueryServiceTest.java | 76 + .../item/ItemQueryServiceTest.java | 226 + .../profile/ProfileQueryServiceTest.java | 30 + .../application/tag/TagsQueryServiceTest.java | 25 + .../java/io/spring/core/item/ItemTest.java | 40 + .../io/spring/infrastructure/DbTestBase.java | 11 + .../comment/MyBatisCommentRepositoryTest.java | 26 + .../MyBatisItemFavoriteRepositoryTest.java | 33 + .../item/ItemRepositoryTransactionTest.java | 40 + .../item/MyBatisItemRepositoryTest.java | 72 + .../service/DefaultJwtServiceTest.java | 42 + .../user/MyBatisUserRepositoryTest.java | 73 + backend/start.sh | 3 - backend/tests/api-tests.postman.json | 1900 --------- backend/tests/env-api-tests.postman.json | 14 - backend/yarn.lock | 3648 ----------------- .../anythink-backend-deployment.yaml | 29 +- charts/templates/database-deployment.yaml | 9 + charts/values.yaml | 12 +- docker-compose.yml | 32 +- 171 files changed, 7323 insertions(+), 6624 deletions(-) delete mode 100644 backend/Dockerfile.aws create mode 100644 backend/LICENSE delete mode 100644 backend/app.js create mode 100644 backend/build.gradle delete mode 100644 backend/config/index.js delete mode 100644 backend/config/passport.js create mode 100644 backend/gradle/wrapper/gradle-wrapper.jar create mode 100644 backend/gradle/wrapper/gradle-wrapper.properties create mode 100755 backend/gradlew create mode 100644 backend/gradlew.bat delete mode 100644 backend/lib/event.js delete mode 100644 backend/models/Comment.js delete mode 100644 backend/models/Item.js delete mode 100644 backend/models/User.js delete mode 100644 backend/package.json delete mode 100644 backend/public/.keep delete mode 100644 backend/routes/api/index.js delete mode 100644 backend/routes/api/items.js delete mode 100644 backend/routes/api/ping.js delete mode 100644 backend/routes/api/profiles.js delete mode 100644 backend/routes/api/tags.js delete mode 100644 backend/routes/api/users.js delete mode 100644 backend/routes/auth.js delete mode 100644 backend/routes/index.js delete mode 100644 backend/scripts/seeds.js delete mode 100755 backend/seeds.sh create mode 100644 backend/src/main/java/io/spring/AnythinkMarketApplication.java create mode 100644 backend/src/main/java/io/spring/JacksonCustomizations.java create mode 100644 backend/src/main/java/io/spring/MyBatisConfig.java create mode 100644 backend/src/main/java/io/spring/Util.java create mode 100644 backend/src/main/java/io/spring/api/CommentsApi.java create mode 100644 backend/src/main/java/io/spring/api/CurrentUserApi.java create mode 100644 backend/src/main/java/io/spring/api/ItemApi.java create mode 100644 backend/src/main/java/io/spring/api/ItemFavoriteApi.java create mode 100644 backend/src/main/java/io/spring/api/ItemsApi.java create mode 100644 backend/src/main/java/io/spring/api/PingApi.java create mode 100644 backend/src/main/java/io/spring/api/ProfileApi.java create mode 100644 backend/src/main/java/io/spring/api/TagsApi.java create mode 100644 backend/src/main/java/io/spring/api/UsersApi.java create mode 100644 backend/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java create mode 100644 backend/src/main/java/io/spring/api/exception/ErrorResource.java create mode 100644 backend/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java create mode 100644 backend/src/main/java/io/spring/api/exception/FieldErrorResource.java create mode 100644 backend/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java create mode 100644 backend/src/main/java/io/spring/api/exception/InvalidRequestException.java create mode 100644 backend/src/main/java/io/spring/api/exception/NoAuthorizationException.java create mode 100644 backend/src/main/java/io/spring/api/exception/ResourceNotFoundException.java create mode 100644 backend/src/main/java/io/spring/api/security/JwtTokenFilter.java create mode 100644 backend/src/main/java/io/spring/api/security/WebSecurityConfig.java create mode 100644 backend/src/main/java/io/spring/application/CommentQueryService.java create mode 100644 backend/src/main/java/io/spring/application/CursorPageParameter.java create mode 100644 backend/src/main/java/io/spring/application/CursorPager.java create mode 100644 backend/src/main/java/io/spring/application/DateTimeCursor.java create mode 100644 backend/src/main/java/io/spring/application/ItemQueryService.java create mode 100644 backend/src/main/java/io/spring/application/Node.java create mode 100644 backend/src/main/java/io/spring/application/Page.java create mode 100644 backend/src/main/java/io/spring/application/PageCursor.java create mode 100644 backend/src/main/java/io/spring/application/ProfileQueryService.java create mode 100644 backend/src/main/java/io/spring/application/TagsQueryService.java create mode 100644 backend/src/main/java/io/spring/application/UserQueryService.java create mode 100644 backend/src/main/java/io/spring/application/data/CommentData.java create mode 100644 backend/src/main/java/io/spring/application/data/ItemData.java create mode 100644 backend/src/main/java/io/spring/application/data/ItemDataList.java create mode 100644 backend/src/main/java/io/spring/application/data/ItemFavoriteCount.java create mode 100644 backend/src/main/java/io/spring/application/data/ProfileData.java create mode 100644 backend/src/main/java/io/spring/application/data/UserData.java create mode 100644 backend/src/main/java/io/spring/application/data/UserWithToken.java create mode 100644 backend/src/main/java/io/spring/application/item/DuplicatedItemConstraint.java create mode 100644 backend/src/main/java/io/spring/application/item/DuplicatedItemValidator.java create mode 100644 backend/src/main/java/io/spring/application/item/ItemCommandService.java create mode 100644 backend/src/main/java/io/spring/application/item/NewItemParam.java create mode 100644 backend/src/main/java/io/spring/application/item/UpdateItemParam.java create mode 100644 backend/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java create mode 100644 backend/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java create mode 100644 backend/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java create mode 100644 backend/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java create mode 100644 backend/src/main/java/io/spring/application/user/RegisterParam.java create mode 100644 backend/src/main/java/io/spring/application/user/UpdateUserCommand.java create mode 100644 backend/src/main/java/io/spring/application/user/UpdateUserParam.java create mode 100644 backend/src/main/java/io/spring/application/user/UserService.java create mode 100644 backend/src/main/java/io/spring/core/comment/Comment.java create mode 100644 backend/src/main/java/io/spring/core/comment/CommentRepository.java create mode 100644 backend/src/main/java/io/spring/core/favorite/ItemFavorite.java create mode 100644 backend/src/main/java/io/spring/core/favorite/ItemFavoriteRepository.java create mode 100644 backend/src/main/java/io/spring/core/item/Item.java create mode 100644 backend/src/main/java/io/spring/core/item/ItemRepository.java create mode 100644 backend/src/main/java/io/spring/core/item/Tag.java create mode 100644 backend/src/main/java/io/spring/core/service/AuthorizationService.java create mode 100644 backend/src/main/java/io/spring/core/service/JwtService.java create mode 100644 backend/src/main/java/io/spring/core/user/FollowRelation.java create mode 100644 backend/src/main/java/io/spring/core/user/User.java create mode 100644 backend/src/main/java/io/spring/core/user/UserRepository.java create mode 100644 backend/src/main/java/io/spring/graphql/CommentDatafetcher.java create mode 100644 backend/src/main/java/io/spring/graphql/CommentMutation.java create mode 100644 backend/src/main/java/io/spring/graphql/ItemDatafetcher.java create mode 100644 backend/src/main/java/io/spring/graphql/ItemMutation.java create mode 100644 backend/src/main/java/io/spring/graphql/MeDatafetcher.java create mode 100644 backend/src/main/java/io/spring/graphql/ProfileDatafetcher.java create mode 100644 backend/src/main/java/io/spring/graphql/RelationMutation.java create mode 100644 backend/src/main/java/io/spring/graphql/SecurityUtil.java create mode 100644 backend/src/main/java/io/spring/graphql/TagDatafetcher.java create mode 100644 backend/src/main/java/io/spring/graphql/UserMutation.java create mode 100644 backend/src/main/java/io/spring/graphql/exception/AuthenticationException.java create mode 100644 backend/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemFavoriteMapper.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemMapper.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemFavoritesReadService.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemReadService.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java create mode 100644 backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java create mode 100644 backend/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java create mode 100644 backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemFavoriteRepository.java create mode 100644 backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemRepository.java create mode 100644 backend/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java create mode 100644 backend/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java create mode 100644 backend/src/main/java/io/spring/infrastructure/service/SendEventService.java create mode 100644 backend/src/main/resources/application-test.properties create mode 100644 backend/src/main/resources/application.properties create mode 100644 backend/src/main/resources/db/migration/V1__create_tables.sql create mode 100644 backend/src/main/resources/mapper/CommentMapper.xml create mode 100644 backend/src/main/resources/mapper/CommentReadService.xml create mode 100644 backend/src/main/resources/mapper/ItemFavoriteMapper.xml create mode 100644 backend/src/main/resources/mapper/ItemFavoritesReadService.xml create mode 100644 backend/src/main/resources/mapper/ItemMapper.xml create mode 100644 backend/src/main/resources/mapper/ItemReadService.xml create mode 100644 backend/src/main/resources/mapper/TagReadService.xml create mode 100644 backend/src/main/resources/mapper/TransferData.xml create mode 100644 backend/src/main/resources/mapper/UserMapper.xml create mode 100644 backend/src/main/resources/mapper/UserReadService.xml create mode 100644 backend/src/main/resources/mapper/UserRelationshipQueryService.xml create mode 100644 backend/src/main/resources/schema/schema.graphqls create mode 100644 backend/src/test/java/io/spring/AnythinkMarketApplicationTests.java create mode 100644 backend/src/test/java/io/spring/TestHelper.java create mode 100644 backend/src/test/java/io/spring/api/CommentsApiTest.java create mode 100644 backend/src/test/java/io/spring/api/CurrentUserApiTest.java create mode 100644 backend/src/test/java/io/spring/api/ItemApiTest.java create mode 100644 backend/src/test/java/io/spring/api/ItemFavoriteApiTest.java create mode 100644 backend/src/test/java/io/spring/api/ItemsApiTest.java create mode 100644 backend/src/test/java/io/spring/api/ListItemApiTest.java create mode 100644 backend/src/test/java/io/spring/api/ProfileApiTest.java create mode 100644 backend/src/test/java/io/spring/api/TestWithCurrentUser.java create mode 100644 backend/src/test/java/io/spring/api/UsersApiTest.java create mode 100644 backend/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java create mode 100644 backend/src/test/java/io/spring/application/item/ItemQueryServiceTest.java create mode 100644 backend/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java create mode 100644 backend/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java create mode 100644 backend/src/test/java/io/spring/core/item/ItemTest.java create mode 100644 backend/src/test/java/io/spring/infrastructure/DbTestBase.java create mode 100644 backend/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java create mode 100644 backend/src/test/java/io/spring/infrastructure/favorite/MyBatisItemFavoriteRepositoryTest.java create mode 100644 backend/src/test/java/io/spring/infrastructure/item/ItemRepositoryTransactionTest.java create mode 100644 backend/src/test/java/io/spring/infrastructure/item/MyBatisItemRepositoryTest.java create mode 100644 backend/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java create mode 100644 backend/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java delete mode 100755 backend/start.sh delete mode 100644 backend/tests/api-tests.postman.json delete mode 100644 backend/tests/env-api-tests.postman.json delete mode 100644 backend/yarn.lock diff --git a/.github/workflows/wilco-actions.yml b/.github/workflows/wilco-actions.yml index 15fa886..14bca61 100644 --- a/.github/workflows/wilco-actions.yml +++ b/.github/workflows/wilco-actions.yml @@ -9,26 +9,46 @@ jobs: timeout-minutes: 10 name: Pr checks + services: + postgres: + image: postgres:13 + env: + POSTGRES_PASSWORD: postgres + SECRET_KEY: secret + POSTGRES_DB: anythink-market + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Check out project uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v3 + - name: Use Java + uses: actions/setup-java@v4 with: - node-version: "16" - - - name: Start MongoDB - uses: supercharge/mongodb-github-action@1.6.0 - with: - mongodb-version: "4.4" + distribution: "adopt" + java-version: "11" - uses: oNaiPs/secrets-to-env-action@v1 with: secrets: ${{ toJSON(secrets) }} + - name: Setup Node for Wilco Checks + uses: actions/setup-node@v3 + with: + node-version: "16" + - name: Wilco checks id: Wilco uses: trywilco/actions@main with: engine: ${{ secrets.WILCO_ENGINE_URL }} + + - name: Print server logs on failure + if: ${{ failure() }} + run: | + cat /tmp/output.log diff --git a/backend/.gitignore b/backend/.gitignore index a812403..dfcbb12 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,37 +1,26 @@ -# Logs -logs -*.log -.DS_Store - -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -node_modules - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - +.gradle/ +/build/ +!gradle/wrapper/gradle-wrapper.jar +*.db + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### .idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ diff --git a/backend/Dockerfile b/backend/Dockerfile index e779cce..30bdf80 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1 +1,2 @@ -FROM public.ecr.aws/v0a2l7y2/wilco/anythink-backend-node:latest +FROM public.ecr.aws/v0a2l7y2/wilco/anythink-backend-java:latest + diff --git a/backend/Dockerfile.aws b/backend/Dockerfile.aws deleted file mode 100644 index 7ce8e4a..0000000 --- a/backend/Dockerfile.aws +++ /dev/null @@ -1,10 +0,0 @@ -FROM node:16 - -WORKDIR /usr/src -COPY backend ./backend -COPY .wilco ./.wilco - -# Pre-install npm packages -WORKDIR /usr/src/backend -RUN yarn install - diff --git a/backend/LICENSE b/backend/LICENSE new file mode 100644 index 0000000..a6fd2b7 --- /dev/null +++ b/backend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Aisensiy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/README.md b/backend/README.md index 14f890a..acbddba 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,22 +1,35 @@ # Anythink Market Backend -The Anythink Market backend is Node web app written with [Express](https://expressjs.com/) +# How it works -## Dependencies +The application uses Spring Boot (Web, Mybatis). -- [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) - For generating JWTs used by authentication -- [mongoose](https://github.com/Automattic/mongoose) - For modeling and mapping MongoDB data to javascript -- [mongoose-unique-validator](https://github.com/blakehaswell/mongoose-unique-validator) - For handling unique validation errors in Mongoose. Mongoose only handles validation at the document level, so a unique index across a collection will throw an exception at the driver level. The `mongoose-unique-validator` plugin helps us by formatting the error like a normal mongoose `ValidationError`. -- [passport](https://github.com/jaredhanson/passport) - For handling user authentication -- [slug](https://github.com/dodo/node-slug) - For encoding titles into a URL-friendly format +And the code is organized as this: -## Application Structure +1. `api` is the web layer implemented by Spring MVC +2. `core` is the business model including entities and services +3. `application` is the high-level services for querying the data transfer objects +4. `infrastructure` contains all the implementation classes as the technique details -- `app.js` - The entry point to our application. This file defines our express server and connects it to MongoDB using mongoose. It also requires the routes and models we'll be using in the application. -- `config/` - This folder contains configuration for passport as well as a central location for configuration/environment variables. -- `routes/` - This folder contains the route definitions for our API. -- `models/` - This folder contains the schema definitions for our Mongoose models. +# Getting started -## Error Handling +You'll need Java 11 installed. -In `routes/api/index.js`, we define a error-handling middleware for handling Mongoose's `ValidationError`. This middleware will respond with a 422 status code and format the response to have [error messages the clients can understand](https://github.com/gothinkster/realworld/blob/master/API.md#errors-and-status-codes) + ./gradlew bootRun + +To test that it works, open a browser tab at http://localhost:3000/api/tags +Alternatively, you can run: + + curl http://localhost:3000/api/tags + +# Run test + +The repository contains a lot of test cases to cover both api test and repository test. + + ./gradlew test + +# Code format + +Use spotless for code format. + + ./gradlew spotlessJavaApply diff --git a/backend/app.js b/backend/app.js deleted file mode 100644 index f4e7f35..0000000 --- a/backend/app.js +++ /dev/null @@ -1,89 +0,0 @@ -require("dotenv").config(); -var http = require("http"), - path = require("path"), - methods = require("methods"), - express = require("express"), - bodyParser = require("body-parser"), - session = require("express-session"), - cors = require("cors"), - passport = require("passport"), - errorhandler = require("errorhandler"), - mongoose = require("mongoose"); - -var isProduction = process.env.NODE_ENV === "production"; - -// Create global app object -var app = express(); - -app.use(cors()); - -// Normal express config defaults -app.use(require("morgan")("dev")); -app.use(bodyParser.urlencoded({ extended: false })); -app.use(bodyParser.json()); - -app.use(require("method-override")()); -app.use(express.static(__dirname + "/public")); - -app.use( - session({ - secret: "secret", - cookie: { maxAge: 60000 }, - resave: false, - saveUninitialized: false - }) -); - -if (!isProduction) { - app.use(errorhandler()); -} - -if (!process.env.MONGODB_URI) { - console.warn("Missing MONGODB_URI in env, please add it to your .env file"); -} - -mongoose.connect(process.env.MONGODB_URI); -if (isProduction) { -} else { - mongoose.set("debug", true); -} - -require("./models/User"); -require("./models/Item"); -require("./models/Comment"); -require("./config/passport"); - -app.use(require("./routes")); - -/// catch 404 and forward to error handler -app.use(function (req, res, next) { - if (req.url === "/favicon.ico") { - res.writeHead(200, { "Content-Type": "image/x-icon" }); - res.end(); - } else { - const err = new Error("Not Found"); - err.status = 404; - next(err); - } -}); - -/// error handler -app.use(function(err, req, res, next) { - console.log(err.stack); - if (isProduction) { - res.sendStatus(err.status || 500) - } else { - res.status(err.status || 500); - res.json({ - errors: { - message: err.message, - error: err - } - }); - } -}); - -// finally, let's start our server... -var server = app.listen(process.env.PORT || 3000, function() { - console.log("Listening on port " + server.address().port); -}); diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 0000000..7991a98 --- /dev/null +++ b/backend/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'org.springframework.boot' version '2.6.3' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' + id "com.netflix.dgs.codegen" version "5.0.6" + id "com.diffplug.spotless" version "6.2.1" +} + +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' +targetCompatibility = '11' + +spotless { + java { + target project.fileTree(project.rootDir) { + include '**/*.java' + exclude 'build/generated/**/*.*', 'build/generated-examples/**/*.*' + } + googleJavaFormat() + } +} + +repositories { + mavenCentral() +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-hateoas' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'com.squareup.okhttp3:okhttp:4.9.1' + implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2' + implementation 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:4.9.21' + implementation 'org.flywaydb:flyway-core' + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', + 'io.jsonwebtoken:jjwt-jackson:0.11.2' + implementation 'joda-time:joda-time:2.10.13' + implementation 'org.xerial:sqlite-jdbc:3.36.0.3' + implementation 'org.postgresql:postgresql:42.2.24' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'io.rest-assured:rest-assured:4.5.1' + testImplementation 'io.rest-assured:json-path:4.5.1' + testImplementation 'io.rest-assured:xml-path:4.5.1' + testImplementation 'io.rest-assured:spring-mock-mvc:4.5.1' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.2.2' + testImplementation 'com.h2database:h2:1.4.200' +} + +tasks.named('test') { + useJUnitPlatform() +} + +tasks.named('clean') { + doFirst { + delete './dev.db' + } +} + +tasks.named('generateJava') { + schemaPaths = ["${projectDir}/src/main/resources/schema"] // List of directories containing schema files + packageName = 'io.spring.graphql' // The package name to use to generate sources +} diff --git a/backend/config/index.js b/backend/config/index.js deleted file mode 100644 index 1bf9d6a..0000000 --- a/backend/config/index.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - secret: process.env.NODE_ENV === 'production' ? process.env.SECRET : 'secret' -}; diff --git a/backend/config/passport.js b/backend/config/passport.js deleted file mode 100644 index abe0ce2..0000000 --- a/backend/config/passport.js +++ /dev/null @@ -1,18 +0,0 @@ -var passport = require('passport'); -var LocalStrategy = require('passport-local').Strategy; -var mongoose = require('mongoose'); -var User = mongoose.model('User'); - -passport.use(new LocalStrategy({ - usernameField: 'user[email]', - passwordField: 'user[password]' -}, function(email, password, done) { - User.findOne({email: email}).then(function(user){ - if(!user || !user.validPassword(password)){ - return done(null, false, {errors: {'email or password': 'is invalid'}}); - } - - return done(null, user); - }).catch(done); -})); - diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..41dfb87 --- /dev/null +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/lib/event.js b/backend/lib/event.js deleted file mode 100644 index 48a270e..0000000 --- a/backend/lib/event.js +++ /dev/null @@ -1,25 +0,0 @@ -const axiosLib = require("axios"); -const fs = require("fs"); - -const WILCO_ID = process.env.WILCO_ID || fs.readFileSync('../.wilco', 'utf8') -const baseURL = process.env.ENGINE_BASE_URL || "https://engine.wilco.gg" - -const axios = axiosLib.create({ - baseURL: baseURL, - headers: { - 'Content-type': 'application/json', - }, -}); - -async function sendEvent(event, metadata) { - try { - const result = await axios.post(`/users/${WILCO_ID}/event`, JSON.stringify({event, metadata})); - return result.data; - } catch (error) { - console.error(`failed to send event ${event} to Wilco engine`) - } -} - -module.exports = { - sendEvent, -} diff --git a/backend/models/Comment.js b/backend/models/Comment.js deleted file mode 100644 index 995c6c0..0000000 --- a/backend/models/Comment.js +++ /dev/null @@ -1,22 +0,0 @@ -var mongoose = require("mongoose"); - -var CommentSchema = new mongoose.Schema( - { - body: String, - seller: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, - item: { type: mongoose.Schema.Types.ObjectId, ref: "Item" } - }, - { timestamps: true } -); - -// Requires population of seller -CommentSchema.methods.toJSONFor = function(user) { - return { - id: this._id, - body: this.body, - createdAt: this.createdAt, - seller: this.seller.toProfileJSONFor(user) - }; -}; - -mongoose.model("Comment", CommentSchema); diff --git a/backend/models/Item.js b/backend/models/Item.js deleted file mode 100644 index 96421a3..0000000 --- a/backend/models/Item.js +++ /dev/null @@ -1,62 +0,0 @@ -var mongoose = require("mongoose"); -var uniqueValidator = require("mongoose-unique-validator"); -var slug = require("slug"); -var User = mongoose.model("User"); - -var ItemSchema = new mongoose.Schema( - { - slug: { type: String, lowercase: true, unique: true }, - title: {type: String, required: [true, "can't be blank"]}, - description: {type: String, required: [true, "can't be blank"]}, - image: String, - favoritesCount: { type: Number, default: 0 }, - comments: [{ type: mongoose.Schema.Types.ObjectId, ref: "Comment" }], - tagList: [{ type: String }], - seller: { type: mongoose.Schema.Types.ObjectId, ref: "User" } - }, - { timestamps: true } -); - -ItemSchema.plugin(uniqueValidator, { message: "is already taken" }); - -ItemSchema.pre("validate", function(next) { - if (!this.slug) { - this.slugify(); - } - - next(); -}); - -ItemSchema.methods.slugify = function() { - this.slug = - slug(this.title) + - "-" + - ((Math.random() * Math.pow(36, 6)) | 0).toString(36); -}; - -ItemSchema.methods.updateFavoriteCount = function() { - var item = this; - - return User.count({ favorites: { $in: [item._id] } }).then(function(count) { - item.favoritesCount = count; - - return item.save(); - }); -}; - -ItemSchema.methods.toJSONFor = function(user) { - return { - slug: this.slug, - title: this.title, - description: this.description, - image: this.image, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - tagList: this.tagList, - favorited: user ? user.isFavorite(this._id) : false, - favoritesCount: this.favoritesCount, - seller: this.seller.toProfileJSONFor(user) - }; -}; - -mongoose.model("Item", ItemSchema); diff --git a/backend/models/User.js b/backend/models/User.js deleted file mode 100644 index 8616f03..0000000 --- a/backend/models/User.js +++ /dev/null @@ -1,130 +0,0 @@ -var mongoose = require("mongoose"); -var uniqueValidator = require("mongoose-unique-validator"); -var crypto = require("crypto"); -var jwt = require("jsonwebtoken"); -var secret = require("../config").secret; - -var UserSchema = new mongoose.Schema( - { - username: { - type: String, - lowercase: true, - unique: true, - required: [true, "can't be blank"], - match: [/^[a-zA-Z0-9]+$/, "is invalid"], - index: true - }, - email: { - type: String, - lowercase: true, - unique: true, - required: [true, "can't be blank"], - match: [/\S+@\S+\.\S+/, "is invalid"], - index: true - }, - bio: String, - image: String, - role: { - type: String, - enum: ["user", "admin"], - default: "user" - }, - favorites: [{ type: mongoose.Schema.Types.ObjectId, ref: "Item" }], - following: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], - hash: String, - salt: String - }, - { timestamps: true } -); - -UserSchema.plugin(uniqueValidator, { message: "is already taken." }); - -UserSchema.methods.validPassword = function(password) { - var hash = crypto - .pbkdf2Sync(password, this.salt, 10000, 512, "sha512") - .toString("hex"); - return this.hash === hash; -}; - -UserSchema.methods.setPassword = function(password) { - this.salt = crypto.randomBytes(16).toString("hex"); - this.hash = crypto - .pbkdf2Sync(password, this.salt, 10000, 512, "sha512") - .toString("hex"); -}; - -UserSchema.methods.generateJWT = function() { - var today = new Date(); - var exp = new Date(today); - exp.setDate(today.getDate() + 60); - - return jwt.sign( - { - id: this._id, - username: this.username, - exp: parseInt(exp.getTime() / 1000) - }, - secret - ); -}; - -UserSchema.methods.toAuthJSON = function() { - return { - username: this.username, - email: this.email, - token: this.generateJWT(), - bio: this.bio, - image: this.image, - role: this.role - }; -}; - -UserSchema.methods.toProfileJSONFor = function(user) { - return { - username: this.username, - bio: this.bio, - image: - this.image || "https://static.productionready.io/images/smiley-cyrus.jpg", - following: user ? user.isFollowing(this._id) : false - }; -}; - -UserSchema.methods.favorite = function(id) { - if (this.favorites.indexOf(id) === -1) { - this.favorites = this.favorites.concat([id]); - } - - return this.save(); -}; - -UserSchema.methods.unfavorite = function(id) { - this.favorites.remove(id); - return this.save(); -}; - -UserSchema.methods.isFavorite = function(id) { - return this.favorites.some(function(favoriteId) { - return favoriteId.toString() === id.toString(); - }); -}; - -UserSchema.methods.follow = function(id) { - if (this.following.indexOf(id) === -1) { - this.following = this.following.concat([id]); - } - - return this.save(); -}; - -UserSchema.methods.unfollow = function(id) { - this.following.remove(id); - return this.save(); -}; - -UserSchema.methods.isFollowing = function(id) { - return this.following.some(function(followId) { - return followId.toString() === id.toString(); - }); -}; - -mongoose.model("User", UserSchema); diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 4dcd6eb..0000000 --- a/backend/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "anythink-market-backend", - "version": "1.0.0", - "main": "app.js", - "engines": { - "node": "^16" - }, - "scripts": { - "start": "node ./app.js", - "dev": "nodemon ./app.js", - "seeds": "node ./scripts/seeds.js", - "test": "newman run ./tests/api-tests.postman.json -e ./tests/env-api-tests.postman.json", - "stop": "lsof -ti :3000 | xargs kill" - }, - "dependencies": { - "axios": "^0.25.0", - "body-parser": "1.15.0", - "cors": "2.7.1", - "dotenv": "^8.2.0", - "ejs": "2.4.1", - "errorhandler": "1.4.3", - "express": "4.13.4", - "express-async-handler": "^1.2.0", - "express-jwt": "3.3.0", - "express-session": "1.13.0", - "jsonwebtoken": "7.1.9", - "method-override": "2.3.5", - "methods": "1.1.2", - "mongoose": "5.12.5", - "mongoose-unique-validator": "^3.0.0", - "morgan": "1.7.0", - "passport": "0.3.2", - "passport-local": "1.0.0", - "request": "2.69.0", - "slug": "0.9.1", - "underscore": "1.8.3" - }, - "devDependencies": { - "newman": "^3.8.2", - "nodemon": "^1.11.0" - } -} diff --git a/backend/public/.keep b/backend/public/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/backend/routes/api/index.js b/backend/routes/api/index.js deleted file mode 100644 index 380d027..0000000 --- a/backend/routes/api/index.js +++ /dev/null @@ -1,23 +0,0 @@ -var router = require('express').Router(); - -router.use('/', require('./users')); -router.use('/profiles', require('./profiles')); -router.use('/items', require('./items')); -router.use('/tags', require('./tags')); -router.use('/ping', require('./ping')); - -router.use(function(err, req, res, next){ - if(err.name === 'ValidationError'){ - return res.status(422).json({ - errors: Object.keys(err.errors).reduce(function(errors, key){ - errors[key] = err.errors[key].message; - - return errors; - }, {}) - }); - } - - return next(err); -}); - -module.exports = router; \ No newline at end of file diff --git a/backend/routes/api/items.js b/backend/routes/api/items.js deleted file mode 100644 index 84a8af9..0000000 --- a/backend/routes/api/items.js +++ /dev/null @@ -1,331 +0,0 @@ -var router = require("express").Router(); -var mongoose = require("mongoose"); -var Item = mongoose.model("Item"); -var Comment = mongoose.model("Comment"); -var User = mongoose.model("User"); -var auth = require("../auth"); -const { sendEvent } = require("../../lib/event"); - -// Preload item objects on routes with ':item' -router.param("item", function(req, res, next, slug) { - Item.findOne({ slug: slug }) - .populate("seller") - .then(function(item) { - if (!item) { - return res.sendStatus(404); - } - - req.item = item; - - return next(); - }) - .catch(next); -}); - -router.param("comment", function(req, res, next, id) { - Comment.findById(id) - .then(function(comment) { - if (!comment) { - return res.sendStatus(404); - } - - req.comment = comment; - - return next(); - }) - .catch(next); -}); - -router.get("/", auth.optional, function(req, res, next) { - var query = {}; - var limit = 100; - var offset = 0; - - if (typeof req.query.limit !== "undefined") { - limit = req.query.limit; - } - - if (typeof req.query.offset !== "undefined") { - offset = req.query.offset; - } - - if (typeof req.query.tag !== "undefined") { - query.tagList = { $in: [req.query.tag] }; - } - - Promise.all([ - req.query.seller ? User.findOne({ username: req.query.seller }) : null, - req.query.favorited ? User.findOne({ username: req.query.favorited }) : null - ]) - .then(function(results) { - var seller = results[0]; - var favoriter = results[1]; - - if (seller) { - query.seller = seller._id; - } - - if (favoriter) { - query._id = { $in: favoriter.favorites }; - } else if (req.query.favorited) { - query._id = { $in: [] }; - } - - return Promise.all([ - Item.find(query) - .limit(Number(limit)) - .skip(Number(offset)) - .sort({ createdAt: "desc" }) - .exec(), - Item.count(query).exec(), - req.payload ? User.findById(req.payload.id) : null - ]).then(async function(results) { - var items = results[0]; - var itemsCount = results[1]; - var user = results[2]; - return res.json({ - items: await Promise.all( - items.map(async function(item) { - item.seller = await User.findById(item.seller); - return item.toJSONFor(user); - }) - ), - itemsCount: itemsCount - }); - }); - }) - .catch(next); -}); - -router.get("/feed", auth.required, function(req, res, next) { - var limit = 20; - var offset = 0; - - if (typeof req.query.limit !== "undefined") { - limit = req.query.limit; - } - - if (typeof req.query.offset !== "undefined") { - offset = req.query.offset; - } - - User.findById(req.payload.id).then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - Promise.all([ - Item.find({ seller: { $in: user.following } }) - .limit(Number(limit)) - .skip(Number(offset)) - .populate("seller") - .exec(), - Item.count({ seller: { $in: user.following } }) - ]) - .then(function(results) { - var items = results[0]; - var itemsCount = results[1]; - - return res.json({ - items: items.map(function(item) { - return item.toJSONFor(user); - }), - itemsCount: itemsCount - }); - }) - .catch(next); - }); -}); - -router.post("/", auth.required, function(req, res, next) { - User.findById(req.payload.id) - .then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - var item = new Item(req.body.item); - - item.seller = user; - - return item.save().then(function() { - sendEvent('item_created', { item: req.body.item }) - return res.json({ item: item.toJSONFor(user) }); - }); - }) - .catch(next); -}); - -// return a item -router.get("/:item", auth.optional, function(req, res, next) { - Promise.all([ - req.payload ? User.findById(req.payload.id) : null, - req.item.populate("seller").execPopulate() - ]) - .then(function(results) { - var user = results[0]; - - return res.json({ item: req.item.toJSONFor(user) }); - }) - .catch(next); -}); - -// update item -router.put("/:item", auth.required, function(req, res, next) { - User.findById(req.payload.id).then(function(user) { - if (req.item.seller._id.toString() === req.payload.id.toString()) { - if (typeof req.body.item.title !== "undefined") { - req.item.title = req.body.item.title; - } - - if (typeof req.body.item.description !== "undefined") { - req.item.description = req.body.item.description; - } - - if (typeof req.body.item.image !== "undefined") { - req.item.image = req.body.item.image; - } - - if (typeof req.body.item.tagList !== "undefined") { - req.item.tagList = req.body.item.tagList; - } - - req.item - .save() - .then(function(item) { - return res.json({ item: item.toJSONFor(user) }); - }) - .catch(next); - } else { - return res.sendStatus(403); - } - }); -}); - -// delete item -router.delete("/:item", auth.required, function(req, res, next) { - User.findById(req.payload.id) - .then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - if (req.item.seller._id.toString() === req.payload.id.toString()) { - return req.item.remove().then(function() { - return res.sendStatus(204); - }); - } else { - return res.sendStatus(403); - } - }) - .catch(next); -}); - -// Favorite an item -router.post("/:item/favorite", auth.required, function(req, res, next) { - var itemId = req.item._id; - - User.findById(req.payload.id) - .then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - return user.favorite(itemId).then(function() { - return req.item.updateFavoriteCount().then(function(item) { - return res.json({ item: item.toJSONFor(user) }); - }); - }); - }) - .catch(next); -}); - -// Unfavorite an item -router.delete("/:item/favorite", auth.required, function(req, res, next) { - var itemId = req.item._id; - - User.findById(req.payload.id) - .then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - return user.unfavorite(itemId).then(function() { - return req.item.updateFavoriteCount().then(function(item) { - return res.json({ item: item.toJSONFor(user) }); - }); - }); - }) - .catch(next); -}); - -// return an item's comments -router.get("/:item/comments", auth.optional, function(req, res, next) { - Promise.resolve(req.payload ? User.findById(req.payload.id) : null) - .then(function(user) { - return req.item - .populate({ - path: "comments", - populate: { - path: "seller" - }, - options: { - sort: { - createdAt: "desc" - } - } - }) - .execPopulate() - .then(function(item) { - return res.json({ - comments: req.item.comments.map(function(comment) { - return comment.toJSONFor(user); - }) - }); - }); - }) - .catch(next); -}); - -// create a new comment -router.post("/:item/comments", auth.required, function(req, res, next) { - User.findById(req.payload.id) - .then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - var comment = new Comment(req.body.comment); - comment.item = req.item; - comment.seller = user; - - return comment.save().then(function() { - req.item.comments = req.item.comments.concat([comment]); - - return req.item.save().then(function(item) { - res.json({ comment: comment.toJSONFor(user) }); - }); - }); - }) - .catch(next); -}); - -router.delete("/:item/comments/:comment", auth.required, function( - req, - res, - next -) { - req.item.comments.remove(req.comment._id); - req.item - .save() - .then( - Comment.find({ _id: req.comment._id }) - .remove() - .exec() - ) - .then(function() { - res.sendStatus(204); - }); -}); - -module.exports = router; diff --git a/backend/routes/api/ping.js b/backend/routes/api/ping.js deleted file mode 100644 index a327948..0000000 --- a/backend/routes/api/ping.js +++ /dev/null @@ -1,19 +0,0 @@ -const router = require("express").Router(); -const asyncHandler = require("express-async-handler"); -const auth = require("../auth"); -const { sendEvent } = require("../../lib/event"); - -router.get("/", - auth.optional, - asyncHandler(async (req, res) => { - - try { - const result = await sendEvent('ping') - return res.json(result); - } catch (e) { - console.error(e) - return res.sendStatus(500); - } - })); - -module.exports = router; diff --git a/backend/routes/api/profiles.js b/backend/routes/api/profiles.js deleted file mode 100644 index ffcd833..0000000 --- a/backend/routes/api/profiles.js +++ /dev/null @@ -1,53 +0,0 @@ -var router = require('express').Router(); -var mongoose = require('mongoose'); -var User = mongoose.model('User'); -var auth = require('../auth'); - -// Preload user profile on routes with ':username' -router.param('username', function(req, res, next, username){ - User.findOne({username: username}).then(function(user){ - if (!user) { return res.sendStatus(404); } - - req.profile = user; - - return next(); - }).catch(next); -}); - -router.get('/:username', auth.optional, function(req, res, next){ - if(req.payload){ - User.findById(req.payload.id).then(function(user){ - if(!user){ return res.json({profile: req.profile.toProfileJSONFor(false)}); } - - return res.json({profile: req.profile.toProfileJSONFor(user)}); - }); - } else { - return res.json({profile: req.profile.toProfileJSONFor(false)}); - } -}); - -router.post('/:username/follow', auth.required, function(req, res, next){ - var profileId = req.profile._id; - - User.findById(req.payload.id).then(function(user){ - if (!user) { return res.sendStatus(401); } - - return user.follow(profileId).then(function(){ - return res.json({profile: req.profile.toProfileJSONFor(user)}); - }); - }).catch(next); -}); - -router.delete('/:username/follow', auth.required, function(req, res, next){ - var profileId = req.profile._id; - - User.findById(req.payload.id).then(function(user){ - if (!user) { return res.sendStatus(401); } - - return user.unfollow(profileId).then(function(){ - return res.json({profile: req.profile.toProfileJSONFor(user)}); - }); - }).catch(next); -}); - -module.exports = router; diff --git a/backend/routes/api/tags.js b/backend/routes/api/tags.js deleted file mode 100644 index 2090495..0000000 --- a/backend/routes/api/tags.js +++ /dev/null @@ -1,12 +0,0 @@ -var router = require('express').Router(); -var mongoose = require('mongoose'); -var Item = mongoose.model('Item'); - -// return a list of tags -router.get('/', function(req, res, next) { - Item.find().distinct('tagList').then(function(tags){ - return res.json({tags: tags}); - }).catch(next); -}); - -module.exports = router; diff --git a/backend/routes/api/users.js b/backend/routes/api/users.js deleted file mode 100644 index aeae77f..0000000 --- a/backend/routes/api/users.js +++ /dev/null @@ -1,90 +0,0 @@ -var mongoose = require("mongoose"); -var router = require("express").Router(); -var passport = require("passport"); -var User = mongoose.model("User"); -var auth = require("../auth"); -const { sendEvent } = require("../../lib/event"); - -router.get("/user", auth.required, function(req, res, next) { - User.findById(req.payload.id) - .then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - return res.json({ user: user.toAuthJSON() }); - }) - .catch(next); -}); - -router.put("/user", auth.required, function(req, res, next) { - User.findById(req.payload.id) - .then(function(user) { - if (!user) { - return res.sendStatus(401); - } - - // only update fields that were actually passed... - if (typeof req.body.user.username !== "undefined") { - user.username = req.body.user.username; - } - if (typeof req.body.user.email !== "undefined") { - user.email = req.body.user.email; - } - if (typeof req.body.user.bio !== "undefined") { - user.bio = req.body.user.bio; - } - if (typeof req.body.user.image !== "undefined") { - user.image = req.body.user.image; - } - if (typeof req.body.user.password !== "undefined") { - user.setPassword(req.body.user.password); - } - - return user.save().then(function() { - return res.json({ user: user.toAuthJSON() }); - }); - }) - .catch(next); -}); - -router.post("/users/login", function(req, res, next) { - if (!req.body.user.email) { - return res.status(422).json({ errors: { email: "can't be blank" } }); - } - - if (!req.body.user.password) { - return res.status(422).json({ errors: { password: "can't be blank" } }); - } - - passport.authenticate("local", { session: false }, function(err, user, info) { - if (err) { - return next(err); - } - - if (user) { - user.token = user.generateJWT(); - return res.json({ user: user.toAuthJSON() }); - } else { - return res.status(422).json(info); - } - })(req, res, next); -}); - -router.post("/users", function(req, res, next) { - var user = new User(); - - user.username = req.body.user.username; - user.email = req.body.user.email; - user.setPassword(req.body.user.password); - - user - .save() - .then(function() { - sendEvent('user_created', { username: req.body.user.username }) - return res.json({ user: user.toAuthJSON() }); - }) - .catch(next); -}); - -module.exports = router; diff --git a/backend/routes/auth.js b/backend/routes/auth.js deleted file mode 100644 index e44a215..0000000 --- a/backend/routes/auth.js +++ /dev/null @@ -1,27 +0,0 @@ -var jwt = require('express-jwt'); -var secret = require('../config').secret; - -function getTokenFromHeader(req){ - if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token' || - req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { - return req.headers.authorization.split(' ')[1]; - } - - return null; -} - -var auth = { - required: jwt({ - secret: secret, - userProperty: 'payload', - getToken: getTokenFromHeader - }), - optional: jwt({ - secret: secret, - userProperty: 'payload', - credentialsRequired: false, - getToken: getTokenFromHeader - }) -}; - -module.exports = auth; diff --git a/backend/routes/index.js b/backend/routes/index.js deleted file mode 100644 index 81d38f9..0000000 --- a/backend/routes/index.js +++ /dev/null @@ -1,13 +0,0 @@ -var router = require('express').Router(); - -router.get('/', (req, res, next) => { - res.send("Anythink backend is up."); -}); - -router.get('/health', (req, res, next) => { - res.sendStatus("200"); -}) - -router.use('/api', require('./api')); - -module.exports = router; diff --git a/backend/scripts/seeds.js b/backend/scripts/seeds.js deleted file mode 100644 index 4989da1..0000000 --- a/backend/scripts/seeds.js +++ /dev/null @@ -1 +0,0 @@ -//TODO: seeds script should come here, so we'll be able to put some data in our local env diff --git a/backend/seeds.sh b/backend/seeds.sh deleted file mode 100755 index 855f73d..0000000 --- a/backend/seeds.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -yarn seeds diff --git a/backend/src/main/java/io/spring/AnythinkMarketApplication.java b/backend/src/main/java/io/spring/AnythinkMarketApplication.java new file mode 100644 index 0000000..8469390 --- /dev/null +++ b/backend/src/main/java/io/spring/AnythinkMarketApplication.java @@ -0,0 +1,12 @@ +package io.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AnythinkMarketApplication { + + public static void main(String[] args) { + SpringApplication.run(AnythinkMarketApplication.class, args); + } +} diff --git a/backend/src/main/java/io/spring/JacksonCustomizations.java b/backend/src/main/java/io/spring/JacksonCustomizations.java new file mode 100644 index 0000000..874a46e --- /dev/null +++ b/backend/src/main/java/io/spring/JacksonCustomizations.java @@ -0,0 +1,44 @@ +package io.spring; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonCustomizations { + + @Bean + public Module anythinkMarkerModules() { + return new AnythinkMarketModules(); + } + + public static class AnythinkMarketModules extends SimpleModule { + public AnythinkMarketModules() { + addSerializer(DateTime.class, new DateTimeSerializer()); + } + } + + public static class DateTimeSerializer extends StdSerializer { + + protected DateTimeSerializer() { + super(DateTime.class); + } + + @Override + public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeString(ISODateTimeFormat.dateTime().withZoneUTC().print(value)); + } + } + } +} diff --git a/backend/src/main/java/io/spring/MyBatisConfig.java b/backend/src/main/java/io/spring/MyBatisConfig.java new file mode 100644 index 0000000..d1f741c --- /dev/null +++ b/backend/src/main/java/io/spring/MyBatisConfig.java @@ -0,0 +1,8 @@ +package io.spring; + +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableTransactionManagement +public class MyBatisConfig {} diff --git a/backend/src/main/java/io/spring/Util.java b/backend/src/main/java/io/spring/Util.java new file mode 100644 index 0000000..d2512ac --- /dev/null +++ b/backend/src/main/java/io/spring/Util.java @@ -0,0 +1,7 @@ +package io.spring; + +public class Util { + public static boolean isEmpty(String value) { + return value == null || value.isEmpty(); + } +} diff --git a/backend/src/main/java/io/spring/api/CommentsApi.java b/backend/src/main/java/io/spring/api/CommentsApi.java new file mode 100644 index 0000000..1d7a483 --- /dev/null +++ b/backend/src/main/java/io/spring/api/CommentsApi.java @@ -0,0 +1,94 @@ +package io.spring.api; + +import com.fasterxml.jackson.annotation.JsonRootName; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.CommentQueryService; +import io.spring.application.data.CommentData; +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.user.User; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/api/items/{slug}/comments") +@AllArgsConstructor +public class CommentsApi { + private ItemRepository itemRepository; + private CommentRepository commentRepository; + private CommentQueryService commentQueryService; + + @PostMapping + public ResponseEntity createComment( + @PathVariable("slug") String slug, + @AuthenticationPrincipal User user, + @Valid @RequestBody NewCommentParam newCommentParam) { + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + Comment comment = new Comment(newCommentParam.getBody(), user.getId(), item.getId()); + commentRepository.save(comment); + return ResponseEntity.status(201) + .body(commentResponse(commentQueryService.findById(comment.getId(), user).get())); + } + + @GetMapping + public ResponseEntity getComments( + @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + List comments = commentQueryService.findByItemId(item.getId(), user); + return ResponseEntity.ok( + new HashMap() { + { + put("comments", comments); + } + }); + } + + @RequestMapping(path = "{id}", method = RequestMethod.DELETE) + public ResponseEntity deleteComment( + @PathVariable("slug") String slug, + @PathVariable("id") String commentId, + @AuthenticationPrincipal User user) { + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + return commentRepository + .findById(item.getId(), commentId) + .map( + comment -> { + commentRepository.remove(comment); + return ResponseEntity.noContent().build(); + }) + .orElseThrow(ResourceNotFoundException::new); + } + + private Map commentResponse(CommentData commentData) { + return new HashMap() { + { + put("comment", commentData); + } + }; + } +} + +@Getter +@NoArgsConstructor +@JsonRootName("comment") +class NewCommentParam { + @NotBlank(message = "can't be empty") + private String body; +} diff --git a/backend/src/main/java/io/spring/api/CurrentUserApi.java b/backend/src/main/java/io/spring/api/CurrentUserApi.java new file mode 100644 index 0000000..3a181f7 --- /dev/null +++ b/backend/src/main/java/io/spring/api/CurrentUserApi.java @@ -0,0 +1,58 @@ +package io.spring.api; + +import io.spring.application.UserQueryService; +import io.spring.application.data.UserData; +import io.spring.application.data.UserWithToken; +import io.spring.application.user.UpdateUserCommand; +import io.spring.application.user.UpdateUserParam; +import io.spring.application.user.UserService; +import io.spring.core.user.User; +import java.util.HashMap; +import java.util.Map; +import javax.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/api/user") +@AllArgsConstructor +public class CurrentUserApi { + + private UserQueryService userQueryService; + private UserService userService; + + @GetMapping + public ResponseEntity currentUser( + @AuthenticationPrincipal User currentUser, + @RequestHeader(value = "Authorization") String authorization) { + UserData userData = userQueryService.findById(currentUser.getId()).get(); + return ResponseEntity.ok( + userResponse(new UserWithToken(userData, authorization.split(" ")[1]))); + } + + @PutMapping + public ResponseEntity updateProfile( + @AuthenticationPrincipal User currentUser, + @RequestHeader("Authorization") String token, + @Valid @RequestBody UpdateUserParam updateUserParam) { + + userService.updateUser(new UpdateUserCommand(currentUser, updateUserParam)); + UserData userData = userQueryService.findById(currentUser.getId()).get(); + return ResponseEntity.ok(userResponse(new UserWithToken(userData, token.split(" ")[1]))); + } + + private Map userResponse(UserWithToken userWithToken) { + return new HashMap() { + { + put("user", userWithToken); + } + }; + } +} diff --git a/backend/src/main/java/io/spring/api/ItemApi.java b/backend/src/main/java/io/spring/api/ItemApi.java new file mode 100644 index 0000000..15b25cc --- /dev/null +++ b/backend/src/main/java/io/spring/api/ItemApi.java @@ -0,0 +1,86 @@ +package io.spring.api; + +import io.spring.api.exception.NoAuthorizationException; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.ItemQueryService; +import io.spring.application.data.ItemData; +import io.spring.application.item.ItemCommandService; +import io.spring.application.item.UpdateItemParam; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.service.AuthorizationService; +import io.spring.core.user.User; +import java.util.HashMap; +import java.util.Map; +import javax.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/api/items/{slug}") +@AllArgsConstructor +public class ItemApi { + private ItemQueryService itemQueryService; + private ItemRepository itemRepository; + private ItemCommandService itemCommandService; + + @GetMapping + public ResponseEntity item( + @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { + return itemQueryService + .findBySlug(slug, user) + .map(itemData -> ResponseEntity.ok(itemResponse(itemData))) + .orElseThrow(ResourceNotFoundException::new); + } + + @PutMapping + public ResponseEntity updateItem( + @PathVariable("slug") String slug, + @AuthenticationPrincipal User user, + @Valid @RequestBody UpdateItemParam updateItemParam) { + return itemRepository + .findBySlug(slug) + .map( + item -> { + if (!AuthorizationService.canWriteItem(user, item)) { + throw new NoAuthorizationException(); + } + Item updatedItem = itemCommandService.updateItem(item, updateItemParam); + return ResponseEntity.ok( + itemResponse(itemQueryService.findBySlug(updatedItem.getSlug(), user).get())); + }) + .orElseThrow(ResourceNotFoundException::new); + } + + @DeleteMapping + public ResponseEntity deleteItem( + @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { + return itemRepository + .findBySlug(slug) + .map( + item -> { + if (!AuthorizationService.canWriteItem(user, item)) { + throw new NoAuthorizationException(); + } + itemRepository.remove(item); + return ResponseEntity.noContent().build(); + }) + .orElseThrow(ResourceNotFoundException::new); + } + + private Map itemResponse(ItemData itemData) { + return new HashMap() { + { + put("item", itemData); + } + }; + } +} diff --git a/backend/src/main/java/io/spring/api/ItemFavoriteApi.java b/backend/src/main/java/io/spring/api/ItemFavoriteApi.java new file mode 100644 index 0000000..48f7a47 --- /dev/null +++ b/backend/src/main/java/io/spring/api/ItemFavoriteApi.java @@ -0,0 +1,59 @@ +package io.spring.api; + +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.ItemQueryService; +import io.spring.application.data.ItemData; +import io.spring.core.favorite.ItemFavorite; +import io.spring.core.favorite.ItemFavoriteRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.user.User; +import java.util.HashMap; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/api/items/{slug}/favorite") +@AllArgsConstructor +public class ItemFavoriteApi { + private ItemFavoriteRepository itemFavoriteRepository; + private ItemRepository itemRepository; + private ItemQueryService itemQueryService; + + @PostMapping + public ResponseEntity favoriteItem( + @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + ItemFavorite itemFavorite = new ItemFavorite(item.getId(), user.getId()); + itemFavoriteRepository.save(itemFavorite); + return responseItemData(itemQueryService.findBySlug(slug, user).get()); + } + + @DeleteMapping + public ResponseEntity unfavoriteItem( + @PathVariable("slug") String slug, @AuthenticationPrincipal User user) { + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + itemFavoriteRepository + .find(item.getId(), user.getId()) + .ifPresent( + favorite -> { + itemFavoriteRepository.remove(favorite); + }); + return responseItemData(itemQueryService.findBySlug(slug, user).get()); + } + + private ResponseEntity> responseItemData(final ItemData itemData) { + return ResponseEntity.ok( + new HashMap() { + { + put("item", itemData); + } + }); + } +} diff --git a/backend/src/main/java/io/spring/api/ItemsApi.java b/backend/src/main/java/io/spring/api/ItemsApi.java new file mode 100644 index 0000000..5714fdd --- /dev/null +++ b/backend/src/main/java/io/spring/api/ItemsApi.java @@ -0,0 +1,67 @@ +package io.spring.api; + +import io.spring.application.ItemQueryService; +import io.spring.application.Page; +import io.spring.application.item.ItemCommandService; +import io.spring.application.item.NewItemParam; +import io.spring.core.item.Item; +import io.spring.core.user.User; +import io.spring.infrastructure.service.SendEventService; +import java.util.HashMap; +import java.util.Map; +import javax.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/api/items") +@AllArgsConstructor +public class ItemsApi { + private ItemCommandService itemCommandService; + private ItemQueryService itemQueryService; + + @PostMapping + public ResponseEntity createItem( + @Valid @RequestBody NewItemParam newItemParam, @AuthenticationPrincipal User user) { + Item item = itemCommandService.createItem(newItemParam, user); + + SendEventService sendEventService = new SendEventService(); + Map metadata = new HashMap<>(); + metadata.put("item", item.getTitle()); + sendEventService.sendEvent("item_created", metadata); + + return ResponseEntity.ok( + new HashMap() { + { + put("item", itemQueryService.findById(item.getId(), user).get()); + } + }); + } + + @GetMapping(path = "feed") + public ResponseEntity getFeed( + @RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "limit", defaultValue = "20") int limit, + @AuthenticationPrincipal User user) { + return ResponseEntity.ok(itemQueryService.findUserFeed(user, new Page(offset, limit))); + } + + @GetMapping + public ResponseEntity getItems( + @RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "limit", defaultValue = "20") int limit, + @RequestParam(value = "tag", required = false) String tag, + @RequestParam(value = "favorited", required = false) String favoritedBy, + @RequestParam(value = "seller", required = false) String seller, + @AuthenticationPrincipal User user) { + return ResponseEntity.ok( + itemQueryService.findRecentItems(tag, seller, favoritedBy, new Page(offset, limit), user)); + } +} diff --git a/backend/src/main/java/io/spring/api/PingApi.java b/backend/src/main/java/io/spring/api/PingApi.java new file mode 100644 index 0000000..6883ac6 --- /dev/null +++ b/backend/src/main/java/io/spring/api/PingApi.java @@ -0,0 +1,21 @@ +package io.spring.api; + +import io.spring.infrastructure.service.SendEventService; +import java.util.HashMap; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "api/ping") +@AllArgsConstructor +public class PingApi { + @GetMapping + public ResponseEntity ping() { + SendEventService sendEventService = new SendEventService(); + String response = sendEventService.sendEvent("ping", new HashMap<>()); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/io/spring/api/ProfileApi.java b/backend/src/main/java/io/spring/api/ProfileApi.java new file mode 100644 index 0000000..61429e1 --- /dev/null +++ b/backend/src/main/java/io/spring/api/ProfileApi.java @@ -0,0 +1,78 @@ +package io.spring.api; + +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.ProfileQueryService; +import io.spring.application.data.ProfileData; +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import java.util.HashMap; +import java.util.Optional; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "api/profiles/{username}") +@AllArgsConstructor +public class ProfileApi { + private ProfileQueryService profileQueryService; + private UserRepository userRepository; + + @GetMapping + public ResponseEntity getProfile( + @PathVariable("username") String username, @AuthenticationPrincipal User user) { + return profileQueryService + .findByUsername(username, user) + .map(this::profileResponse) + .orElseThrow(ResourceNotFoundException::new); + } + + @PostMapping(path = "follow") + public ResponseEntity follow( + @PathVariable("username") String username, @AuthenticationPrincipal User user) { + return userRepository + .findByUsername(username) + .map( + target -> { + FollowRelation followRelation = new FollowRelation(user.getId(), target.getId()); + userRepository.saveRelation(followRelation); + return profileResponse(profileQueryService.findByUsername(username, user).get()); + }) + .orElseThrow(ResourceNotFoundException::new); + } + + @DeleteMapping(path = "follow") + public ResponseEntity unfollow( + @PathVariable("username") String username, @AuthenticationPrincipal User user) { + Optional userOptional = userRepository.findByUsername(username); + if (userOptional.isPresent()) { + User target = userOptional.get(); + return userRepository + .findRelation(user.getId(), target.getId()) + .map( + relation -> { + userRepository.removeRelation(relation); + return profileResponse(profileQueryService.findByUsername(username, user).get()); + }) + .orElseThrow(ResourceNotFoundException::new); + } else { + throw new ResourceNotFoundException(); + } + } + + private ResponseEntity profileResponse(ProfileData profile) { + return ResponseEntity.ok( + new HashMap() { + { + put("profile", profile); + } + }); + } +} diff --git a/backend/src/main/java/io/spring/api/TagsApi.java b/backend/src/main/java/io/spring/api/TagsApi.java new file mode 100644 index 0000000..9720f37 --- /dev/null +++ b/backend/src/main/java/io/spring/api/TagsApi.java @@ -0,0 +1,26 @@ +package io.spring.api; + +import io.spring.application.TagsQueryService; +import java.util.HashMap; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "api/tags") +@AllArgsConstructor +public class TagsApi { + private TagsQueryService tagsQueryService; + + @GetMapping + public ResponseEntity getTags() { + return ResponseEntity.ok( + new HashMap() { + { + put("tags", tagsQueryService.allTags()); + } + }); + } +} diff --git a/backend/src/main/java/io/spring/api/UsersApi.java b/backend/src/main/java/io/spring/api/UsersApi.java new file mode 100644 index 0000000..e849bd1 --- /dev/null +++ b/backend/src/main/java/io/spring/api/UsersApi.java @@ -0,0 +1,86 @@ +package io.spring.api; + +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +import com.fasterxml.jackson.annotation.JsonRootName; +import io.spring.api.exception.InvalidAuthenticationException; +import io.spring.application.UserQueryService; +import io.spring.application.data.UserData; +import io.spring.application.data.UserWithToken; +import io.spring.application.user.RegisterParam; +import io.spring.application.user.UserService; +import io.spring.core.service.JwtService; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.service.SendEventService; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class UsersApi { + private UserRepository userRepository; + private UserQueryService userQueryService; + private PasswordEncoder passwordEncoder; + private JwtService jwtService; + private UserService userService; + + @RequestMapping(path = "/api/users", method = POST) + public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam) { + User user = userService.createUser(registerParam); + + SendEventService sendEventService = new SendEventService(); + Map metadata = new HashMap<>(); + metadata.put("username", user.getUsername()); + sendEventService.sendEvent("user_created", metadata); + + UserData userData = userQueryService.findById(user.getId()).get(); + return ResponseEntity.status(201) + .body(userResponse(new UserWithToken(userData, jwtService.toToken(user)))); + } + + @RequestMapping(path = "/api/users/login", method = POST) + public ResponseEntity userLogin(@Valid @RequestBody LoginParam loginParam) { + Optional optional = userRepository.findByEmail(loginParam.getEmail()); + if (optional.isPresent() + && passwordEncoder.matches(loginParam.getPassword(), optional.get().getPassword())) { + UserData userData = userQueryService.findById(optional.get().getId()).get(); + return ResponseEntity.ok( + userResponse(new UserWithToken(userData, jwtService.toToken(optional.get())))); + } else { + throw new InvalidAuthenticationException(); + } + } + + private Map userResponse(UserWithToken userWithToken) { + return new HashMap() { + { + put("user", userWithToken); + } + }; + } +} + +@Getter +@JsonRootName("user") +@NoArgsConstructor +class LoginParam { + @NotBlank(message = "can't be empty") + @Email(message = "should be an email") + private String email; + + @NotBlank(message = "can't be empty") + private String password; +} diff --git a/backend/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java b/backend/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java new file mode 100644 index 0000000..ade3ff4 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/CustomizeExceptionHandler.java @@ -0,0 +1,109 @@ +package io.spring.api.exception; + +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@RestControllerAdvice +public class CustomizeExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({InvalidRequestException.class}) + public ResponseEntity handleInvalidRequest(RuntimeException e, WebRequest request) { + InvalidRequestException ire = (InvalidRequestException) e; + + List errorResources = + ire.getErrors().getFieldErrors().stream() + .map( + fieldError -> + new FieldErrorResource( + fieldError.getObjectName(), + fieldError.getField(), + fieldError.getCode(), + fieldError.getDefaultMessage())) + .collect(Collectors.toList()); + + ErrorResource error = new ErrorResource(errorResources); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + return handleExceptionInternal(e, error, headers, UNPROCESSABLE_ENTITY, request); + } + + @ExceptionHandler(InvalidAuthenticationException.class) + public ResponseEntity handleInvalidAuthentication( + InvalidAuthenticationException e, WebRequest request) { + return ResponseEntity.status(UNPROCESSABLE_ENTITY) + .body( + new HashMap() { + { + put("message", e.getMessage()); + } + }); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, + HttpHeaders headers, + HttpStatus status, + WebRequest request) { + List errorResources = + e.getBindingResult().getFieldErrors().stream() + .map( + fieldError -> + new FieldErrorResource( + fieldError.getObjectName(), + fieldError.getField(), + fieldError.getCode(), + fieldError.getDefaultMessage())) + .collect(Collectors.toList()); + + return ResponseEntity.status(UNPROCESSABLE_ENTITY).body(new ErrorResource(errorResources)); + } + + @ExceptionHandler({ConstraintViolationException.class}) + @ResponseStatus(UNPROCESSABLE_ENTITY) + @ResponseBody + public ErrorResource handleConstraintViolation( + ConstraintViolationException ex, WebRequest request) { + List errors = new ArrayList<>(); + for (ConstraintViolation violation : ex.getConstraintViolations()) { + FieldErrorResource fieldErrorResource = + new FieldErrorResource( + violation.getRootBeanClass().getName(), + getParam(violation.getPropertyPath().toString()), + violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), + violation.getMessage()); + errors.add(fieldErrorResource); + } + + return new ErrorResource(errors); + } + + private String getParam(String s) { + String[] splits = s.split("\\."); + if (splits.length == 1) { + return s; + } else { + return String.join(".", Arrays.copyOfRange(splits, 2, splits.length)); + } + } +} diff --git a/backend/src/main/java/io/spring/api/exception/ErrorResource.java b/backend/src/main/java/io/spring/api/exception/ErrorResource.java new file mode 100644 index 0000000..1c27805 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/ErrorResource.java @@ -0,0 +1,18 @@ +package io.spring.api.exception; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.List; + +@JsonSerialize(using = ErrorResourceSerializer.class) +@JsonIgnoreProperties(ignoreUnknown = true) +@lombok.Getter +@JsonRootName("errors") +public class ErrorResource { + private List fieldErrors; + + public ErrorResource(List fieldErrorResources) { + this.fieldErrors = fieldErrorResources; + } +} diff --git a/backend/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java b/backend/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java new file mode 100644 index 0000000..2ce3816 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/ErrorResourceSerializer.java @@ -0,0 +1,42 @@ +package io.spring.api.exception; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ErrorResourceSerializer extends JsonSerializer { + @Override + public void serialize(ErrorResource value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { + Map> json = new HashMap<>(); + gen.writeStartObject(); + gen.writeObjectFieldStart("errors"); + for (FieldErrorResource fieldErrorResource : value.getFieldErrors()) { + if (!json.containsKey(fieldErrorResource.getField())) { + json.put(fieldErrorResource.getField(), new ArrayList()); + } + json.get(fieldErrorResource.getField()).add(fieldErrorResource.getMessage()); + } + for (Map.Entry> pair : json.entrySet()) { + gen.writeArrayFieldStart(pair.getKey()); + pair.getValue() + .forEach( + content -> { + try { + gen.writeString(content); + } catch (IOException e) { + e.printStackTrace(); + } + }); + gen.writeEndArray(); + } + gen.writeEndObject(); + gen.writeEndObject(); + } +} diff --git a/backend/src/main/java/io/spring/api/exception/FieldErrorResource.java b/backend/src/main/java/io/spring/api/exception/FieldErrorResource.java new file mode 100644 index 0000000..13d5731 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/FieldErrorResource.java @@ -0,0 +1,15 @@ +package io.spring.api.exception; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Getter +@AllArgsConstructor +public class FieldErrorResource { + private String resource; + private String field; + private String code; + private String message; +} diff --git a/backend/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java b/backend/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java new file mode 100644 index 0000000..96af7a8 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/InvalidAuthenticationException.java @@ -0,0 +1,8 @@ +package io.spring.api.exception; + +public class InvalidAuthenticationException extends RuntimeException { + + public InvalidAuthenticationException() { + super("invalid email or password"); + } +} diff --git a/backend/src/main/java/io/spring/api/exception/InvalidRequestException.java b/backend/src/main/java/io/spring/api/exception/InvalidRequestException.java new file mode 100644 index 0000000..68b6c86 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/InvalidRequestException.java @@ -0,0 +1,17 @@ +package io.spring.api.exception; + +import org.springframework.validation.Errors; + +@SuppressWarnings("serial") +public class InvalidRequestException extends RuntimeException { + private final Errors errors; + + public InvalidRequestException(Errors errors) { + super(""); + this.errors = errors; + } + + public Errors getErrors() { + return errors; + } +} diff --git a/backend/src/main/java/io/spring/api/exception/NoAuthorizationException.java b/backend/src/main/java/io/spring/api/exception/NoAuthorizationException.java new file mode 100644 index 0000000..6741423 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/NoAuthorizationException.java @@ -0,0 +1,7 @@ +package io.spring.api.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.FORBIDDEN) +public class NoAuthorizationException extends RuntimeException {} diff --git a/backend/src/main/java/io/spring/api/exception/ResourceNotFoundException.java b/backend/src/main/java/io/spring/api/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..8401e52 --- /dev/null +++ b/backend/src/main/java/io/spring/api/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package io.spring.api.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException {} diff --git a/backend/src/main/java/io/spring/api/security/JwtTokenFilter.java b/backend/src/main/java/io/spring/api/security/JwtTokenFilter.java new file mode 100644 index 0000000..1b5c501 --- /dev/null +++ b/backend/src/main/java/io/spring/api/security/JwtTokenFilter.java @@ -0,0 +1,62 @@ +package io.spring.api.security; + +import io.spring.core.service.JwtService; +import io.spring.core.user.UserRepository; +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +@SuppressWarnings("SpringJavaAutowiringInspection") +public class JwtTokenFilter extends OncePerRequestFilter { + @Autowired private UserRepository userRepository; + @Autowired private JwtService jwtService; + private final String header = "Authorization"; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + getTokenString(request.getHeader(header)) + .flatMap(token -> jwtService.getSubFromToken(token)) + .ifPresent( + id -> { + if (SecurityContextHolder.getContext().getAuthentication() == null) { + userRepository + .findById(id) + .ifPresent( + user -> { + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken( + user, null, Collections.emptyList()); + authenticationToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + }); + } + }); + + filterChain.doFilter(request, response); + } + + private Optional getTokenString(String header) { + if (header == null) { + return Optional.empty(); + } else { + String[] split = header.split(" "); + if (split.length < 2) { + return Optional.empty(); + } else { + return Optional.ofNullable(split[1]); + } + } + } +} diff --git a/backend/src/main/java/io/spring/api/security/WebSecurityConfig.java b/backend/src/main/java/io/spring/api/security/WebSecurityConfig.java new file mode 100644 index 0000000..94b3ae4 --- /dev/null +++ b/backend/src/main/java/io/spring/api/security/WebSecurityConfig.java @@ -0,0 +1,89 @@ +package io.spring.api.security; + +import static java.util.Arrays.asList; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Bean + public JwtTokenFilter jwtTokenFilter() { + return new JwtTokenFilter(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http.csrf() + .disable() + .cors() + .and() + .exceptionHandling() + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers(HttpMethod.OPTIONS) + .permitAll() + .antMatchers("/graphiql") + .permitAll() + .antMatchers("/actuator/**") + .permitAll() + .antMatchers("/health") + .permitAll() + .antMatchers("/api/ping") + .permitAll() + .antMatchers("/graphql") + .permitAll() + .antMatchers(HttpMethod.GET, "/api/items/feed") + .authenticated() + .antMatchers(HttpMethod.POST, "/api/users", "/api/users/login") + .permitAll() + .antMatchers(HttpMethod.GET, "/api/items/**", "/api/profiles/**", "/api/tags") + .permitAll() + .anyRequest() + .authenticated(); + + http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + final CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(asList("*")); + configuration.setAllowedMethods(asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH")); + // setAllowCredentials(true) is important, otherwise: + // The value of the 'Access-Control-Allow-Origin' header in the response must not be the + // wildcard '*' when the request's credentials mode is 'include'. + configuration.setAllowCredentials(false); + // setAllowedHeaders is important! Without it, OPTIONS preflight request + // will fail with 403 Invalid CORS request + configuration.setAllowedHeaders(asList("Authorization", "Cache-Control", "Content-Type")); + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/backend/src/main/java/io/spring/application/CommentQueryService.java b/backend/src/main/java/io/spring/application/CommentQueryService.java new file mode 100644 index 0000000..dfba6ef --- /dev/null +++ b/backend/src/main/java/io/spring/application/CommentQueryService.java @@ -0,0 +1,85 @@ +package io.spring.application; + +import io.spring.application.data.CommentData; +import io.spring.core.user.User; +import io.spring.infrastructure.mybatis.readservice.CommentReadService; +import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.joda.time.DateTime; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class CommentQueryService { + private CommentReadService commentReadService; + private UserRelationshipQueryService userRelationshipQueryService; + + public Optional findById(String id, User user) { + CommentData commentData = commentReadService.findById(id); + if (commentData == null) { + return Optional.empty(); + } else { + commentData + .getProfileData() + .setFollowing( + userRelationshipQueryService.isUserFollowing( + user.getId(), commentData.getProfileData().getId())); + } + return Optional.ofNullable(commentData); + } + + public List findByItemId(String itemId, User user) { + List comments = commentReadService.findByItemId(itemId); + if (comments.size() > 0 && user != null) { + Set followingSellers = + userRelationshipQueryService.followingSellers( + user.getId(), + comments.stream() + .map(commentData -> commentData.getProfileData().getId()) + .collect(Collectors.toList())); + comments.forEach( + commentData -> { + if (followingSellers.contains(commentData.getProfileData().getId())) { + commentData.getProfileData().setFollowing(true); + } + }); + } + return comments; + } + + public CursorPager findByItemIdWithCursor( + String itemId, User user, CursorPageParameter page) { + List comments = commentReadService.findByItemIdWithCursor(itemId, page); + if (comments.isEmpty()) { + return new CursorPager<>(new ArrayList<>(), page.getDirection(), false); + } + if (user != null) { + Set followingSellers = + userRelationshipQueryService.followingSellers( + user.getId(), + comments.stream() + .map(commentData -> commentData.getProfileData().getId()) + .collect(Collectors.toList())); + comments.forEach( + commentData -> { + if (followingSellers.contains(commentData.getProfileData().getId())) { + commentData.getProfileData().setFollowing(true); + } + }); + } + boolean hasExtra = comments.size() > page.getLimit(); + if (hasExtra) { + comments.remove(page.getLimit()); + } + if (!page.isNext()) { + Collections.reverse(comments); + } + return new CursorPager<>(comments, page.getDirection(), hasExtra); + } +} diff --git a/backend/src/main/java/io/spring/application/CursorPageParameter.java b/backend/src/main/java/io/spring/application/CursorPageParameter.java new file mode 100644 index 0000000..1953137 --- /dev/null +++ b/backend/src/main/java/io/spring/application/CursorPageParameter.java @@ -0,0 +1,40 @@ +package io.spring.application; + +import io.spring.application.CursorPager.Direction; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class CursorPageParameter { + private static final int MAX_LIMIT = 1000; + private int limit = 20; + private T cursor; + private Direction direction; + + public CursorPageParameter(T cursor, int limit, Direction direction) { + setLimit(limit); + setCursor(cursor); + setDirection(direction); + } + + public boolean isNext() { + return direction == Direction.NEXT; + } + + public int getQueryLimit() { + return limit + 1; + } + + private void setCursor(T cursor) { + this.cursor = cursor; + } + + private void setLimit(int limit) { + if (limit > MAX_LIMIT) { + this.limit = MAX_LIMIT; + } else if (limit > 0) { + this.limit = limit; + } + } +} diff --git a/backend/src/main/java/io/spring/application/CursorPager.java b/backend/src/main/java/io/spring/application/CursorPager.java new file mode 100644 index 0000000..13d55d4 --- /dev/null +++ b/backend/src/main/java/io/spring/application/CursorPager.java @@ -0,0 +1,44 @@ +package io.spring.application; + +import java.util.List; +import lombok.Getter; + +@Getter +public class CursorPager { + private List data; + private boolean next; + private boolean previous; + + public CursorPager(List data, Direction direction, boolean hasExtra) { + this.data = data; + + if (direction == Direction.NEXT) { + this.previous = false; + this.next = hasExtra; + } else { + this.next = false; + this.previous = hasExtra; + } + } + + public boolean hasNext() { + return next; + } + + public boolean hasPrevious() { + return previous; + } + + public PageCursor getStartCursor() { + return data.isEmpty() ? null : data.get(0).getCursor(); + } + + public PageCursor getEndCursor() { + return data.isEmpty() ? null : data.get(data.size() - 1).getCursor(); + } + + public enum Direction { + PREV, + NEXT + } +} diff --git a/backend/src/main/java/io/spring/application/DateTimeCursor.java b/backend/src/main/java/io/spring/application/DateTimeCursor.java new file mode 100644 index 0000000..cfcc86b --- /dev/null +++ b/backend/src/main/java/io/spring/application/DateTimeCursor.java @@ -0,0 +1,23 @@ +package io.spring.application; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +public class DateTimeCursor extends PageCursor { + + public DateTimeCursor(DateTime data) { + super(data); + } + + @Override + public String toString() { + return String.valueOf(getData().getMillis()); + } + + public static DateTime parse(String cursor) { + if (cursor == null) { + return null; + } + return new DateTime().withMillis(Long.parseLong(cursor)).withZone(DateTimeZone.UTC); + } +} diff --git a/backend/src/main/java/io/spring/application/ItemQueryService.java b/backend/src/main/java/io/spring/application/ItemQueryService.java new file mode 100644 index 0000000..d78a366 --- /dev/null +++ b/backend/src/main/java/io/spring/application/ItemQueryService.java @@ -0,0 +1,185 @@ +package io.spring.application; + +import static java.util.stream.Collectors.toList; + +import io.spring.application.data.ItemData; +import io.spring.application.data.ItemDataList; +import io.spring.application.data.ItemFavoriteCount; +import io.spring.core.user.User; +import io.spring.infrastructure.mybatis.readservice.ItemFavoritesReadService; +import io.spring.infrastructure.mybatis.readservice.ItemReadService; +import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import lombok.AllArgsConstructor; +import org.joda.time.DateTime; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class ItemQueryService { + private ItemReadService itemReadService; + private UserRelationshipQueryService userRelationshipQueryService; + private ItemFavoritesReadService itemFavoritesReadService; + + public Optional findById(String id, User user) { + ItemData itemData = itemReadService.findById(id); + if (itemData == null) { + return Optional.empty(); + } else { + if (user != null) { + fillExtraInfo(id, user, itemData); + } + return Optional.of(itemData); + } + } + + public Optional findBySlug(String slug, User user) { + ItemData itemData = itemReadService.findBySlug(slug); + if (itemData == null) { + return Optional.empty(); + } else { + if (user != null) { + fillExtraInfo(itemData.getId(), user, itemData); + } + + setFavoriteCount(Collections.singletonList(itemData)); + return Optional.of(itemData); + } + } + + public CursorPager findRecentItemsWithCursor( + String tag, + String seller, + String favoritedBy, + CursorPageParameter page, + User currentUser) { + List itemIds = itemReadService.findItemsWithCursor(tag, seller, favoritedBy, page); + if (itemIds.size() == 0) { + return new CursorPager<>(new ArrayList<>(), page.getDirection(), false); + } else { + boolean hasExtra = itemIds.size() > page.getLimit(); + if (hasExtra) { + itemIds.remove(page.getLimit()); + } + if (!page.isNext()) { + Collections.reverse(itemIds); + } + + List items = itemReadService.findItems(itemIds); + fillExtraInfo(items, currentUser); + + return new CursorPager<>(items, page.getDirection(), hasExtra); + } + } + + public CursorPager findUserFeedWithCursor( + User user, CursorPageParameter page) { + List followedUsers = userRelationshipQueryService.followedUsers(user.getId()); + if (followedUsers.size() == 0) { + return new CursorPager<>(new ArrayList<>(), page.getDirection(), false); + } else { + List items = itemReadService.findItemsOfSellersWithCursor(followedUsers, page); + boolean hasExtra = items.size() > page.getLimit(); + if (hasExtra) { + items.remove(page.getLimit()); + } + if (!page.isNext()) { + Collections.reverse(items); + } + fillExtraInfo(items, user); + return new CursorPager<>(items, page.getDirection(), hasExtra); + } + } + + public ItemDataList findRecentItems( + String tag, String seller, String favoritedBy, Page page, User currentUser) { + List itemIds = itemReadService.queryItems(tag, seller, favoritedBy, page); + int itemCount = itemReadService.countItem(tag, seller, favoritedBy); + if (itemIds.size() == 0) { + return new ItemDataList(new ArrayList<>(), itemCount); + } else { + List items = itemReadService.findItems(itemIds); + fillExtraInfo(items, currentUser); + return new ItemDataList(items, itemCount); + } + } + + public ItemDataList findUserFeed(User user, Page page) { + List followedUsers = userRelationshipQueryService.followedUsers(user.getId()); + if (followedUsers.size() == 0) { + return new ItemDataList(new ArrayList<>(), 0); + } else { + List items = itemReadService.findItemsOfSellers(followedUsers, page); + int offset = page.getOffset(); + int limit = page.getLimit(); + int endIndex = Math.min(offset + limit, items.size()); + items = items.subList(offset, endIndex); + + fillExtraInfo(items, user); + int count = itemReadService.countFeedSize(followedUsers); + return new ItemDataList(items, count); + } + } + + private void fillExtraInfo(List items, User currentUser) { + setFavoriteCount(items); + if (currentUser != null) { + setIsFavorite(items, currentUser); + setIsFollowingSeller(items, currentUser); + } + } + + private void setIsFollowingSeller(List items, User currentUser) { + Set followingSellers = + userRelationshipQueryService.followingSellers( + currentUser.getId(), + items.stream().map(itemData1 -> itemData1.getProfileData().getId()).collect(toList())); + items.forEach( + itemData -> { + if (followingSellers.contains(itemData.getProfileData().getId())) { + itemData.getProfileData().setFollowing(true); + } + }); + } + + private void setFavoriteCount(List items) { + List favoritesCounts = + itemFavoritesReadService.itemsFavoriteCount( + items.stream().map(ItemData::getId).collect(toList())); + Map countMap = new HashMap<>(); + favoritesCounts.forEach( + item -> { + countMap.put(item.getId(), item.getCount()); + }); + items.forEach(itemData -> itemData.setFavoritesCount(countMap.get(itemData.getId()))); + } + + private void setIsFavorite(List items, User currentUser) { + Set favoritedItems = + itemFavoritesReadService.userFavorites( + items.stream().map(itemData -> itemData.getId()).collect(toList()), currentUser); + + items.forEach( + itemData -> { + if (favoritedItems.contains(itemData.getId())) { + itemData.setFavorited(true); + } + }); + } + + private void fillExtraInfo(String id, User user, ItemData itemData) { + itemData.setFavorited(itemFavoritesReadService.isUserFavorite(user.getId(), id)); + itemData.setFavoritesCount(itemFavoritesReadService.itemFavoriteCount(id)); + itemData + .getProfileData() + .setFollowing( + userRelationshipQueryService.isUserFollowing( + user.getId(), itemData.getProfileData().getId())); + } +} diff --git a/backend/src/main/java/io/spring/application/Node.java b/backend/src/main/java/io/spring/application/Node.java new file mode 100644 index 0000000..e4ccac8 --- /dev/null +++ b/backend/src/main/java/io/spring/application/Node.java @@ -0,0 +1,5 @@ +package io.spring.application; + +public interface Node { + PageCursor getCursor(); +} diff --git a/backend/src/main/java/io/spring/application/Page.java b/backend/src/main/java/io/spring/application/Page.java new file mode 100644 index 0000000..d273e99 --- /dev/null +++ b/backend/src/main/java/io/spring/application/Page.java @@ -0,0 +1,31 @@ +package io.spring.application; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +public class Page { + private static final int MAX_LIMIT = 100; + private int offset = 0; + private int limit = 20; + + public Page(int offset, int limit) { + setOffset(offset); + setLimit(limit); + } + + private void setOffset(int offset) { + if (offset > 0) { + this.offset = offset; + } + } + + private void setLimit(int limit) { + if (limit > MAX_LIMIT) { + this.limit = MAX_LIMIT; + } else if (limit > 0) { + this.limit = limit; + } + } +} diff --git a/backend/src/main/java/io/spring/application/PageCursor.java b/backend/src/main/java/io/spring/application/PageCursor.java new file mode 100644 index 0000000..0279f3b --- /dev/null +++ b/backend/src/main/java/io/spring/application/PageCursor.java @@ -0,0 +1,18 @@ +package io.spring.application; + +public abstract class PageCursor { + private T data; + + public PageCursor(T data) { + this.data = data; + } + + public T getData() { + return data; + } + + @Override + public String toString() { + return data.toString(); + } +} diff --git a/backend/src/main/java/io/spring/application/ProfileQueryService.java b/backend/src/main/java/io/spring/application/ProfileQueryService.java new file mode 100644 index 0000000..d92542d --- /dev/null +++ b/backend/src/main/java/io/spring/application/ProfileQueryService.java @@ -0,0 +1,35 @@ +package io.spring.application; + +import io.spring.application.data.ProfileData; +import io.spring.application.data.UserData; +import io.spring.core.user.User; +import io.spring.infrastructure.mybatis.readservice.UserReadService; +import io.spring.infrastructure.mybatis.readservice.UserRelationshipQueryService; +import java.util.Optional; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public class ProfileQueryService { + private UserReadService userReadService; + private UserRelationshipQueryService userRelationshipQueryService; + + public Optional findByUsername(String username, User currentUser) { + UserData userData = userReadService.findByUsername(username); + if (userData == null) { + return Optional.empty(); + } else { + ProfileData profileData = + new ProfileData( + userData.getId(), + userData.getUsername(), + userData.getBio(), + userData.getImage(), + currentUser != null + && userRelationshipQueryService.isUserFollowing( + currentUser.getId(), userData.getId())); + return Optional.of(profileData); + } + } +} diff --git a/backend/src/main/java/io/spring/application/TagsQueryService.java b/backend/src/main/java/io/spring/application/TagsQueryService.java new file mode 100644 index 0000000..12e0790 --- /dev/null +++ b/backend/src/main/java/io/spring/application/TagsQueryService.java @@ -0,0 +1,16 @@ +package io.spring.application; + +import io.spring.infrastructure.mybatis.readservice.TagReadService; +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class TagsQueryService { + private TagReadService tagReadService; + + public List allTags() { + return tagReadService.all(); + } +} diff --git a/backend/src/main/java/io/spring/application/UserQueryService.java b/backend/src/main/java/io/spring/application/UserQueryService.java new file mode 100644 index 0000000..f0f901a --- /dev/null +++ b/backend/src/main/java/io/spring/application/UserQueryService.java @@ -0,0 +1,17 @@ +package io.spring.application; + +import io.spring.application.data.UserData; +import io.spring.infrastructure.mybatis.readservice.UserReadService; +import java.util.Optional; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class UserQueryService { + private UserReadService userReadService; + + public Optional findById(String id) { + return Optional.ofNullable(userReadService.findById(id)); + } +} diff --git a/backend/src/main/java/io/spring/application/data/CommentData.java b/backend/src/main/java/io/spring/application/data/CommentData.java new file mode 100644 index 0000000..a31dbf4 --- /dev/null +++ b/backend/src/main/java/io/spring/application/data/CommentData.java @@ -0,0 +1,29 @@ +package io.spring.application.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.spring.application.DateTimeCursor; +import io.spring.application.Node; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CommentData implements Node { + private String id; + private String body; + @JsonIgnore private String itemId; + private DateTime createdAt; + private DateTime updatedAt; + + @JsonProperty("seller") + private ProfileData profileData; + + @Override + public DateTimeCursor getCursor() { + return new DateTimeCursor(createdAt); + } +} diff --git a/backend/src/main/java/io/spring/application/data/ItemData.java b/backend/src/main/java/io/spring/application/data/ItemData.java new file mode 100644 index 0000000..e65f04c --- /dev/null +++ b/backend/src/main/java/io/spring/application/data/ItemData.java @@ -0,0 +1,33 @@ +package io.spring.application.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.spring.application.DateTimeCursor; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ItemData implements io.spring.application.Node { + private String id; + private String slug; + private String title; + private String description; + private String image; + private boolean favorited; + private int favoritesCount; + private DateTime createdAt; + private DateTime updatedAt; + private List tagList; + + @JsonProperty("seller") + private ProfileData profileData; + + @Override + public DateTimeCursor getCursor() { + return new DateTimeCursor(updatedAt); + } +} diff --git a/backend/src/main/java/io/spring/application/data/ItemDataList.java b/backend/src/main/java/io/spring/application/data/ItemDataList.java new file mode 100644 index 0000000..983d17c --- /dev/null +++ b/backend/src/main/java/io/spring/application/data/ItemDataList.java @@ -0,0 +1,20 @@ +package io.spring.application.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; + +@Getter +public class ItemDataList { + @JsonProperty("items") + private final List itemDatas; + + @JsonProperty("itemsCount") + private final int count; + + public ItemDataList(List itemDatas, int count) { + + this.itemDatas = itemDatas; + this.count = count; + } +} diff --git a/backend/src/main/java/io/spring/application/data/ItemFavoriteCount.java b/backend/src/main/java/io/spring/application/data/ItemFavoriteCount.java new file mode 100644 index 0000000..6d875df --- /dev/null +++ b/backend/src/main/java/io/spring/application/data/ItemFavoriteCount.java @@ -0,0 +1,9 @@ +package io.spring.application.data; + +import lombok.Value; + +@Value +public class ItemFavoriteCount { + private String id; + private Integer count; +} diff --git a/backend/src/main/java/io/spring/application/data/ProfileData.java b/backend/src/main/java/io/spring/application/data/ProfileData.java new file mode 100644 index 0000000..82ef5f9 --- /dev/null +++ b/backend/src/main/java/io/spring/application/data/ProfileData.java @@ -0,0 +1,17 @@ +package io.spring.application.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProfileData { + @JsonIgnore private String id; + private String username; + private String bio; + private String image; + private boolean following; +} diff --git a/backend/src/main/java/io/spring/application/data/UserData.java b/backend/src/main/java/io/spring/application/data/UserData.java new file mode 100644 index 0000000..c50cc19 --- /dev/null +++ b/backend/src/main/java/io/spring/application/data/UserData.java @@ -0,0 +1,16 @@ +package io.spring.application.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserData { + private String id; + private String email; + private String username; + private String bio; + private String image; +} diff --git a/backend/src/main/java/io/spring/application/data/UserWithToken.java b/backend/src/main/java/io/spring/application/data/UserWithToken.java new file mode 100644 index 0000000..eac7f1b --- /dev/null +++ b/backend/src/main/java/io/spring/application/data/UserWithToken.java @@ -0,0 +1,20 @@ +package io.spring.application.data; + +import lombok.Getter; + +@Getter +public class UserWithToken { + private String email; + private String username; + private String bio; + private String image; + private String token; + + public UserWithToken(UserData userData, String token) { + this.email = userData.getEmail(); + this.username = userData.getUsername(); + this.bio = userData.getBio(); + this.image = userData.getImage(); + this.token = token; + } +} diff --git a/backend/src/main/java/io/spring/application/item/DuplicatedItemConstraint.java b/backend/src/main/java/io/spring/application/item/DuplicatedItemConstraint.java new file mode 100644 index 0000000..d0d0aaa --- /dev/null +++ b/backend/src/main/java/io/spring/application/item/DuplicatedItemConstraint.java @@ -0,0 +1,21 @@ +package io.spring.application.item; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Documented +@Constraint(validatedBy = DuplicatedItemValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DuplicatedItemConstraint { + String message() default "item name exists"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/backend/src/main/java/io/spring/application/item/DuplicatedItemValidator.java b/backend/src/main/java/io/spring/application/item/DuplicatedItemValidator.java new file mode 100644 index 0000000..a7042a3 --- /dev/null +++ b/backend/src/main/java/io/spring/application/item/DuplicatedItemValidator.java @@ -0,0 +1,17 @@ +package io.spring.application.item; + +import io.spring.application.ItemQueryService; +import io.spring.core.item.Item; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import org.springframework.beans.factory.annotation.Autowired; + +class DuplicatedItemValidator implements ConstraintValidator { + + @Autowired private ItemQueryService itemQueryService; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return !itemQueryService.findBySlug(Item.toSlug(value), null).isPresent(); + } +} diff --git a/backend/src/main/java/io/spring/application/item/ItemCommandService.java b/backend/src/main/java/io/spring/application/item/ItemCommandService.java new file mode 100644 index 0000000..14a9e15 --- /dev/null +++ b/backend/src/main/java/io/spring/application/item/ItemCommandService.java @@ -0,0 +1,36 @@ +package io.spring.application.item; + +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.user.User; +import javax.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +@AllArgsConstructor +public class ItemCommandService { + + private ItemRepository itemRepository; + + public Item createItem(@Valid NewItemParam newItemParam, User creator) { + Item item = + new Item( + newItemParam.getTitle(), + newItemParam.getDescription(), + newItemParam.getImage(), + newItemParam.getTagList(), + creator.getId()); + itemRepository.save(item); + return item; + } + + public Item updateItem(Item item, @Valid UpdateItemParam updateItemParam) { + item.update( + updateItemParam.getTitle(), updateItemParam.getDescription(), updateItemParam.getImage()); + itemRepository.save(item); + return item; + } +} diff --git a/backend/src/main/java/io/spring/application/item/NewItemParam.java b/backend/src/main/java/io/spring/application/item/NewItemParam.java new file mode 100644 index 0000000..89a4d84 --- /dev/null +++ b/backend/src/main/java/io/spring/application/item/NewItemParam.java @@ -0,0 +1,27 @@ +package io.spring.application.item; + +import com.fasterxml.jackson.annotation.JsonRootName; +import java.util.List; +import javax.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@JsonRootName("item") +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NewItemParam { + @NotBlank(message = "can't be empty") + @DuplicatedItemConstraint + private String title; + + @NotBlank(message = "can't be empty") + private String description; + + private String image; + + private List tagList; +} diff --git a/backend/src/main/java/io/spring/application/item/UpdateItemParam.java b/backend/src/main/java/io/spring/application/item/UpdateItemParam.java new file mode 100644 index 0000000..d2f214e --- /dev/null +++ b/backend/src/main/java/io/spring/application/item/UpdateItemParam.java @@ -0,0 +1,16 @@ +package io.spring.application.item; + +import com.fasterxml.jackson.annotation.JsonRootName; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@JsonRootName("item") +public class UpdateItemParam { + private String title = ""; + private String image = ""; + private String description = ""; +} diff --git a/backend/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java b/backend/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java new file mode 100644 index 0000000..e41eb00 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/DuplicatedEmailConstraint.java @@ -0,0 +1,16 @@ +package io.spring.application.user; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = DuplicatedEmailValidator.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface DuplicatedEmailConstraint { + String message() default "duplicated email"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/backend/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java b/backend/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java new file mode 100644 index 0000000..e307114 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/DuplicatedEmailValidator.java @@ -0,0 +1,17 @@ +package io.spring.application.user; + +import io.spring.core.user.UserRepository; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import org.springframework.beans.factory.annotation.Autowired; + +public class DuplicatedEmailValidator + implements ConstraintValidator { + + @Autowired private UserRepository userRepository; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return (value == null || value.isEmpty()) || !userRepository.findByEmail(value).isPresent(); + } +} diff --git a/backend/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java b/backend/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java new file mode 100644 index 0000000..4f365b7 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/DuplicatedUsernameConstraint.java @@ -0,0 +1,16 @@ +package io.spring.application.user; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = DuplicatedUsernameValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@interface DuplicatedUsernameConstraint { + String message() default "duplicated username"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/backend/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java b/backend/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java new file mode 100644 index 0000000..ae1fd21 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/DuplicatedUsernameValidator.java @@ -0,0 +1,17 @@ +package io.spring.application.user; + +import io.spring.core.user.UserRepository; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import org.springframework.beans.factory.annotation.Autowired; + +class DuplicatedUsernameValidator + implements ConstraintValidator { + + @Autowired private UserRepository userRepository; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return (value == null || value.isEmpty()) || !userRepository.findByUsername(value).isPresent(); + } +} diff --git a/backend/src/main/java/io/spring/application/user/RegisterParam.java b/backend/src/main/java/io/spring/application/user/RegisterParam.java new file mode 100644 index 0000000..3ba1234 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/RegisterParam.java @@ -0,0 +1,26 @@ +package io.spring.application.user; + +import com.fasterxml.jackson.annotation.JsonRootName; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@JsonRootName("user") +@AllArgsConstructor +@NoArgsConstructor +public class RegisterParam { + @NotBlank(message = "can't be empty") + @Email(message = "should be an email") + @DuplicatedEmailConstraint + private String email; + + @NotBlank(message = "can't be empty") + @DuplicatedUsernameConstraint + private String username; + + @NotBlank(message = "can't be empty") + private String password; +} diff --git a/backend/src/main/java/io/spring/application/user/UpdateUserCommand.java b/backend/src/main/java/io/spring/application/user/UpdateUserCommand.java new file mode 100644 index 0000000..9df5230 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/UpdateUserCommand.java @@ -0,0 +1,14 @@ +package io.spring.application.user; + +import io.spring.core.user.User; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@UpdateUserConstraint +public class UpdateUserCommand { + + private User targetUser; + private UpdateUserParam param; +} diff --git a/backend/src/main/java/io/spring/application/user/UpdateUserParam.java b/backend/src/main/java/io/spring/application/user/UpdateUserParam.java new file mode 100644 index 0000000..54cd774 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/UpdateUserParam.java @@ -0,0 +1,25 @@ +package io.spring.application.user; + +import com.fasterxml.jackson.annotation.JsonRootName; +import javax.validation.constraints.Email; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@JsonRootName("user") +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UpdateUserParam { + + @Builder.Default + @Email(message = "should be an email") + private String email = ""; + + @Builder.Default private String password = ""; + @Builder.Default private String username = ""; + @Builder.Default private String bio = ""; + @Builder.Default private String image = ""; +} diff --git a/backend/src/main/java/io/spring/application/user/UserService.java b/backend/src/main/java/io/spring/application/user/UserService.java new file mode 100644 index 0000000..48c6735 --- /dev/null +++ b/backend/src/main/java/io/spring/application/user/UserService.java @@ -0,0 +1,106 @@ +package io.spring.application.user; + +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +public class UserService { + private UserRepository userRepository; + private String defaultImage; + private PasswordEncoder passwordEncoder; + + @Autowired + public UserService( + UserRepository userRepository, + @Value("${image.default}") String defaultImage, + PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.defaultImage = defaultImage; + this.passwordEncoder = passwordEncoder; + } + + public User createUser(@Valid RegisterParam registerParam) { + User user = + new User( + registerParam.getEmail(), + registerParam.getUsername(), + passwordEncoder.encode(registerParam.getPassword()), + "", + defaultImage); + userRepository.save(user); + return user; + } + + public void updateUser(@Valid UpdateUserCommand command) { + User user = command.getTargetUser(); + UpdateUserParam updateUserParam = command.getParam(); + user.update( + updateUserParam.getEmail(), + updateUserParam.getUsername(), + updateUserParam.getPassword(), + updateUserParam.getBio(), + updateUserParam.getImage()); + userRepository.save(user); + } +} + +@Constraint(validatedBy = UpdateUserValidator.class) +@Retention(RetentionPolicy.RUNTIME) +@interface UpdateUserConstraint { + + String message() default "invalid update param"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} + +class UpdateUserValidator implements ConstraintValidator { + + @Autowired private UserRepository userRepository; + + @Override + public boolean isValid(UpdateUserCommand value, ConstraintValidatorContext context) { + String inputEmail = value.getParam().getEmail(); + String inputUsername = value.getParam().getUsername(); + final User targetUser = value.getTargetUser(); + + boolean isEmailValid = + userRepository.findByEmail(inputEmail).map(user -> user.equals(targetUser)).orElse(true); + boolean isUsernameValid = + userRepository + .findByUsername(inputUsername) + .map(user -> user.equals(targetUser)) + .orElse(true); + if (isEmailValid && isUsernameValid) { + return true; + } else { + context.disableDefaultConstraintViolation(); + if (!isEmailValid) { + context + .buildConstraintViolationWithTemplate("email already exist") + .addPropertyNode("email") + .addConstraintViolation(); + } + if (!isUsernameValid) { + context + .buildConstraintViolationWithTemplate("username already exist") + .addPropertyNode("username") + .addConstraintViolation(); + } + return false; + } + } +} diff --git a/backend/src/main/java/io/spring/core/comment/Comment.java b/backend/src/main/java/io/spring/core/comment/Comment.java new file mode 100644 index 0000000..a7a6bc4 --- /dev/null +++ b/backend/src/main/java/io/spring/core/comment/Comment.java @@ -0,0 +1,26 @@ +package io.spring.core.comment; + +import java.util.UUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class Comment { + private String id; + private String body; + private String userId; + private String itemId; + private DateTime createdAt; + + public Comment(String body, String userId, String itemId) { + this.id = UUID.randomUUID().toString(); + this.body = body; + this.userId = userId; + this.itemId = itemId; + this.createdAt = new DateTime(); + } +} diff --git a/backend/src/main/java/io/spring/core/comment/CommentRepository.java b/backend/src/main/java/io/spring/core/comment/CommentRepository.java new file mode 100644 index 0000000..ad3ab07 --- /dev/null +++ b/backend/src/main/java/io/spring/core/comment/CommentRepository.java @@ -0,0 +1,11 @@ +package io.spring.core.comment; + +import java.util.Optional; + +public interface CommentRepository { + void save(Comment comment); + + Optional findById(String itemId, String id); + + void remove(Comment comment); +} diff --git a/backend/src/main/java/io/spring/core/favorite/ItemFavorite.java b/backend/src/main/java/io/spring/core/favorite/ItemFavorite.java new file mode 100644 index 0000000..39ce24d --- /dev/null +++ b/backend/src/main/java/io/spring/core/favorite/ItemFavorite.java @@ -0,0 +1,18 @@ +package io.spring.core.favorite; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +@EqualsAndHashCode +public class ItemFavorite { + private String itemId; + private String userId; + + public ItemFavorite(String itemId, String userId) { + this.itemId = itemId; + this.userId = userId; + } +} diff --git a/backend/src/main/java/io/spring/core/favorite/ItemFavoriteRepository.java b/backend/src/main/java/io/spring/core/favorite/ItemFavoriteRepository.java new file mode 100644 index 0000000..61db705 --- /dev/null +++ b/backend/src/main/java/io/spring/core/favorite/ItemFavoriteRepository.java @@ -0,0 +1,11 @@ +package io.spring.core.favorite; + +import java.util.Optional; + +public interface ItemFavoriteRepository { + void save(ItemFavorite itemFavorite); + + Optional find(String itemId, String userId); + + void remove(ItemFavorite favorite); +} diff --git a/backend/src/main/java/io/spring/core/item/Item.java b/backend/src/main/java/io/spring/core/item/Item.java new file mode 100644 index 0000000..6b39667 --- /dev/null +++ b/backend/src/main/java/io/spring/core/item/Item.java @@ -0,0 +1,74 @@ +package io.spring.core.item; + +import io.spring.Util; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = {"id"}) +public class Item { + private String sellerId; + private String id; + private String slug; + private String title; + private String description; + private String image; + private List tags; + private DateTime createdAt; + private DateTime updatedAt; + + public Item( + String title, String description, String image, List tagList, String sellerId) { + this(title, description, image, tagList, sellerId, new DateTime()); + } + + public Item( + String title, + String description, + String image, + List tagList, + String sellerId, + DateTime createdAt) { + this.id = UUID.randomUUID().toString(); + this.slug = toSlug(title); + this.title = title; + this.description = description; + this.image = image; + this.tags = + Optional.ofNullable(tagList) + .map(list -> new HashSet<>(list).stream().map(Tag::new).collect(Collectors.toList())) + .orElse(new ArrayList()); + + this.sellerId = sellerId; + this.createdAt = createdAt; + this.updatedAt = createdAt; + } + + public void update(String title, String description, String image) { + if (!Util.isEmpty(title)) { + this.title = title; + this.updatedAt = new DateTime(); + } + if (!Util.isEmpty(description)) { + this.description = description; + this.updatedAt = new DateTime(); + } + if (!Util.isEmpty(image)) { + this.image = image; + this.updatedAt = new DateTime(); + } + } + + public static String toSlug(String title) { + return title.toLowerCase().replaceAll("[\\&|[\\uFE30-\\uFFA0]|\\’|\\”|\\s\\?\\,\\.]+", "-"); + } +} diff --git a/backend/src/main/java/io/spring/core/item/ItemRepository.java b/backend/src/main/java/io/spring/core/item/ItemRepository.java new file mode 100644 index 0000000..6dea2ea --- /dev/null +++ b/backend/src/main/java/io/spring/core/item/ItemRepository.java @@ -0,0 +1,14 @@ +package io.spring.core.item; + +import java.util.Optional; + +public interface ItemRepository { + + void save(Item item); + + Optional findById(String id); + + Optional findBySlug(String slug); + + void remove(Item item); +} diff --git a/backend/src/main/java/io/spring/core/item/Tag.java b/backend/src/main/java/io/spring/core/item/Tag.java new file mode 100644 index 0000000..7edd313 --- /dev/null +++ b/backend/src/main/java/io/spring/core/item/Tag.java @@ -0,0 +1,22 @@ +package io.spring.core.item; + +import java.util.UUID; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +@NoArgsConstructor +@Data +@EqualsAndHashCode(of = "name") +public class Tag { + private String id; + private String name; + private DateTime createdAt; + + public Tag(String name) { + this.id = UUID.randomUUID().toString(); + this.name = name; + this.createdAt = new DateTime(); + } +} diff --git a/backend/src/main/java/io/spring/core/service/AuthorizationService.java b/backend/src/main/java/io/spring/core/service/AuthorizationService.java new file mode 100644 index 0000000..bb7fba2 --- /dev/null +++ b/backend/src/main/java/io/spring/core/service/AuthorizationService.java @@ -0,0 +1,15 @@ +package io.spring.core.service; + +import io.spring.core.comment.Comment; +import io.spring.core.item.Item; +import io.spring.core.user.User; + +public class AuthorizationService { + public static boolean canWriteItem(User user, Item item) { + return user.getId().equals(item.getSellerId()); + } + + public static boolean canWriteComment(User user, Item item, Comment comment) { + return user.getId().equals(item.getSellerId()) || user.getId().equals(comment.getUserId()); + } +} diff --git a/backend/src/main/java/io/spring/core/service/JwtService.java b/backend/src/main/java/io/spring/core/service/JwtService.java new file mode 100644 index 0000000..d143076 --- /dev/null +++ b/backend/src/main/java/io/spring/core/service/JwtService.java @@ -0,0 +1,12 @@ +package io.spring.core.service; + +import io.spring.core.user.User; +import java.util.Optional; +import org.springframework.stereotype.Service; + +@Service +public interface JwtService { + String toToken(User user); + + Optional getSubFromToken(String token); +} diff --git a/backend/src/main/java/io/spring/core/user/FollowRelation.java b/backend/src/main/java/io/spring/core/user/FollowRelation.java new file mode 100644 index 0000000..7d7b538 --- /dev/null +++ b/backend/src/main/java/io/spring/core/user/FollowRelation.java @@ -0,0 +1,17 @@ +package io.spring.core.user; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +public class FollowRelation { + private String userId; + private String targetId; + + public FollowRelation(String userId, String targetId) { + + this.userId = userId; + this.targetId = targetId; + } +} diff --git a/backend/src/main/java/io/spring/core/user/User.java b/backend/src/main/java/io/spring/core/user/User.java new file mode 100644 index 0000000..3044d50 --- /dev/null +++ b/backend/src/main/java/io/spring/core/user/User.java @@ -0,0 +1,50 @@ +package io.spring.core.user; + +import io.spring.Util; +import java.util.UUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = {"id"}) +public class User { + private String id; + private String email; + private String username; + private String password; + private String bio; + private String image; + + public User(String email, String username, String password, String bio, String image) { + this.id = UUID.randomUUID().toString(); + this.email = email; + this.username = username; + this.password = password; + this.bio = bio; + this.image = image; + } + + public void update(String email, String username, String password, String bio, String image) { + if (!Util.isEmpty(email)) { + this.email = email; + } + + if (!Util.isEmpty(username)) { + this.username = username; + } + + if (!Util.isEmpty(password)) { + this.password = password; + } + + if (!Util.isEmpty(bio)) { + this.bio = bio; + } + + if (!Util.isEmpty(image)) { + this.image = image; + } + } +} diff --git a/backend/src/main/java/io/spring/core/user/UserRepository.java b/backend/src/main/java/io/spring/core/user/UserRepository.java new file mode 100644 index 0000000..f52c772 --- /dev/null +++ b/backend/src/main/java/io/spring/core/user/UserRepository.java @@ -0,0 +1,21 @@ +package io.spring.core.user; + +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository { + void save(User user); + + Optional findById(String id); + + Optional findByUsername(String username); + + Optional findByEmail(String email); + + void saveRelation(FollowRelation followRelation); + + Optional findRelation(String userId, String targetId); + + void removeRelation(FollowRelation followRelation); +} diff --git a/backend/src/main/java/io/spring/graphql/CommentDatafetcher.java b/backend/src/main/java/io/spring/graphql/CommentDatafetcher.java new file mode 100644 index 0000000..4f4bc89 --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/CommentDatafetcher.java @@ -0,0 +1,122 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import com.netflix.graphql.dgs.DgsDataFetchingEnvironment; +import com.netflix.graphql.dgs.InputArgument; +import graphql.execution.DataFetcherResult; +import graphql.relay.DefaultConnectionCursor; +import graphql.relay.DefaultPageInfo; +import io.spring.application.CommentQueryService; +import io.spring.application.CursorPageParameter; +import io.spring.application.CursorPager; +import io.spring.application.CursorPager.Direction; +import io.spring.application.DateTimeCursor; +import io.spring.application.data.CommentData; +import io.spring.application.data.ItemData; +import io.spring.core.user.User; +import io.spring.graphql.DgsConstants.COMMENTPAYLOAD; +import io.spring.graphql.DgsConstants.ITEM; +import io.spring.graphql.types.Comment; +import io.spring.graphql.types.CommentEdge; +import io.spring.graphql.types.CommentsConnection; +import io.spring.graphql.types.Item; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.joda.time.format.ISODateTimeFormat; + +@DgsComponent +@AllArgsConstructor +public class CommentDatafetcher { + private CommentQueryService commentQueryService; + + @DgsData(parentType = COMMENTPAYLOAD.TYPE_NAME, field = COMMENTPAYLOAD.Comment) + public DataFetcherResult getComment(DgsDataFetchingEnvironment dfe) { + CommentData comment = dfe.getLocalContext(); + Comment commentResult = buildCommentResult(comment); + return DataFetcherResult.newResult() + .data(commentResult) + .localContext( + new HashMap() { + { + put(comment.getId(), comment); + } + }) + .build(); + } + + @DgsData(parentType = ITEM.TYPE_NAME, field = ITEM.Comments) + public DataFetcherResult itemComments( + @InputArgument("first") Integer first, + @InputArgument("after") String after, + @InputArgument("last") Integer last, + @InputArgument("before") String before, + DgsDataFetchingEnvironment dfe) { + + if (first == null && last == null) { + throw new IllegalArgumentException("first 和 last 必须只存在一个"); + } + + User current = SecurityUtil.getCurrentUser().orElse(null); + Item item = dfe.getSource(); + Map map = dfe.getLocalContext(); + ItemData itemData = map.get(item.getSlug()); + + CursorPager comments; + if (first != null) { + comments = + commentQueryService.findByItemIdWithCursor( + itemData.getId(), + current, + new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT)); + } else { + comments = + commentQueryService.findByItemIdWithCursor( + itemData.getId(), + current, + new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV)); + } + graphql.relay.PageInfo pageInfo = buildCommentPageInfo(comments); + CommentsConnection result = + CommentsConnection.newBuilder() + .pageInfo(pageInfo) + .edges( + comments.getData().stream() + .map( + a -> + CommentEdge.newBuilder() + .cursor(a.getCursor().toString()) + .node(buildCommentResult(a)) + .build()) + .collect(Collectors.toList())) + .build(); + return DataFetcherResult.newResult() + .data(result) + .localContext( + comments.getData().stream().collect(Collectors.toMap(CommentData::getId, c -> c))) + .build(); + } + + private DefaultPageInfo buildCommentPageInfo(CursorPager comments) { + return new DefaultPageInfo( + comments.getStartCursor() == null + ? null + : new DefaultConnectionCursor(comments.getStartCursor().toString()), + comments.getEndCursor() == null + ? null + : new DefaultConnectionCursor(comments.getEndCursor().toString()), + comments.hasPrevious(), + comments.hasNext()); + } + + private Comment buildCommentResult(CommentData comment) { + return Comment.newBuilder() + .id(comment.getId()) + .body(comment.getBody()) + .updatedAt(ISODateTimeFormat.dateTime().withZoneUTC().print(comment.getCreatedAt())) + .createdAt(ISODateTimeFormat.dateTime().withZoneUTC().print(comment.getCreatedAt())) + .build(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/CommentMutation.java b/backend/src/main/java/io/spring/graphql/CommentMutation.java new file mode 100644 index 0000000..35b7f76 --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/CommentMutation.java @@ -0,0 +1,66 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import com.netflix.graphql.dgs.InputArgument; +import graphql.execution.DataFetcherResult; +import io.spring.api.exception.NoAuthorizationException; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.CommentQueryService; +import io.spring.application.data.CommentData; +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.service.AuthorizationService; +import io.spring.core.user.User; +import io.spring.graphql.DgsConstants.MUTATION; +import io.spring.graphql.exception.AuthenticationException; +import io.spring.graphql.types.CommentPayload; +import io.spring.graphql.types.DeletionStatus; +import lombok.AllArgsConstructor; + +@DgsComponent +@AllArgsConstructor +public class CommentMutation { + + private ItemRepository itemRepository; + private CommentRepository commentRepository; + private CommentQueryService commentQueryService; + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.AddComment) + public DataFetcherResult createComment( + @InputArgument("slug") String slug, @InputArgument("body") String body) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + Comment comment = new Comment(body, user.getId(), item.getId()); + commentRepository.save(comment); + CommentData commentData = + commentQueryService + .findById(comment.getId(), user) + .orElseThrow(ResourceNotFoundException::new); + return DataFetcherResult.newResult() + .localContext(commentData) + .data(CommentPayload.newBuilder().build()) + .build(); + } + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.DeleteComment) + public DeletionStatus removeComment( + @InputArgument("slug") String slug, @InputArgument("id") String commentId) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + return commentRepository + .findById(item.getId(), commentId) + .map( + comment -> { + if (!AuthorizationService.canWriteComment(user, item, comment)) { + throw new NoAuthorizationException(); + } + commentRepository.remove(comment); + return DeletionStatus.newBuilder().success(true).build(); + }) + .orElseThrow(ResourceNotFoundException::new); + } +} diff --git a/backend/src/main/java/io/spring/graphql/ItemDatafetcher.java b/backend/src/main/java/io/spring/graphql/ItemDatafetcher.java new file mode 100644 index 0000000..3dd97d6 --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/ItemDatafetcher.java @@ -0,0 +1,378 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import com.netflix.graphql.dgs.DgsDataFetchingEnvironment; +import com.netflix.graphql.dgs.DgsQuery; +import com.netflix.graphql.dgs.InputArgument; +import graphql.execution.DataFetcherResult; +import graphql.relay.DefaultConnectionCursor; +import graphql.relay.DefaultPageInfo; +import graphql.schema.DataFetchingEnvironment; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.CursorPageParameter; +import io.spring.application.CursorPager; +import io.spring.application.CursorPager.Direction; +import io.spring.application.DateTimeCursor; +import io.spring.application.ItemQueryService; +import io.spring.application.data.CommentData; +import io.spring.application.data.ItemData; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.graphql.DgsConstants.COMMENT; +import io.spring.graphql.DgsConstants.ITEMPAYLOAD; +import io.spring.graphql.DgsConstants.PROFILE; +import io.spring.graphql.DgsConstants.QUERY; +import io.spring.graphql.types.Item; +import io.spring.graphql.types.ItemEdge; +import io.spring.graphql.types.ItemsConnection; +import io.spring.graphql.types.Profile; +import java.util.HashMap; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.joda.time.format.ISODateTimeFormat; + +@DgsComponent +@AllArgsConstructor +public class ItemDatafetcher { + + private ItemQueryService itemQueryService; + private UserRepository userRepository; + + @DgsQuery(field = QUERY.Feed) + public DataFetcherResult getFeed( + @InputArgument("first") Integer first, + @InputArgument("after") String after, + @InputArgument("last") Integer last, + @InputArgument("before") String before, + DgsDataFetchingEnvironment dfe) { + if (first == null && last == null) { + throw new IllegalArgumentException("first 和 last 必须只存在一个"); + } + + User current = SecurityUtil.getCurrentUser().orElse(null); + + CursorPager items; + if (first != null) { + items = + itemQueryService.findUserFeedWithCursor( + current, + new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT)); + } else { + items = + itemQueryService.findUserFeedWithCursor( + current, + new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV)); + } + graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); + ItemsConnection itemsConnection = + ItemsConnection.newBuilder() + .pageInfo(pageInfo) + .edges( + items.getData().stream() + .map( + a -> + ItemEdge.newBuilder() + .cursor(a.getCursor().toString()) + .node(buildItemResult(a)) + .build()) + .collect(Collectors.toList())) + .build(); + return DataFetcherResult.newResult() + .data(itemsConnection) + .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) + .build(); + } + + @DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Feed) + public DataFetcherResult userFeed( + @InputArgument("first") Integer first, + @InputArgument("after") String after, + @InputArgument("last") Integer last, + @InputArgument("before") String before, + DgsDataFetchingEnvironment dfe) { + if (first == null && last == null) { + throw new IllegalArgumentException("first 和 last 必须只存在一个"); + } + + Profile profile = dfe.getSource(); + User target = + userRepository + .findByUsername(profile.getUsername()) + .orElseThrow(ResourceNotFoundException::new); + + CursorPager items; + if (first != null) { + items = + itemQueryService.findUserFeedWithCursor( + target, + new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT)); + } else { + items = + itemQueryService.findUserFeedWithCursor( + target, + new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV)); + } + graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); + ItemsConnection itemsConnection = + ItemsConnection.newBuilder() + .pageInfo(pageInfo) + .edges( + items.getData().stream() + .map( + a -> + ItemEdge.newBuilder() + .cursor(a.getCursor().toString()) + .node(buildItemResult(a)) + .build()) + .collect(Collectors.toList())) + .build(); + return DataFetcherResult.newResult() + .data(itemsConnection) + .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) + .build(); + } + + @DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Favorites) + public DataFetcherResult userFavorites( + @InputArgument("first") Integer first, + @InputArgument("after") String after, + @InputArgument("last") Integer last, + @InputArgument("before") String before, + DgsDataFetchingEnvironment dfe) { + if (first == null && last == null) { + throw new IllegalArgumentException("first 和 last 必须只存在一个"); + } + + User current = SecurityUtil.getCurrentUser().orElse(null); + Profile profile = dfe.getSource(); + + CursorPager items; + if (first != null) { + items = + itemQueryService.findRecentItemsWithCursor( + null, + null, + profile.getUsername(), + new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT), + current); + } else { + items = + itemQueryService.findRecentItemsWithCursor( + null, + null, + profile.getUsername(), + new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV), + current); + } + graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); + + ItemsConnection itemsConnection = + ItemsConnection.newBuilder() + .pageInfo(pageInfo) + .edges( + items.getData().stream() + .map( + a -> + ItemEdge.newBuilder() + .cursor(a.getCursor().toString()) + .node(buildItemResult(a)) + .build()) + .collect(Collectors.toList())) + .build(); + return DataFetcherResult.newResult() + .data(itemsConnection) + .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) + .build(); + } + + @DgsData(parentType = PROFILE.TYPE_NAME, field = PROFILE.Items) + public DataFetcherResult userItems( + @InputArgument("first") Integer first, + @InputArgument("after") String after, + @InputArgument("last") Integer last, + @InputArgument("before") String before, + DgsDataFetchingEnvironment dfe) { + if (first == null && last == null) { + throw new IllegalArgumentException("first 和 last 必须只存在一个"); + } + + User current = SecurityUtil.getCurrentUser().orElse(null); + Profile profile = dfe.getSource(); + + CursorPager items; + if (first != null) { + items = + itemQueryService.findRecentItemsWithCursor( + null, + profile.getUsername(), + null, + new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT), + current); + } else { + items = + itemQueryService.findRecentItemsWithCursor( + null, + profile.getUsername(), + null, + new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV), + current); + } + graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); + ItemsConnection itemsConnection = + ItemsConnection.newBuilder() + .pageInfo(pageInfo) + .edges( + items.getData().stream() + .map( + a -> + ItemEdge.newBuilder() + .cursor(a.getCursor().toString()) + .node(buildItemResult(a)) + .build()) + .collect(Collectors.toList())) + .build(); + return DataFetcherResult.newResult() + .data(itemsConnection) + .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) + .build(); + } + + @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Items) + public DataFetcherResult getItems( + @InputArgument("first") Integer first, + @InputArgument("after") String after, + @InputArgument("last") Integer last, + @InputArgument("before") String before, + @InputArgument("soldBy") String soldBy, + @InputArgument("favoritedBy") String favoritedBy, + @InputArgument("withTag") String withTag, + DgsDataFetchingEnvironment dfe) { + if (first == null && last == null) { + throw new IllegalArgumentException("first 和 last 必须只存在一个"); + } + + User current = SecurityUtil.getCurrentUser().orElse(null); + + CursorPager items; + if (first != null) { + items = + itemQueryService.findRecentItemsWithCursor( + withTag, + soldBy, + favoritedBy, + new CursorPageParameter<>(DateTimeCursor.parse(after), first, Direction.NEXT), + current); + } else { + items = + itemQueryService.findRecentItemsWithCursor( + withTag, + soldBy, + favoritedBy, + new CursorPageParameter<>(DateTimeCursor.parse(before), last, Direction.PREV), + current); + } + graphql.relay.PageInfo pageInfo = buildItemPageInfo(items); + ItemsConnection itemsConnection = + ItemsConnection.newBuilder() + .pageInfo(pageInfo) + .edges( + items.getData().stream() + .map( + a -> + ItemEdge.newBuilder() + .cursor(a.getCursor().toString()) + .node(buildItemResult(a)) + .build()) + .collect(Collectors.toList())) + .build(); + return DataFetcherResult.newResult() + .data(itemsConnection) + .localContext(items.getData().stream().collect(Collectors.toMap(ItemData::getSlug, a -> a))) + .build(); + } + + @DgsData(parentType = ITEMPAYLOAD.TYPE_NAME, field = ITEMPAYLOAD.Item) + public DataFetcherResult getItem(DataFetchingEnvironment dfe) { + io.spring.core.item.Item item = dfe.getLocalContext(); + + User current = SecurityUtil.getCurrentUser().orElse(null); + ItemData itemData = + itemQueryService + .findById(item.getId(), current) + .orElseThrow(ResourceNotFoundException::new); + Item itemResult = buildItemResult(itemData); + return DataFetcherResult.newResult() + .localContext( + new HashMap() { + { + put(itemData.getSlug(), itemData); + } + }) + .data(itemResult) + .build(); + } + + @DgsData(parentType = COMMENT.TYPE_NAME, field = COMMENT.Item) + public DataFetcherResult getCommentItem(DataFetchingEnvironment dataFetchingEnvironment) { + CommentData comment = dataFetchingEnvironment.getLocalContext(); + User current = SecurityUtil.getCurrentUser().orElse(null); + ItemData itemData = + itemQueryService + .findById(comment.getItemId(), current) + .orElseThrow(ResourceNotFoundException::new); + Item itemResult = buildItemResult(itemData); + return DataFetcherResult.newResult() + .localContext( + new HashMap() { + { + put(itemData.getSlug(), itemData); + } + }) + .data(itemResult) + .build(); + } + + @DgsQuery(field = QUERY.Item) + public DataFetcherResult findItemBySlug(@InputArgument("slug") String slug) { + User current = SecurityUtil.getCurrentUser().orElse(null); + ItemData itemData = + itemQueryService.findBySlug(slug, current).orElseThrow(ResourceNotFoundException::new); + Item itemResult = buildItemResult(itemData); + return DataFetcherResult.newResult() + .localContext( + new HashMap() { + { + put(itemData.getSlug(), itemData); + } + }) + .data(itemResult) + .build(); + } + + private DefaultPageInfo buildItemPageInfo(CursorPager items) { + return new DefaultPageInfo( + items.getStartCursor() == null + ? null + : new DefaultConnectionCursor(items.getStartCursor().toString()), + items.getEndCursor() == null + ? null + : new DefaultConnectionCursor(items.getEndCursor().toString()), + items.hasPrevious(), + items.hasNext()); + } + + private Item buildItemResult(ItemData itemData) { + return Item.newBuilder() + .image(itemData.getImage()) + .createdAt(ISODateTimeFormat.dateTime().withZoneUTC().print(itemData.getCreatedAt())) + .description(itemData.getDescription()) + .favorited(itemData.isFavorited()) + .favoritesCount(itemData.getFavoritesCount()) + .slug(itemData.getSlug()) + .tagList(itemData.getTagList()) + .title(itemData.getTitle()) + .updatedAt(ISODateTimeFormat.dateTime().withZoneUTC().print(itemData.getUpdatedAt())) + .build(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/ItemMutation.java b/backend/src/main/java/io/spring/graphql/ItemMutation.java new file mode 100644 index 0000000..2e09d92 --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/ItemMutation.java @@ -0,0 +1,110 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsMutation; +import com.netflix.graphql.dgs.InputArgument; +import graphql.execution.DataFetcherResult; +import io.spring.api.exception.NoAuthorizationException; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.item.ItemCommandService; +import io.spring.application.item.NewItemParam; +import io.spring.application.item.UpdateItemParam; +import io.spring.core.favorite.ItemFavorite; +import io.spring.core.favorite.ItemFavoriteRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.service.AuthorizationService; +import io.spring.core.user.User; +import io.spring.graphql.DgsConstants.MUTATION; +import io.spring.graphql.exception.AuthenticationException; +import io.spring.graphql.types.CreateItemInput; +import io.spring.graphql.types.DeletionStatus; +import io.spring.graphql.types.ItemPayload; +import io.spring.graphql.types.UpdateItemInput; +import java.util.Collections; +import lombok.AllArgsConstructor; + +@DgsComponent +@AllArgsConstructor +public class ItemMutation { + + private ItemCommandService itemCommandService; + private ItemFavoriteRepository itemFavoriteRepository; + private ItemRepository itemRepository; + + @DgsMutation(field = MUTATION.CreateItem) + public DataFetcherResult createItem(@InputArgument("input") CreateItemInput input) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + NewItemParam newItemParam = + NewItemParam.builder() + .title(input.getTitle()) + .description(input.getDescription()) + .image(input.getImage()) + .tagList(input.getTagList() == null ? Collections.emptyList() : input.getTagList()) + .build(); + Item item = itemCommandService.createItem(newItemParam, user); + return DataFetcherResult.newResult() + .data(ItemPayload.newBuilder().build()) + .localContext(item) + .build(); + } + + @DgsMutation(field = MUTATION.UpdateItem) + public DataFetcherResult updateItem( + @InputArgument("slug") String slug, @InputArgument("changes") UpdateItemInput params) { + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + if (!AuthorizationService.canWriteItem(user, item)) { + throw new NoAuthorizationException(); + } + item = + itemCommandService.updateItem( + item, + new UpdateItemParam(params.getTitle(), params.getImage(), params.getDescription())); + return DataFetcherResult.newResult() + .data(ItemPayload.newBuilder().build()) + .localContext(item) + .build(); + } + + @DgsMutation(field = MUTATION.FavoriteItem) + public DataFetcherResult favoriteItem(@InputArgument("slug") String slug) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + ItemFavorite itemFavorite = new ItemFavorite(item.getId(), user.getId()); + itemFavoriteRepository.save(itemFavorite); + return DataFetcherResult.newResult() + .data(ItemPayload.newBuilder().build()) + .localContext(item) + .build(); + } + + @DgsMutation(field = MUTATION.UnfavoriteItem) + public DataFetcherResult unfavoriteItem(@InputArgument("slug") String slug) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + itemFavoriteRepository + .find(item.getId(), user.getId()) + .ifPresent( + favorite -> { + itemFavoriteRepository.remove(favorite); + }); + return DataFetcherResult.newResult() + .data(ItemPayload.newBuilder().build()) + .localContext(item) + .build(); + } + + @DgsMutation(field = MUTATION.DeleteItem) + public DeletionStatus deleteItem(@InputArgument("slug") String slug) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + Item item = itemRepository.findBySlug(slug).orElseThrow(ResourceNotFoundException::new); + + if (!AuthorizationService.canWriteItem(user, item)) { + throw new NoAuthorizationException(); + } + + itemRepository.remove(item); + return DeletionStatus.newBuilder().success(true).build(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/MeDatafetcher.java b/backend/src/main/java/io/spring/graphql/MeDatafetcher.java new file mode 100644 index 0000000..9398596 --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/MeDatafetcher.java @@ -0,0 +1,61 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import graphql.execution.DataFetcherResult; +import graphql.schema.DataFetchingEnvironment; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.UserQueryService; +import io.spring.application.data.UserData; +import io.spring.application.data.UserWithToken; +import io.spring.core.service.JwtService; +import io.spring.graphql.DgsConstants.QUERY; +import io.spring.graphql.DgsConstants.USERPAYLOAD; +import io.spring.graphql.types.User; +import lombok.AllArgsConstructor; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.RequestHeader; + +@DgsComponent +@AllArgsConstructor +public class MeDatafetcher { + private UserQueryService userQueryService; + private JwtService jwtService; + + @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Me) + public DataFetcherResult getMe( + @RequestHeader(value = "Authorization") String authorization, + DataFetchingEnvironment dataFetchingEnvironment) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof AnonymousAuthenticationToken + || authentication.getPrincipal() == null) { + return null; + } + io.spring.core.user.User user = (io.spring.core.user.User) authentication.getPrincipal(); + UserData userData = + userQueryService.findById(user.getId()).orElseThrow(ResourceNotFoundException::new); + UserWithToken userWithToken = new UserWithToken(userData, authorization.split(" ")[1]); + User result = + User.newBuilder() + .email(userWithToken.getEmail()) + .username(userWithToken.getUsername()) + .token(userWithToken.getToken()) + .build(); + return DataFetcherResult.newResult().data(result).localContext(user).build(); + } + + @DgsData(parentType = USERPAYLOAD.TYPE_NAME, field = USERPAYLOAD.User) + public DataFetcherResult getUserPayloadUser( + DataFetchingEnvironment dataFetchingEnvironment) { + io.spring.core.user.User user = dataFetchingEnvironment.getLocalContext(); + User result = + User.newBuilder() + .email(user.getEmail()) + .username(user.getUsername()) + .token(jwtService.toToken(user)) + .build(); + return DataFetcherResult.newResult().data(result).localContext(user).build(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/ProfileDatafetcher.java b/backend/src/main/java/io/spring/graphql/ProfileDatafetcher.java new file mode 100644 index 0000000..e421eea --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/ProfileDatafetcher.java @@ -0,0 +1,71 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import com.netflix.graphql.dgs.InputArgument; +import graphql.schema.DataFetchingEnvironment; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.ProfileQueryService; +import io.spring.application.data.CommentData; +import io.spring.application.data.ItemData; +import io.spring.application.data.ProfileData; +import io.spring.core.user.User; +import io.spring.graphql.DgsConstants.COMMENT; +import io.spring.graphql.DgsConstants.ITEM; +import io.spring.graphql.DgsConstants.QUERY; +import io.spring.graphql.DgsConstants.USER; +import io.spring.graphql.types.Comment; +import io.spring.graphql.types.Item; +import io.spring.graphql.types.Profile; +import io.spring.graphql.types.ProfilePayload; +import java.util.Map; +import lombok.AllArgsConstructor; + +@DgsComponent +@AllArgsConstructor +public class ProfileDatafetcher { + + private ProfileQueryService profileQueryService; + + @DgsData(parentType = USER.TYPE_NAME, field = USER.Profile) + public Profile getUserProfile(DataFetchingEnvironment dataFetchingEnvironment) { + User user = dataFetchingEnvironment.getLocalContext(); + String username = user.getUsername(); + return queryProfile(username); + } + + @DgsData(parentType = ITEM.TYPE_NAME, field = ITEM.Seller) + public Profile getSeller(DataFetchingEnvironment dataFetchingEnvironment) { + Map map = dataFetchingEnvironment.getLocalContext(); + Item item = dataFetchingEnvironment.getSource(); + return queryProfile(map.get(item.getSlug()).getProfileData().getUsername()); + } + + @DgsData(parentType = COMMENT.TYPE_NAME, field = COMMENT.Seller) + public Profile getCommentSeller(DataFetchingEnvironment dataFetchingEnvironment) { + Comment comment = dataFetchingEnvironment.getSource(); + Map map = dataFetchingEnvironment.getLocalContext(); + return queryProfile(map.get(comment.getId()).getProfileData().getUsername()); + } + + @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Profile) + public ProfilePayload queryProfile( + @InputArgument("username") String username, DataFetchingEnvironment dataFetchingEnvironment) { + Profile profile = queryProfile(dataFetchingEnvironment.getArgument("username")); + return ProfilePayload.newBuilder().profile(profile).build(); + } + + private Profile queryProfile(String username) { + User current = SecurityUtil.getCurrentUser().orElse(null); + ProfileData profileData = + profileQueryService + .findByUsername(username, current) + .orElseThrow(ResourceNotFoundException::new); + return Profile.newBuilder() + .username(profileData.getUsername()) + .bio(profileData.getBio()) + .image(profileData.getImage()) + .following(profileData.isFollowing()) + .build(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/RelationMutation.java b/backend/src/main/java/io/spring/graphql/RelationMutation.java new file mode 100644 index 0000000..317b4fc --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/RelationMutation.java @@ -0,0 +1,65 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import com.netflix.graphql.dgs.InputArgument; +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.ProfileQueryService; +import io.spring.application.data.ProfileData; +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.graphql.DgsConstants.MUTATION; +import io.spring.graphql.exception.AuthenticationException; +import io.spring.graphql.types.Profile; +import io.spring.graphql.types.ProfilePayload; +import lombok.AllArgsConstructor; + +@DgsComponent +@AllArgsConstructor +public class RelationMutation { + + private UserRepository userRepository; + private ProfileQueryService profileQueryService; + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.FollowUser) + public ProfilePayload follow(@InputArgument("username") String username) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + return userRepository + .findByUsername(username) + .map( + target -> { + FollowRelation followRelation = new FollowRelation(user.getId(), target.getId()); + userRepository.saveRelation(followRelation); + Profile profile = buildProfile(username, user); + return ProfilePayload.newBuilder().profile(profile).build(); + }) + .orElseThrow(ResourceNotFoundException::new); + } + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.UnfollowUser) + public ProfilePayload unfollow(@InputArgument("username") String username) { + User user = SecurityUtil.getCurrentUser().orElseThrow(AuthenticationException::new); + User target = + userRepository.findByUsername(username).orElseThrow(ResourceNotFoundException::new); + return userRepository + .findRelation(user.getId(), target.getId()) + .map( + relation -> { + userRepository.removeRelation(relation); + Profile profile = buildProfile(username, user); + return ProfilePayload.newBuilder().profile(profile).build(); + }) + .orElseThrow(ResourceNotFoundException::new); + } + + private Profile buildProfile(@InputArgument("username") String username, User current) { + ProfileData profileData = profileQueryService.findByUsername(username, current).get(); + return Profile.newBuilder() + .username(profileData.getUsername()) + .bio(profileData.getBio()) + .image(profileData.getImage()) + .following(profileData.isFollowing()) + .build(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/SecurityUtil.java b/backend/src/main/java/io/spring/graphql/SecurityUtil.java new file mode 100644 index 0000000..24b723b --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/SecurityUtil.java @@ -0,0 +1,19 @@ +package io.spring.graphql; + +import io.spring.core.user.User; +import java.util.Optional; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtil { + public static Optional getCurrentUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof AnonymousAuthenticationToken + || authentication.getPrincipal() == null) { + return Optional.empty(); + } + io.spring.core.user.User currentUser = (io.spring.core.user.User) authentication.getPrincipal(); + return Optional.of(currentUser); + } +} diff --git a/backend/src/main/java/io/spring/graphql/TagDatafetcher.java b/backend/src/main/java/io/spring/graphql/TagDatafetcher.java new file mode 100644 index 0000000..6b70bf5 --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/TagDatafetcher.java @@ -0,0 +1,19 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import io.spring.application.TagsQueryService; +import io.spring.graphql.DgsConstants.QUERY; +import java.util.List; +import lombok.AllArgsConstructor; + +@DgsComponent +@AllArgsConstructor +public class TagDatafetcher { + private TagsQueryService tagsQueryService; + + @DgsData(parentType = DgsConstants.QUERY_TYPE, field = QUERY.Tags) + public List getTags() { + return tagsQueryService.allTags(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/UserMutation.java b/backend/src/main/java/io/spring/graphql/UserMutation.java new file mode 100644 index 0000000..581a5b7 --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/UserMutation.java @@ -0,0 +1,93 @@ +package io.spring.graphql; + +import com.netflix.graphql.dgs.DgsComponent; +import com.netflix.graphql.dgs.DgsData; +import com.netflix.graphql.dgs.InputArgument; +import graphql.execution.DataFetcherResult; +import io.spring.api.exception.InvalidAuthenticationException; +import io.spring.application.user.RegisterParam; +import io.spring.application.user.UpdateUserCommand; +import io.spring.application.user.UpdateUserParam; +import io.spring.application.user.UserService; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.graphql.DgsConstants.MUTATION; +import io.spring.graphql.exception.GraphQLCustomizeExceptionHandler; +import io.spring.graphql.types.CreateUserInput; +import io.spring.graphql.types.UpdateUserInput; +import io.spring.graphql.types.UserPayload; +import io.spring.graphql.types.UserResult; +import java.util.Optional; +import javax.validation.ConstraintViolationException; +import lombok.AllArgsConstructor; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@DgsComponent +@AllArgsConstructor +public class UserMutation { + + private UserRepository userRepository; + private PasswordEncoder encryptService; + private UserService userService; + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.CreateUser) + public DataFetcherResult createUser(@InputArgument("input") CreateUserInput input) { + RegisterParam registerParam = + new RegisterParam(input.getEmail(), input.getUsername(), input.getPassword()); + User user; + try { + user = userService.createUser(registerParam); + } catch (ConstraintViolationException cve) { + return DataFetcherResult.newResult() + .data(GraphQLCustomizeExceptionHandler.getErrorsAsData(cve)) + .build(); + } + + return DataFetcherResult.newResult() + .data(UserPayload.newBuilder().build()) + .localContext(user) + .build(); + } + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.Login) + public DataFetcherResult login( + @InputArgument("password") String password, @InputArgument("email") String email) { + Optional optional = userRepository.findByEmail(email); + if (optional.isPresent() && encryptService.matches(password, optional.get().getPassword())) { + return DataFetcherResult.newResult() + .data(UserPayload.newBuilder().build()) + .localContext(optional.get()) + .build(); + } else { + throw new InvalidAuthenticationException(); + } + } + + @DgsData(parentType = MUTATION.TYPE_NAME, field = MUTATION.UpdateUser) + public DataFetcherResult updateUser( + @InputArgument("changes") UpdateUserInput updateUserInput) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof AnonymousAuthenticationToken + || authentication.getPrincipal() == null) { + return null; + } + io.spring.core.user.User currentUser = (io.spring.core.user.User) authentication.getPrincipal(); + UpdateUserParam param = + UpdateUserParam.builder() + .username(updateUserInput.getUsername()) + .email(updateUserInput.getEmail()) + .bio(updateUserInput.getBio()) + .password(updateUserInput.getPassword()) + .image(updateUserInput.getImage()) + .build(); + + userService.updateUser(new UpdateUserCommand(currentUser, param)); + return DataFetcherResult.newResult() + .data(UserPayload.newBuilder().build()) + .localContext(currentUser) + .build(); + } +} diff --git a/backend/src/main/java/io/spring/graphql/exception/AuthenticationException.java b/backend/src/main/java/io/spring/graphql/exception/AuthenticationException.java new file mode 100644 index 0000000..417029f --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/exception/AuthenticationException.java @@ -0,0 +1,3 @@ +package io.spring.graphql.exception; + +public class AuthenticationException extends RuntimeException {} diff --git a/backend/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java b/backend/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java new file mode 100644 index 0000000..bf4768b --- /dev/null +++ b/backend/src/main/java/io/spring/graphql/exception/GraphQLCustomizeExceptionHandler.java @@ -0,0 +1,114 @@ +package io.spring.graphql.exception; + +import com.netflix.graphql.dgs.exceptions.DefaultDataFetcherExceptionHandler; +import com.netflix.graphql.types.errors.ErrorType; +import com.netflix.graphql.types.errors.TypedGraphQLError; +import graphql.GraphQLError; +import graphql.execution.DataFetcherExceptionHandler; +import graphql.execution.DataFetcherExceptionHandlerParameters; +import graphql.execution.DataFetcherExceptionHandlerResult; +import io.spring.api.exception.FieldErrorResource; +import io.spring.api.exception.InvalidAuthenticationException; +import io.spring.graphql.types.Error; +import io.spring.graphql.types.ErrorItem; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import org.springframework.stereotype.Component; + +@Component +public class GraphQLCustomizeExceptionHandler implements DataFetcherExceptionHandler { + + private final DefaultDataFetcherExceptionHandler defaultHandler = + new DefaultDataFetcherExceptionHandler(); + + @Override + public DataFetcherExceptionHandlerResult onException( + DataFetcherExceptionHandlerParameters handlerParameters) { + if (handlerParameters.getException() instanceof InvalidAuthenticationException) { + GraphQLError graphqlError = + TypedGraphQLError.newBuilder() + .errorType(ErrorType.UNAUTHENTICATED) + .message(handlerParameters.getException().getMessage()) + .path(handlerParameters.getPath()) + .build(); + return DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build(); + } else if (handlerParameters.getException() instanceof ConstraintViolationException) { + List errors = new ArrayList<>(); + for (ConstraintViolation violation : + ((ConstraintViolationException) handlerParameters.getException()) + .getConstraintViolations()) { + FieldErrorResource fieldErrorResource = + new FieldErrorResource( + violation.getRootBeanClass().getName(), + getParam(violation.getPropertyPath().toString()), + violation + .getConstraintDescriptor() + .getAnnotation() + .annotationType() + .getSimpleName(), + violation.getMessage()); + errors.add(fieldErrorResource); + } + GraphQLError graphqlError = + TypedGraphQLError.newBadRequestBuilder() + .message(handlerParameters.getException().getMessage()) + .path(handlerParameters.getPath()) + .extensions(errorsToMap(errors)) + .build(); + return DataFetcherExceptionHandlerResult.newResult().error(graphqlError).build(); + } else { + return defaultHandler.onException(handlerParameters); + } + } + + public static Error getErrorsAsData(ConstraintViolationException cve) { + List errors = new ArrayList<>(); + for (ConstraintViolation violation : cve.getConstraintViolations()) { + FieldErrorResource fieldErrorResource = + new FieldErrorResource( + violation.getRootBeanClass().getName(), + getParam(violation.getPropertyPath().toString()), + violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), + violation.getMessage()); + errors.add(fieldErrorResource); + } + Map> errorMap = new HashMap<>(); + for (FieldErrorResource fieldErrorResource : errors) { + if (!errorMap.containsKey(fieldErrorResource.getField())) { + errorMap.put(fieldErrorResource.getField(), new ArrayList<>()); + } + errorMap.get(fieldErrorResource.getField()).add(fieldErrorResource.getMessage()); + } + List errorItems = + errorMap.entrySet().stream() + .map(kv -> ErrorItem.newBuilder().key(kv.getKey()).value(kv.getValue()).build()) + .collect(Collectors.toList()); + return Error.newBuilder().message("BAD_REQUEST").errors(errorItems).build(); + } + + private static String getParam(String s) { + String[] splits = s.split("\\."); + if (splits.length == 1) { + return s; + } else { + return String.join(".", Arrays.copyOfRange(splits, 2, splits.length)); + } + } + + private static Map errorsToMap(List errors) { + Map json = new HashMap<>(); + for (FieldErrorResource fieldErrorResource : errors) { + if (!json.containsKey(fieldErrorResource.getField())) { + json.put(fieldErrorResource.getField(), new ArrayList<>()); + } + ((List) json.get(fieldErrorResource.getField())).add(fieldErrorResource.getMessage()); + } + return json; + } +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java b/backend/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java new file mode 100644 index 0000000..19323e5 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/DateTimeHandler.java @@ -0,0 +1,44 @@ +package io.spring.infrastructure.mybatis; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.TimeZone; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; +import org.joda.time.DateTime; + +@MappedTypes(DateTime.class) +public class DateTimeHandler implements TypeHandler { + + private static final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + + @Override + public void setParameter(PreparedStatement ps, int i, DateTime parameter, JdbcType jdbcType) + throws SQLException { + ps.setTimestamp( + i, parameter != null ? new Timestamp(parameter.getMillis()) : null, UTC_CALENDAR); + } + + @Override + public DateTime getResult(ResultSet rs, String columnName) throws SQLException { + Timestamp timestamp = rs.getTimestamp(columnName, UTC_CALENDAR); + return timestamp != null ? new DateTime(timestamp.getTime()) : null; + } + + @Override + public DateTime getResult(ResultSet rs, int columnIndex) throws SQLException { + Timestamp timestamp = rs.getTimestamp(columnIndex, UTC_CALENDAR); + return timestamp != null ? new DateTime(timestamp.getTime()) : null; + } + + @Override + public DateTime getResult(CallableStatement cs, int columnIndex) throws SQLException { + Timestamp ts = cs.getTimestamp(columnIndex, UTC_CALENDAR); + return ts != null ? new DateTime(ts.getTime()) : null; + } +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java new file mode 100644 index 0000000..5b7c3cc --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/CommentMapper.java @@ -0,0 +1,14 @@ +package io.spring.infrastructure.mybatis.mapper; + +import io.spring.core.comment.Comment; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface CommentMapper { + void insert(@Param("comment") Comment comment); + + Comment findById(@Param("itemId") String itemId, @Param("id") String id); + + void delete(@Param("id") String id); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemFavoriteMapper.java b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemFavoriteMapper.java new file mode 100644 index 0000000..4ff92dc --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemFavoriteMapper.java @@ -0,0 +1,14 @@ +package io.spring.infrastructure.mybatis.mapper; + +import io.spring.core.favorite.ItemFavorite; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ItemFavoriteMapper { + ItemFavorite find(@Param("itemId") String itemId, @Param("userId") String userId); + + void insert(@Param("itemFavorite") ItemFavorite itemFavorite); + + void delete(@Param("favorite") ItemFavorite favorite); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemMapper.java b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemMapper.java new file mode 100644 index 0000000..8dd8696 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/ItemMapper.java @@ -0,0 +1,29 @@ +package io.spring.infrastructure.mybatis.mapper; + +import io.spring.core.item.Item; +import io.spring.core.item.Tag; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.joda.time.DateTime; + +@Mapper +public interface ItemMapper { + void insert(@Param("item") Item item); + + Item findById(@Param("id") String id); + + Tag findTag(@Param("tagName") String tagName); + + void insertTag(@Param("tag") Tag tag); + + void insertItemTagRelation( + @Param("itemId") String itemId, + @Param("tagId") String tagId, + @Param("createdAt") DateTime createdAt); + + Item findBySlug(@Param("slug") String slug); + + void update(@Param("item") Item item); + + void delete(@Param("id") String id); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java new file mode 100644 index 0000000..54f36c7 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/mapper/UserMapper.java @@ -0,0 +1,25 @@ +package io.spring.infrastructure.mybatis.mapper; + +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserMapper { + void insert(@Param("user") User user); + + User findByUsername(@Param("username") String username); + + User findByEmail(@Param("email") String email); + + User findById(@Param("id") String id); + + void update(@Param("user") User user); + + FollowRelation findRelation(@Param("userId") String userId, @Param("targetId") String targetId); + + void saveRelation(@Param("followRelation") FollowRelation followRelation); + + void deleteRelation(@Param("followRelation") FollowRelation followRelation); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java new file mode 100644 index 0000000..2c27b7c --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/CommentReadService.java @@ -0,0 +1,18 @@ +package io.spring.infrastructure.mybatis.readservice; + +import io.spring.application.CursorPageParameter; +import io.spring.application.data.CommentData; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.joda.time.DateTime; + +@Mapper +public interface CommentReadService { + CommentData findById(@Param("id") String id); + + List findByItemId(@Param("itemId") String itemId); + + List findByItemIdWithCursor( + @Param("itemId") String itemId, @Param("page") CursorPageParameter page); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemFavoritesReadService.java b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemFavoritesReadService.java new file mode 100644 index 0000000..c913a23 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemFavoritesReadService.java @@ -0,0 +1,19 @@ +package io.spring.infrastructure.mybatis.readservice; + +import io.spring.application.data.ItemFavoriteCount; +import io.spring.core.user.User; +import java.util.List; +import java.util.Set; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ItemFavoritesReadService { + boolean isUserFavorite(@Param("userId") String userId, @Param("itemId") String itemId); + + int itemFavoriteCount(@Param("itemId") String itemId); + + List itemsFavoriteCount(@Param("ids") List ids); + + Set userFavorites(@Param("ids") List ids, @Param("currentUser") User currentUser); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemReadService.java b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemReadService.java new file mode 100644 index 0000000..e8198f9 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/ItemReadService.java @@ -0,0 +1,42 @@ +package io.spring.infrastructure.mybatis.readservice; + +import io.spring.application.CursorPageParameter; +import io.spring.application.Page; +import io.spring.application.data.ItemData; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface ItemReadService { + ItemData findById(@Param("id") String id); + + ItemData findBySlug(@Param("slug") String slug); + + List queryItems( + @Param("tag") String tag, + @Param("seller") String seller, + @Param("favoritedBy") String favoritedBy, + @Param("page") Page page); + + int countItem( + @Param("tag") String tag, + @Param("seller") String seller, + @Param("favoritedBy") String favoritedBy); + + List findItems(@Param("itemIds") List itemIds); + + List findItemsOfSellers( + @Param("sellers") List authors, @Param("page") Page page); + + List findItemsOfSellersWithCursor( + @Param("sellers") List authors, @Param("page") CursorPageParameter page); + + int countFeedSize(@Param("sellers") List sellers); + + List findItemsWithCursor( + @Param("tag") String tag, + @Param("seller") String seller, + @Param("favoritedBy") String favoritedBy, + @Param("page") CursorPageParameter page); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java new file mode 100644 index 0000000..8737687 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/TagReadService.java @@ -0,0 +1,9 @@ +package io.spring.infrastructure.mybatis.readservice; + +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TagReadService { + List all(); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java new file mode 100644 index 0000000..ae25a48 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserReadService.java @@ -0,0 +1,13 @@ +package io.spring.infrastructure.mybatis.readservice; + +import io.spring.application.data.UserData; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserReadService { + + UserData findByUsername(@Param("username") String username); + + UserData findById(@Param("id") String id); +} diff --git a/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java new file mode 100644 index 0000000..4a8b230 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/mybatis/readservice/UserRelationshipQueryService.java @@ -0,0 +1,16 @@ +package io.spring.infrastructure.mybatis.readservice; + +import java.util.List; +import java.util.Set; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserRelationshipQueryService { + boolean isUserFollowing( + @Param("userId") String userId, @Param("anotherUserId") String anotherUserId); + + Set followingSellers(@Param("userId") String userId, @Param("ids") List ids); + + List followedUsers(@Param("userId") String userId); +} diff --git a/backend/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java new file mode 100644 index 0000000..e0d33e3 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisCommentRepository.java @@ -0,0 +1,33 @@ +package io.spring.infrastructure.repository; + +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.infrastructure.mybatis.mapper.CommentMapper; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MyBatisCommentRepository implements CommentRepository { + private CommentMapper commentMapper; + + @Autowired + public MyBatisCommentRepository(CommentMapper commentMapper) { + this.commentMapper = commentMapper; + } + + @Override + public void save(Comment comment) { + commentMapper.insert(comment); + } + + @Override + public Optional findById(String itemId, String id) { + return Optional.ofNullable(commentMapper.findById(itemId, id)); + } + + @Override + public void remove(Comment comment) { + commentMapper.delete(comment.getId()); + } +} diff --git a/backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemFavoriteRepository.java b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemFavoriteRepository.java new file mode 100644 index 0000000..138bc99 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemFavoriteRepository.java @@ -0,0 +1,35 @@ +package io.spring.infrastructure.repository; + +import io.spring.core.favorite.ItemFavorite; +import io.spring.core.favorite.ItemFavoriteRepository; +import io.spring.infrastructure.mybatis.mapper.ItemFavoriteMapper; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class MyBatisItemFavoriteRepository implements ItemFavoriteRepository { + private ItemFavoriteMapper mapper; + + @Autowired + public MyBatisItemFavoriteRepository(ItemFavoriteMapper mapper) { + this.mapper = mapper; + } + + @Override + public void save(ItemFavorite itemFavorite) { + if (mapper.find(itemFavorite.getItemId(), itemFavorite.getUserId()) == null) { + mapper.insert(itemFavorite); + } + } + + @Override + public Optional find(String itemId, String userId) { + return Optional.ofNullable(mapper.find(itemId, userId)); + } + + @Override + public void remove(ItemFavorite favorite) { + mapper.delete(favorite); + } +} diff --git a/backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemRepository.java b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemRepository.java new file mode 100644 index 0000000..1f30b58 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisItemRepository.java @@ -0,0 +1,59 @@ +package io.spring.infrastructure.repository; + +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.item.Tag; +import io.spring.infrastructure.mybatis.mapper.ItemMapper; +import java.util.Optional; +import org.joda.time.DateTime; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public class MyBatisItemRepository implements ItemRepository { + private ItemMapper itemMapper; + + public MyBatisItemRepository(ItemMapper itemMapper) { + this.itemMapper = itemMapper; + } + + @Override + @Transactional + public void save(Item item) { + if (itemMapper.findById(item.getId()) == null) { + createNew(item); + } else { + itemMapper.update(item); + } + } + + private void createNew(Item item) { + for (Tag tag : item.getTags()) { + Tag targetTag = + Optional.ofNullable(itemMapper.findTag(tag.getName())) + .orElseGet( + () -> { + itemMapper.insertTag(tag); + return tag; + }); + + itemMapper.insertItemTagRelation(item.getId(), targetTag.getId(), new DateTime()); + } + itemMapper.insert(item); + } + + @Override + public Optional findById(String id) { + return Optional.ofNullable(itemMapper.findById(id)); + } + + @Override + public Optional findBySlug(String slug) { + return Optional.ofNullable(itemMapper.findBySlug(slug)); + } + + @Override + public void remove(Item item) { + itemMapper.delete(item.getId()); + } +} diff --git a/backend/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java new file mode 100644 index 0000000..3c24dd5 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/repository/MyBatisUserRepository.java @@ -0,0 +1,60 @@ +package io.spring.infrastructure.repository; + +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.mybatis.mapper.UserMapper; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class MyBatisUserRepository implements UserRepository { + private final UserMapper userMapper; + + @Autowired + public MyBatisUserRepository(UserMapper userMapper) { + this.userMapper = userMapper; + } + + @Override + public void save(User user) { + if (userMapper.findById(user.getId()) == null) { + userMapper.insert(user); + } else { + userMapper.update(user); + } + } + + @Override + public Optional findById(String id) { + return Optional.ofNullable(userMapper.findById(id)); + } + + @Override + public Optional findByUsername(String username) { + return Optional.ofNullable(userMapper.findByUsername(username)); + } + + @Override + public Optional findByEmail(String email) { + return Optional.ofNullable(userMapper.findByEmail(email)); + } + + @Override + public void saveRelation(FollowRelation followRelation) { + if (!findRelation(followRelation.getUserId(), followRelation.getTargetId()).isPresent()) { + userMapper.saveRelation(followRelation); + } + } + + @Override + public Optional findRelation(String userId, String targetId) { + return Optional.ofNullable(userMapper.findRelation(userId, targetId)); + } + + @Override + public void removeRelation(FollowRelation followRelation) { + userMapper.deleteRelation(followRelation); + } +} diff --git a/backend/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java b/backend/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java new file mode 100644 index 0000000..515d661 --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java @@ -0,0 +1,54 @@ +package io.spring.infrastructure.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.spring.core.service.JwtService; +import io.spring.core.user.User; +import java.util.Date; +import java.util.Optional; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class DefaultJwtService implements JwtService { + private final SecretKey signingKey; + private final SignatureAlgorithm signatureAlgorithm; + private int sessionTime; + + @Autowired + public DefaultJwtService( + @Value("${jwt.secret}") String secret, @Value("${jwt.sessionTime}") int sessionTime) { + this.sessionTime = sessionTime; + signatureAlgorithm = SignatureAlgorithm.HS512; + this.signingKey = new SecretKeySpec(secret.getBytes(), signatureAlgorithm.getJcaName()); + } + + @Override + public String toToken(User user) { + return Jwts.builder() + .setSubject(user.getId()) + .setExpiration(expireTimeFromNow()) + .signWith(signingKey) + .compact(); + } + + @Override + public Optional getSubFromToken(String token) { + try { + Jws claimsJws = + Jwts.parserBuilder().setSigningKey(signingKey).build().parseClaimsJws(token); + return Optional.ofNullable(claimsJws.getBody().getSubject()); + } catch (Exception e) { + return Optional.empty(); + } + } + + private Date expireTimeFromNow() { + return new Date(System.currentTimeMillis() + sessionTime * 1000L); + } +} diff --git a/backend/src/main/java/io/spring/infrastructure/service/SendEventService.java b/backend/src/main/java/io/spring/infrastructure/service/SendEventService.java new file mode 100644 index 0000000..401704d --- /dev/null +++ b/backend/src/main/java/io/spring/infrastructure/service/SendEventService.java @@ -0,0 +1,77 @@ +package io.spring.infrastructure.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import okhttp3.*; + +public class SendEventService { + + private static final String PATH_TO_WILCO_ID = "../.wilco"; + private static final String BASE_URL = + Objects.requireNonNullElse(System.getenv("ENGINE_BASE_URL"), "https://engine.wilco.gg"); + private static String wilcoId; + + private final OkHttpClient client; + private final ObjectMapper objectMapper; + + public SendEventService() { + this.client = new OkHttpClient(); + this.objectMapper = new ObjectMapper(); + this.wilcoId = System.getenv("WILCO_ID"); + + if (wilcoId == null && doesFileExist(PATH_TO_WILCO_ID)) { + wilcoId = readFile(PATH_TO_WILCO_ID); + } + } + + public String sendEvent(String event, Map metadata) { + MediaType mediaType = MediaType.parse("application/json"); + + JsonNode metadataNode = objectMapper.valueToTree(metadata); + + ObjectNode data = objectMapper.createObjectNode(); + data.put("event", event); + data.set("metadata", metadataNode); + + RequestBody requestBody = RequestBody.create(mediaType, data.toString()); + + Request request = + new Request.Builder() + .url(BASE_URL + "/users/" + wilcoId + "/event") + .post(requestBody) + .addHeader("Content-type", "application/json") + .build(); + + try { + Response response = client.newCall(request).execute(); + return response.body().string(); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Failed to send event " + event + " to Wilco engine"); + return null; + } + } + + private static boolean doesFileExist(String filePath) { + return Objects.requireNonNull(new java.io.File(filePath).exists()); + } + + private static String readFile(String filePath) { + StringBuilder content = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + return content.toString(); + } +} diff --git a/backend/src/main/resources/application-test.properties b/backend/src/main/resources/application-test.properties new file mode 100644 index 0000000..612f0b6 --- /dev/null +++ b/backend/src/main/resources/application-test.properties @@ -0,0 +1,4 @@ +spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..4319b5e --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,24 @@ +spring.datasource.url=jdbc:postgresql://postgres-java:5432/anythink-market +spring.datasource.username=postgres +spring.datasource.password= +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true + +image.default=https://static.productionready.io/images/smiley-cyrus.jpg + +jwt.secret=nRvyYC4soFxBdZ-F-5Nnzz5USXstR1YylsTd-mA0aKtI9HUlriGrtkf-TiuDapkLiUCogO3JOK7kwZisrHp6wA +jwt.sessionTime=86400 + +mybatis.configuration.cache-enabled=true +mybatis.configuration.default-statement-timeout=3000 +mybatis.configuration.map-underscore-to-camel-case=true +mybatis.configuration.use-generated-keys=true +mybatis.type-handlers-package=io.spring.infrastructure.mybatis +mybatis.mapper-locations=mapper/*.xml + +logging.level.io.spring.infrastructure.mybatis.readservice.ItemReadService=DEBUG +logging.level.io.spring.infrastructure.mybatis.mapper=DEBUG + +server.port=3000 + +management.endpoints.web.base-path=/ diff --git a/backend/src/main/resources/db/migration/V1__create_tables.sql b/backend/src/main/resources/db/migration/V1__create_tables.sql new file mode 100644 index 0000000..d2838d0 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__create_tables.sql @@ -0,0 +1,50 @@ +create table users ( + id varchar(255) primary key, + username varchar(255) UNIQUE, + password varchar(255), + email varchar(255) UNIQUE, + bio text, + image varchar(511) +); + +create table items ( + id varchar(255) primary key, + seller_id varchar(255), + slug varchar(255) UNIQUE, + title varchar(255), + description text, + image text, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +create table item_favorites ( + item_id varchar(255) not null, + user_id varchar(255) not null, + primary key(item_id, user_id) +); + +create table follows ( + user_id varchar(255) not null, + follow_id varchar(255) not null +); + +create table tags ( + id varchar(255) primary key, + name varchar(255) not null +); + +create table item_tags ( + item_id varchar(255) not null, + tag_id varchar(255) not null, + created_at TIMESTAMP not null +); + +create table comments ( + id varchar(255) primary key, + body text, + item_id varchar(255), + user_id varchar(255), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/backend/src/main/resources/mapper/CommentMapper.xml b/backend/src/main/resources/mapper/CommentMapper.xml new file mode 100644 index 0000000..4f11282 --- /dev/null +++ b/backend/src/main/resources/mapper/CommentMapper.xml @@ -0,0 +1,35 @@ + + + + + insert into comments(id, body, user_id, item_id, created_at, updated_at) + values ( + #{comment.id}, + #{comment.body}, + #{comment.userId}, + #{comment.itemId}, + #{comment.createdAt}, + #{comment.createdAt} + ) + + + delete from comments where id = #{id} + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/CommentReadService.xml b/backend/src/main/resources/mapper/CommentReadService.xml new file mode 100644 index 0000000..2681aa6 --- /dev/null +++ b/backend/src/main/resources/mapper/CommentReadService.xml @@ -0,0 +1,44 @@ + + + + + SELECT + C.id commentId, + C.body commentBody, + C.created_at commentCreatedAt, + C.item_id commentItemId, + + from comments C + left join users U + on C.user_id = U.id + + + + + + diff --git a/backend/src/main/resources/mapper/ItemFavoriteMapper.xml b/backend/src/main/resources/mapper/ItemFavoriteMapper.xml new file mode 100644 index 0000000..5fbba91 --- /dev/null +++ b/backend/src/main/resources/mapper/ItemFavoriteMapper.xml @@ -0,0 +1,21 @@ + + + + + insert into item_favorites (item_id, user_id) values (#{itemFavorite.itemId}, #{itemFavorite.userId}) + + + delete from item_favorites where item_id = #{favorite.itemId} and user_id = #{favorite.userId} + + + + + + + diff --git a/backend/src/main/resources/mapper/ItemFavoritesReadService.xml b/backend/src/main/resources/mapper/ItemFavoritesReadService.xml new file mode 100644 index 0000000..16a673f --- /dev/null +++ b/backend/src/main/resources/mapper/ItemFavoritesReadService.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/backend/src/main/resources/mapper/ItemMapper.xml b/backend/src/main/resources/mapper/ItemMapper.xml new file mode 100644 index 0000000..f907a17 --- /dev/null +++ b/backend/src/main/resources/mapper/ItemMapper.xml @@ -0,0 +1,82 @@ + + + + + insert into items(id, slug, title, description, image, seller_id, created_at, updated_at) + values( + #{item.id}, + #{item.slug}, + #{item.title}, + #{item.description}, + #{item.image}, + #{item.sellerId}, + #{item.createdAt}, + #{item.updatedAt}) + + + insert into tags (id, name) values (#{tag.id}, #{tag.name}) + + + insert into item_tags (item_id, tag_id, created_at) values(#{itemId}, #{tagId}, #{createdAt}) + + + update items + + title = #{item.title}, + slug = #{item.slug}, + description = #{item.description}, + image = #{item.image} + + where id = #{item.id} + + + delete from items where id = #{id} + + + select + A.id itemId, + A.slug itemSlug, + A.title itemTitle, + A.description itemDescription, + A.image itemImage, + A.seller_id itemSellerId, + A.created_at itemCreatedAt, + A.updated_at itemUpdatedAt, + T.id tagId, + T.name tagName + from items A + left join item_tags AT on A.id = AT.item_id + left join tags T on T.id = AT.tag_id + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/ItemReadService.xml b/backend/src/main/resources/mapper/ItemReadService.xml new file mode 100644 index 0000000..2740017 --- /dev/null +++ b/backend/src/main/resources/mapper/ItemReadService.xml @@ -0,0 +1,160 @@ + + + + + U.id userId, + U.username userUsername, + U.bio userBio, + U.image userImage + + + select + A.id itemId, + A.slug itemSlug, + A.title itemTitle, + A.description itemDescription, + A.image itemImage, + A.created_at itemCreatedAt, + A.updated_at itemUpdatedAt, + T.name tagName, + + from + items A + left join item_tags AT on A.id = AT.item_id + left join tags T on T.id = AT.tag_id + left join users U on U.id = A.seller_id + + + select + DISTINCT(A.id) itemId, A.created_at + from + items A + left join item_tags AT on A.id = AT.item_id + left join tags T on T.id = AT.tag_id + left join item_favorites AF on AF.item_id = A.id + left join users AU on AU.id = A.seller_id + left join users AFU on AFU.id = AF.user_id + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/TagReadService.xml b/backend/src/main/resources/mapper/TagReadService.xml new file mode 100644 index 0000000..0e8ceef --- /dev/null +++ b/backend/src/main/resources/mapper/TagReadService.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/mapper/TransferData.xml b/backend/src/main/resources/mapper/TransferData.xml new file mode 100644 index 0000000..45bcd8d --- /dev/null +++ b/backend/src/main/resources/mapper/TransferData.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/mapper/UserMapper.xml b/backend/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..08e89b2 --- /dev/null +++ b/backend/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,61 @@ + + + + + insert into users (id, username, email, password, bio, image) values( + #{user.id}, + #{user.username}, + #{user.email}, + #{user.password}, + #{user.bio}, + #{user.image} + ) + + + insert into follows(user_id, follow_id) values (#{followRelation.userId}, #{followRelation.targetId}) + + + update users + + username = #{user.username}, + email = #{user.email}, + password = #{user.password}, + bio = #{user.bio}, + image = #{user.image} + + where id = #{user.id} + + + delete from follows where user_id = #{followRelation.userId} and follow_id = #{followRelation.targetId} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/mapper/UserReadService.xml b/backend/src/main/resources/mapper/UserReadService.xml new file mode 100644 index 0000000..edf53c1 --- /dev/null +++ b/backend/src/main/resources/mapper/UserReadService.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/mapper/UserRelationshipQueryService.xml b/backend/src/main/resources/mapper/UserRelationshipQueryService.xml new file mode 100644 index 0000000..8bed754 --- /dev/null +++ b/backend/src/main/resources/mapper/UserRelationshipQueryService.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/backend/src/main/resources/schema/schema.graphqls b/backend/src/main/resources/schema/schema.graphqls new file mode 100644 index 0000000..7a73b4d --- /dev/null +++ b/backend/src/main/resources/schema/schema.graphqls @@ -0,0 +1,177 @@ +# Build the schema. +type Query { + item(slug: String!): Item + items( + first: Int, + after: String, + last: Int, + before: String, + soldBy: String + favoritedBy: String + withTag: String + ): ItemsConnection + me: User + feed(first: Int, after: String, last: Int, before: String): ItemsConnection + profile(username: String!): ProfilePayload + tags: [String] +} + +union UserResult = UserPayload | Error + +type Mutation { + ### User & Profile + createUser(input: CreateUserInput): UserResult + login(password: String!, email: String!): UserPayload + updateUser(changes: UpdateUserInput!): UserPayload + followUser(username: String!): ProfilePayload + unfollowUser(username: String!): ProfilePayload + + ### Item + createItem(input: CreateItemInput!): ItemPayload + updateItem(slug: String!, changes: UpdateItemInput!): ItemPayload + favoriteItem(slug: String!): ItemPayload + unfavoriteItem(slug: String!): ItemPayload + deleteItem(slug: String!): DeletionStatus + + ### Comment + addComment(slug: String!, body: String!): CommentPayload + deleteComment(slug: String!, id: ID!): DeletionStatus +} + +schema { + query: Query + mutation: Mutation +} + +### Items +type Item { + seller: Profile! + comments(first: Int, after: String, last: Int, before: String): CommentsConnection + createdAt: String! + description: String! + favorited: Boolean! + favoritesCount: Int! + image: String! + slug: String! + tagList: [String], + title: String! + updatedAt: String! +} + +type ItemEdge { + cursor: String! + node: Item +} + +type ItemsConnection { + edges: [ItemEdge] + pageInfo: PageInfo! +} + +### Comments +type Comment { + id: ID! + seller: Profile! + item: Item! + body: String! + createdAt: String! + updatedAt: String! +} + +type CommentEdge { + cursor: String! + node: Comment +} + +type CommentsConnection { + edges: [CommentEdge] + pageInfo: PageInfo! +} + +type DeletionStatus { + success: Boolean! +} + +type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String +} + +### Profile +type Profile { + username: String! + bio: String + following: Boolean! + image: String + items(first: Int, after: String, last: Int, before: String): ItemsConnection + favorites(first: Int, after: String, last: Int, before: String): ItemsConnection + feed(first: Int, after: String, last: Int, before: String): ItemsConnection +} + +### User +type User { + email: String! + profile: Profile! + token: String! + username: String! +} + +### Error +type Error { + message: String + errors: [ErrorItem!] +} + +type ErrorItem { + key: String! + value: [String!]! +} + +## Mutations + +# Input types. +input UpdateItemInput { + description: String + image: String + title: String +} + +input CreateItemInput { + description: String! + image: String! + tagList: [String] + title: String! +} + +type ItemPayload { + item: Item +} + +type CommentPayload { + comment: Comment +} + +input CreateUserInput { + email: String! + username: String! + password: String! +} + +input UpdateUserInput { + email: String + username: String + password: String + image: String + bio: String +} + +type UserPayload { + user: User +} + +type ProfilePayload { + profile: Profile +} + diff --git a/backend/src/test/java/io/spring/AnythinkMarketApplicationTests.java b/backend/src/test/java/io/spring/AnythinkMarketApplicationTests.java new file mode 100644 index 0000000..a793b92 --- /dev/null +++ b/backend/src/test/java/io/spring/AnythinkMarketApplicationTests.java @@ -0,0 +1,13 @@ +package io.spring; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest +public class AnythinkMarketApplicationTests { + + @Test + public void contextLoads() {} +} diff --git a/backend/src/test/java/io/spring/TestHelper.java b/backend/src/test/java/io/spring/TestHelper.java new file mode 100644 index 0000000..3dd29f2 --- /dev/null +++ b/backend/src/test/java/io/spring/TestHelper.java @@ -0,0 +1,42 @@ +package io.spring; + +import io.spring.application.data.ItemData; +import io.spring.application.data.ProfileData; +import io.spring.core.item.Item; +import io.spring.core.user.User; +import java.util.ArrayList; +import java.util.Arrays; +import org.joda.time.DateTime; + +public class TestHelper { + public static ItemData itemDataFixture(String seed, User user) { + DateTime now = new DateTime(); + return new ItemData( + seed + "id", + "title-" + seed, + "title " + seed, + "desc " + seed, + "image" + seed, + false, + 0, + now, + now, + new ArrayList<>(), + new ProfileData(user.getId(), user.getUsername(), user.getBio(), user.getImage(), false)); + } + + public static ItemData getItemDataFromItemAndUser(Item item, User user) { + return new ItemData( + item.getId(), + item.getSlug(), + item.getTitle(), + item.getDescription(), + item.getImage(), + false, + 0, + item.getCreatedAt(), + item.getUpdatedAt(), + Arrays.asList("joda"), + new ProfileData(user.getId(), user.getUsername(), user.getBio(), user.getImage(), false)); + } +} diff --git a/backend/src/test/java/io/spring/api/CommentsApiTest.java b/backend/src/test/java/io/spring/api/CommentsApiTest.java new file mode 100644 index 0000000..c13246e --- /dev/null +++ b/backend/src/test/java/io/spring/api/CommentsApiTest.java @@ -0,0 +1,141 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.CommentQueryService; +import io.spring.application.data.CommentData; +import io.spring.application.data.ProfileData; +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(CommentsApi.class) +@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +public class CommentsApiTest extends TestWithCurrentUser { + + @MockBean private ItemRepository itemRepository; + + @MockBean private CommentRepository commentRepository; + @MockBean private CommentQueryService commentQueryService; + + private Item item; + private CommentData commentData; + private Comment comment; + @Autowired private MockMvc mvc; + + @BeforeEach + public void setUp() throws Exception { + RestAssuredMockMvc.mockMvc(mvc); + super.setUp(); + item = new Item("title", "desc", "image", Arrays.asList("test", "java"), user.getId()); + when(itemRepository.findBySlug(eq(item.getSlug()))).thenReturn(Optional.of(item)); + comment = new Comment("comment", user.getId(), item.getId()); + commentData = + new CommentData( + comment.getId(), + comment.getBody(), + comment.getItemId(), + comment.getCreatedAt(), + comment.getCreatedAt(), + new ProfileData( + user.getId(), user.getUsername(), user.getBio(), user.getImage(), false)); + } + + @Test + public void should_create_comment_success() throws Exception { + Map param = + new HashMap() { + { + put( + "comment", + new HashMap() { + { + put("body", "comment content"); + } + }); + } + }; + + when(commentQueryService.findById(anyString(), eq(user))).thenReturn(Optional.of(commentData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/api/items/{slug}/comments", item.getSlug()) + .then() + .statusCode(201) + .body("comment.body", equalTo(commentData.getBody())); + } + + @Test + public void should_get_422_with_empty_body() throws Exception { + Map param = + new HashMap() { + { + put( + "comment", + new HashMap() { + { + put("body", ""); + } + }); + } + }; + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/api/items/{slug}/comments", item.getSlug()) + .then() + .statusCode(422) + .body("errors.body[0]", equalTo("can't be empty")); + } + + @Test + public void should_get_comments_of_item_success() throws Exception { + when(commentQueryService.findByItemId(anyString(), eq(null))) + .thenReturn(Arrays.asList(commentData)); + RestAssuredMockMvc.when() + .get("/api/items/{slug}/comments", item.getSlug()) + .prettyPeek() + .then() + .statusCode(200) + .body("comments[0].id", equalTo(commentData.getId())); + } + + @Test + public void should_delete_comment_success() throws Exception { + when(commentRepository.findById(eq(item.getId()), eq(comment.getId()))) + .thenReturn(Optional.of(comment)); + + given() + .header("Authorization", "Token " + token) + .when() + .delete("/api/items/{slug}/comments/{id}", item.getSlug(), comment.getId()) + .then() + .statusCode(204); + } +} diff --git a/backend/src/test/java/io/spring/api/CurrentUserApiTest.java b/backend/src/test/java/io/spring/api/CurrentUserApiTest.java new file mode 100644 index 0000000..e00e05c --- /dev/null +++ b/backend/src/test/java/io/spring/api/CurrentUserApiTest.java @@ -0,0 +1,179 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.UserQueryService; +import io.spring.application.user.UserService; +import io.spring.core.user.User; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(CurrentUserApi.class) +@Import({ + WebSecurityConfig.class, + JacksonCustomizations.class, + UserService.class, + ValidationAutoConfiguration.class, + BCryptPasswordEncoder.class +}) +public class CurrentUserApiTest extends TestWithCurrentUser { + + @Autowired private MockMvc mvc; + + @MockBean private UserQueryService userQueryService; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + } + + @Test + public void should_get_current_user_with_token() throws Exception { + when(userQueryService.findById(any())).thenReturn(Optional.of(userData)); + + given() + .header("Authorization", "Token " + token) + .contentType("application/json") + .when() + .get("/api/user") + .then() + .statusCode(200) + .body("user.email", equalTo(email)) + .body("user.username", equalTo(username)) + .body("user.bio", equalTo("")) + .body("user.image", equalTo(defaultAvatar)) + .body("user.token", equalTo(token)); + } + + @Test + public void should_get_401_without_token() throws Exception { + given().contentType("application/json").when().get("/api/user").then().statusCode(401); + } + + @Test + public void should_get_401_with_invalid_token() throws Exception { + String invalidToken = "asdfasd"; + when(jwtService.getSubFromToken(eq(invalidToken))).thenReturn(Optional.empty()); + given() + .contentType("application/json") + .header("Authorization", "Token " + invalidToken) + .when() + .get("/api/user") + .then() + .statusCode(401); + } + + @Test + public void should_update_current_user_profile() throws Exception { + String newEmail = "newemail@example.com"; + String newBio = "updated"; + String newUsername = "newusernamee"; + + Map param = + new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", newEmail); + put("bio", newBio); + put("username", newUsername); + } + }); + } + }; + + when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); + when(userRepository.findByEmail(eq(newEmail))).thenReturn(Optional.empty()); + + when(userQueryService.findById(eq(user.getId()))).thenReturn(Optional.of(userData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .put("/api/user") + .then() + .statusCode(200); + } + + @Test + public void should_get_error_if_email_exists_when_update_user_profile() throws Exception { + String newEmail = "newemail@example.com"; + String newBio = "updated"; + String newUsername = "newusernamee"; + + Map param = prepareUpdateParam(newEmail, newBio, newUsername); + + when(userRepository.findByEmail(eq(newEmail))) + .thenReturn(Optional.of(new User(newEmail, "username", "123", "", ""))); + when(userRepository.findByUsername(eq(newUsername))).thenReturn(Optional.empty()); + + when(userQueryService.findById(eq(user.getId()))).thenReturn(Optional.of(userData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .put("/api/user") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.email[0]", equalTo("email already exist")); + } + + private HashMap prepareUpdateParam( + final String newEmail, final String newBio, final String newUsername) { + return new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", newEmail); + put("bio", newBio); + put("username", newUsername); + } + }); + } + }; + } + + @Test + public void should_get_401_if_not_login() throws Exception { + given() + .contentType("application/json") + .body( + new HashMap() { + { + put("user", new HashMap()); + } + }) + .when() + .put("/api/user") + .then() + .statusCode(401); + } +} diff --git a/backend/src/test/java/io/spring/api/ItemApiTest.java b/backend/src/test/java/io/spring/api/ItemApiTest.java new file mode 100644 index 0000000..c521582 --- /dev/null +++ b/backend/src/test/java/io/spring/api/ItemApiTest.java @@ -0,0 +1,222 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.TestHelper; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.ItemQueryService; +import io.spring.application.data.ItemData; +import io.spring.application.data.ProfileData; +import io.spring.application.item.ItemCommandService; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.user.User; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest({ItemApi.class}) +@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +public class ItemApiTest extends TestWithCurrentUser { + @Autowired private MockMvc mvc; + + @MockBean private ItemQueryService itemQueryService; + + @MockBean private ItemRepository itemRepository; + + @MockBean ItemCommandService itemCommandService; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + } + + @Test + public void should_read_item_success() throws Exception { + String slug = "test-new-item"; + DateTime time = new DateTime(); + Item item = + new Item( + "Test New Item", + "Desc", + "Image", + Arrays.asList("java", "spring", "jpg"), + user.getId(), + time); + ItemData itemData = TestHelper.getItemDataFromItemAndUser(item, user); + + when(itemQueryService.findBySlug(eq(slug), eq(null))).thenReturn(Optional.of(itemData)); + + RestAssuredMockMvc.when() + .get("/api/items/{slug}", slug) + .then() + .statusCode(200) + .body("item.slug", equalTo(slug)) + .body("item.image", equalTo(itemData.getImage())) + .body("item.createdAt", equalTo(ISODateTimeFormat.dateTime().withZoneUTC().print(time))); + } + + @Test + public void should_404_if_item_not_found() throws Exception { + when(itemQueryService.findBySlug(anyString(), any())).thenReturn(Optional.empty()); + RestAssuredMockMvc.when().get("/api/items/not-exists").then().statusCode(404); + } + + @Test + public void should_update_item_content_success() throws Exception { + List tagList = Arrays.asList("java", "spring", "jpg"); + + Item originalItem = + new Item("old title", "old description", "old image", tagList, user.getId()); + + Item updatedItem = new Item("new title", "new description", "old image", tagList, user.getId()); + + Map updateParam = + prepareUpdateParam( + updatedItem.getTitle(), updatedItem.getImage(), updatedItem.getDescription()); + + ItemData updatedItemData = TestHelper.getItemDataFromItemAndUser(updatedItem, user); + + when(itemRepository.findBySlug(eq(originalItem.getSlug()))) + .thenReturn(Optional.of(originalItem)); + when(itemCommandService.updateItem(eq(originalItem), any())).thenReturn(updatedItem); + when(itemQueryService.findBySlug(eq(updatedItem.getSlug()), eq(user))) + .thenReturn(Optional.of(updatedItemData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(updateParam) + .when() + .put("/api/items/{slug}", originalItem.getSlug()) + .then() + .statusCode(200) + .body("item.slug", equalTo(updatedItemData.getSlug())); + } + + @Test + public void should_get_403_if_not_user_to_update_item() throws Exception { + String title = "new-title"; + String image = "new image"; + String description = "new description"; + Map updateParam = prepareUpdateParam(title, image, description); + + User anotherUser = new User("test@test.com", "test", "123123", "", ""); + + Item item = + new Item( + title, description, image, Arrays.asList("java", "spring", "jpg"), anotherUser.getId()); + + DateTime time = new DateTime(); + ItemData itemData = + new ItemData( + item.getId(), + item.getSlug(), + item.getTitle(), + item.getDescription(), + item.getImage(), + false, + 0, + time, + time, + Arrays.asList("joda"), + new ProfileData( + anotherUser.getId(), + anotherUser.getUsername(), + anotherUser.getBio(), + anotherUser.getImage(), + false)); + + when(itemRepository.findBySlug(eq(item.getSlug()))).thenReturn(Optional.of(item)); + when(itemQueryService.findBySlug(eq(item.getSlug()), eq(user))) + .thenReturn(Optional.of(itemData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(updateParam) + .when() + .put("/api/items/{slug}", item.getSlug()) + .then() + .statusCode(403); + } + + @Test + public void should_delete_item_success() throws Exception { + String title = "title"; + String image = "image"; + String description = "description"; + + Item item = + new Item(title, description, image, Arrays.asList("java", "spring", "jpg"), user.getId()); + when(itemRepository.findBySlug(eq(item.getSlug()))).thenReturn(Optional.of(item)); + + given() + .header("Authorization", "Token " + token) + .when() + .delete("/api/items/{slug}", item.getSlug()) + .then() + .statusCode(204); + + verify(itemRepository).remove(eq(item)); + } + + @Test + public void should_403_if_not_author_delete_item() throws Exception { + String title = "new-title"; + String image = "new image"; + String description = "new description"; + + User anotherUser = new User("test@test.com", "test", "123123", "", ""); + + Item item = + new Item( + title, description, image, Arrays.asList("java", "spring", "jpg"), anotherUser.getId()); + + when(itemRepository.findBySlug(eq(item.getSlug()))).thenReturn(Optional.of(item)); + given() + .header("Authorization", "Token " + token) + .when() + .delete("/api/items/{slug}", item.getSlug()) + .then() + .statusCode(403); + } + + private HashMap prepareUpdateParam( + final String title, final String image, final String description) { + return new HashMap() { + { + put( + "item", + new HashMap() { + { + put("title", title); + put("image", image); + put("description", description); + } + }); + } + }; + } +} diff --git a/backend/src/test/java/io/spring/api/ItemFavoriteApiTest.java b/backend/src/test/java/io/spring/api/ItemFavoriteApiTest.java new file mode 100644 index 0000000..5455a3c --- /dev/null +++ b/backend/src/test/java/io/spring/api/ItemFavoriteApiTest.java @@ -0,0 +1,103 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.ItemQueryService; +import io.spring.application.data.ItemData; +import io.spring.application.data.ProfileData; +import io.spring.core.favorite.ItemFavorite; +import io.spring.core.favorite.ItemFavoriteRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.item.Tag; +import io.spring.core.user.User; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(ItemFavoriteApi.class) +@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +public class ItemFavoriteApiTest extends TestWithCurrentUser { + @Autowired private MockMvc mvc; + + @MockBean private ItemFavoriteRepository itemFavoriteRepository; + + @MockBean private ItemRepository itemRepository; + + @MockBean private ItemQueryService itemQueryService; + + private Item item; + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + User anotherUser = new User("other@test.com", "other", "123", "", ""); + item = new Item("title", "desc", "image", Arrays.asList("java"), anotherUser.getId()); + when(itemRepository.findBySlug(eq(item.getSlug()))).thenReturn(Optional.of(item)); + ItemData itemData = + new ItemData( + item.getId(), + item.getSlug(), + item.getTitle(), + item.getDescription(), + item.getImage(), + true, + 1, + item.getCreatedAt(), + item.getUpdatedAt(), + item.getTags().stream().map(Tag::getName).collect(Collectors.toList()), + new ProfileData( + anotherUser.getId(), + anotherUser.getUsername(), + anotherUser.getBio(), + anotherUser.getImage(), + false)); + when(itemQueryService.findBySlug(eq(itemData.getSlug()), eq(user))) + .thenReturn(Optional.of(itemData)); + } + + @Test + public void should_favorite_an_item_success() throws Exception { + given() + .header("Authorization", "Token " + token) + .when() + .post("/api/items/{slug}/favorite", item.getSlug()) + .prettyPeek() + .then() + .statusCode(200) + .body("item.id", equalTo(item.getId())); + + verify(itemFavoriteRepository).save(any()); + } + + @Test + public void should_unfavorite_an_item_success() throws Exception { + when(itemFavoriteRepository.find(eq(item.getId()), eq(user.getId()))) + .thenReturn(Optional.of(new ItemFavorite(item.getId(), user.getId()))); + given() + .header("Authorization", "Token " + token) + .when() + .delete("/api/items/{slug}/favorite", item.getSlug()) + .prettyPeek() + .then() + .statusCode(200) + .body("item.id", equalTo(item.getId())); + verify(itemFavoriteRepository).remove(new ItemFavorite(item.getId(), user.getId())); + } +} diff --git a/backend/src/test/java/io/spring/api/ItemsApiTest.java b/backend/src/test/java/io/spring/api/ItemsApiTest.java new file mode 100644 index 0000000..bc02966 --- /dev/null +++ b/backend/src/test/java/io/spring/api/ItemsApiTest.java @@ -0,0 +1,174 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static java.util.Arrays.asList; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.ItemQueryService; +import io.spring.application.data.ItemData; +import io.spring.application.data.ProfileData; +import io.spring.application.item.ItemCommandService; +import io.spring.core.item.Item; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.joda.time.DateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest({ItemsApi.class}) +@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +public class ItemsApiTest extends TestWithCurrentUser { + @Autowired private MockMvc mvc; + + @MockBean private ItemQueryService itemQueryService; + + @MockBean private ItemCommandService itemCommandService; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + } + + @Test + public void should_create_item_success() throws Exception { + String title = "How to train your dragon"; + String slug = "how-to-train-your-dragon"; + String description = "Ever wonder how?"; + String image = "Another image"; + List tagList = asList("reactjs", "angularjs", "dragons"); + Map param = prepareParam(title, description, image, tagList); + + ItemData itemData = + new ItemData( + "123", + slug, + title, + description, + image, + false, + 0, + new DateTime(), + new DateTime(), + tagList, + new ProfileData("userid", user.getUsername(), user.getBio(), user.getImage(), false)); + + when(itemCommandService.createItem(any(), any())) + .thenReturn(new Item(title, description, image, tagList, user.getId())); + + when(itemQueryService.findBySlug(eq(Item.toSlug(title)), any())).thenReturn(Optional.empty()); + + when(itemQueryService.findById(any(), any())).thenReturn(Optional.of(itemData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/api/items") + .then() + .statusCode(200) + .body("item.title", equalTo(title)) + .body("item.favorited", equalTo(false)) + .body("item.favoritesCount", equalTo(0)) + .body("item.seller.username", equalTo(user.getUsername())) + .body("item.seller.id", equalTo(null)); + + verify(itemCommandService).createItem(any(), any()); + } + + @Test + public void should_get_error_message_with_wrong_parameter() throws Exception { + String title = ""; + String description = "Ever wonder how?"; + String image = "Image URL"; + String[] tagList = {"reactjs", "angularjs", "dragons"}; + Map param = prepareParam(title, description, image, asList(tagList)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/api/items") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.title[0]", equalTo("can't be empty")); + } + + @Test + public void should_get_error_message_with_duplicated_title() { + String title = "How to train your dragon"; + String slug = "how-to-train-your-dragon"; + String description = "Ever wonder how?"; + String image = "Image URL"; + String[] tagList = {"reactjs", "angularjs", "dragons"}; + Map param = prepareParam(title, description, image, asList(tagList)); + + ItemData itemData = + new ItemData( + "123", + slug, + title, + description, + image, + false, + 0, + new DateTime(), + new DateTime(), + asList(tagList), + new ProfileData("userid", user.getUsername(), user.getBio(), user.getImage(), false)); + + when(itemQueryService.findBySlug(eq(Item.toSlug(title)), any())) + .thenReturn(Optional.of(itemData)); + + when(itemQueryService.findById(any(), any())).thenReturn(Optional.of(itemData)); + + given() + .contentType("application/json") + .header("Authorization", "Token " + token) + .body(param) + .when() + .post("/api/items") + .prettyPeek() + .then() + .statusCode(422); + } + + private HashMap prepareParam( + final String title, + final String description, + final String image, + final List tagList) { + return new HashMap() { + { + put( + "item", + new HashMap() { + { + put("title", title); + put("description", description); + put("image", image); + put("tagList", tagList); + } + }); + } + }; + } +} diff --git a/backend/src/test/java/io/spring/api/ListItemApiTest.java b/backend/src/test/java/io/spring/api/ListItemApiTest.java new file mode 100644 index 0000000..9f36d2c --- /dev/null +++ b/backend/src/test/java/io/spring/api/ListItemApiTest.java @@ -0,0 +1,72 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static io.spring.TestHelper.itemDataFixture; +import static java.util.Arrays.asList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.ItemQueryService; +import io.spring.application.Page; +import io.spring.application.data.ItemDataList; +import io.spring.application.item.ItemCommandService; +import io.spring.core.item.ItemRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(ItemsApi.class) +@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +public class ListItemApiTest extends TestWithCurrentUser { + @MockBean private ItemRepository itemRepository; + + @MockBean private ItemQueryService itemQueryService; + + @MockBean private ItemCommandService itemCommandService; + + @Autowired private MockMvc mvc; + + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + } + + @Test + public void should_get_default_item_list() throws Exception { + ItemDataList itemDataList = + new ItemDataList(asList(itemDataFixture("1", user), itemDataFixture("2", user)), 2); + when(itemQueryService.findRecentItems( + eq(null), eq(null), eq(null), eq(new Page(0, 20)), eq(null))) + .thenReturn(itemDataList); + RestAssuredMockMvc.when().get("/api/items").prettyPeek().then().statusCode(200); + } + + @Test + public void should_get_feeds_401_without_login() throws Exception { + RestAssuredMockMvc.when().get("/api/items/feed").prettyPeek().then().statusCode(401); + } + + @Test + public void should_get_feeds_success() throws Exception { + ItemDataList itemDataList = + new ItemDataList(asList(itemDataFixture("1", user), itemDataFixture("2", user)), 2); + when(itemQueryService.findUserFeed(eq(user), eq(new Page(0, 20)))).thenReturn(itemDataList); + + given() + .header("Authorization", "Token " + token) + .when() + .get("/api/items/feed") + .prettyPeek() + .then() + .statusCode(200); + } +} diff --git a/backend/src/test/java/io/spring/api/ProfileApiTest.java b/backend/src/test/java/io/spring/api/ProfileApiTest.java new file mode 100644 index 0000000..2e3ab9c --- /dev/null +++ b/backend/src/test/java/io/spring/api/ProfileApiTest.java @@ -0,0 +1,96 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.ProfileQueryService; +import io.spring.application.data.ProfileData; +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(ProfileApi.class) +@Import({WebSecurityConfig.class, JacksonCustomizations.class}) +public class ProfileApiTest extends TestWithCurrentUser { + private User anotherUser; + + @Autowired private MockMvc mvc; + + @MockBean private ProfileQueryService profileQueryService; + + private ProfileData profileData; + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + RestAssuredMockMvc.mockMvc(mvc); + anotherUser = new User("username@test.com", "username", "123", "", ""); + profileData = + new ProfileData( + anotherUser.getId(), + anotherUser.getUsername(), + anotherUser.getBio(), + anotherUser.getImage(), + false); + when(userRepository.findByUsername(eq(anotherUser.getUsername()))) + .thenReturn(Optional.of(anotherUser)); + } + + @Test + public void should_get_user_profile_success() throws Exception { + when(profileQueryService.findByUsername(eq(profileData.getUsername()), eq(null))) + .thenReturn(Optional.of(profileData)); + RestAssuredMockMvc.when() + .get("/api/profiles/{username}", profileData.getUsername()) + .prettyPeek() + .then() + .statusCode(200) + .body("profile.username", equalTo(profileData.getUsername())); + } + + @Test + public void should_follow_user_success() throws Exception { + when(profileQueryService.findByUsername(eq(profileData.getUsername()), eq(user))) + .thenReturn(Optional.of(profileData)); + given() + .header("Authorization", "Token " + token) + .when() + .post("/api/profiles/{username}/follow", anotherUser.getUsername()) + .prettyPeek() + .then() + .statusCode(200); + verify(userRepository).saveRelation(new FollowRelation(user.getId(), anotherUser.getId())); + } + + @Test + public void should_unfollow_user_success() throws Exception { + FollowRelation followRelation = new FollowRelation(user.getId(), anotherUser.getId()); + when(userRepository.findRelation(eq(user.getId()), eq(anotherUser.getId()))) + .thenReturn(Optional.of(followRelation)); + when(profileQueryService.findByUsername(eq(profileData.getUsername()), eq(user))) + .thenReturn(Optional.of(profileData)); + + given() + .header("Authorization", "Token " + token) + .when() + .delete("/api/profiles/{username}/follow", anotherUser.getUsername()) + .prettyPeek() + .then() + .statusCode(200); + + verify(userRepository).removeRelation(eq(followRelation)); + } +} diff --git a/backend/src/test/java/io/spring/api/TestWithCurrentUser.java b/backend/src/test/java/io/spring/api/TestWithCurrentUser.java new file mode 100644 index 0000000..7d3b104 --- /dev/null +++ b/backend/src/test/java/io/spring/api/TestWithCurrentUser.java @@ -0,0 +1,49 @@ +package io.spring.api; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import io.spring.application.data.UserData; +import io.spring.core.service.JwtService; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.mybatis.readservice.UserReadService; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.mock.mockito.MockBean; + +abstract class TestWithCurrentUser { + @MockBean protected UserRepository userRepository; + + @MockBean protected UserReadService userReadService; + + protected User user; + protected UserData userData; + protected String token; + protected String email; + protected String username; + protected String defaultAvatar; + + @MockBean protected JwtService jwtService; + + protected void userFixture() { + email = "john@jacob.com"; + username = "johnjacob"; + defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg"; + + user = new User(email, username, "123", "", defaultAvatar); + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.of(user)); + when(userRepository.findById(eq(user.getId()))).thenReturn(Optional.of(user)); + + userData = new UserData(user.getId(), email, username, "", defaultAvatar); + when(userReadService.findById(eq(user.getId()))).thenReturn(userData); + + token = "token"; + when(jwtService.getSubFromToken(eq(token))).thenReturn(Optional.of(user.getId())); + } + + @BeforeEach + public void setUp() throws Exception { + userFixture(); + } +} diff --git a/backend/src/test/java/io/spring/api/UsersApiTest.java b/backend/src/test/java/io/spring/api/UsersApiTest.java new file mode 100644 index 0000000..2eb3ba6 --- /dev/null +++ b/backend/src/test/java/io/spring/api/UsersApiTest.java @@ -0,0 +1,271 @@ +package io.spring.api; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import io.spring.JacksonCustomizations; +import io.spring.api.security.WebSecurityConfig; +import io.spring.application.UserQueryService; +import io.spring.application.data.UserData; +import io.spring.application.user.UserService; +import io.spring.core.service.JwtService; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.mybatis.readservice.UserReadService; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(UsersApi.class) +@Import({ + WebSecurityConfig.class, + UserQueryService.class, + BCryptPasswordEncoder.class, + JacksonCustomizations.class +}) +public class UsersApiTest { + @Autowired private MockMvc mvc; + + @MockBean private UserRepository userRepository; + + @MockBean private JwtService jwtService; + + @MockBean private UserReadService userReadService; + + @MockBean private UserService userService; + + @Autowired private PasswordEncoder passwordEncoder; + + private String defaultAvatar; + + @BeforeEach + public void setUp() throws Exception { + RestAssuredMockMvc.mockMvc(mvc); + defaultAvatar = "https://static.productionready.io/images/smiley-cyrus.jpg"; + } + + @Test + public void should_create_user_success() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob"; + + when(jwtService.toToken(any())).thenReturn("123"); + User user = new User(email, username, "123", "", defaultAvatar); + UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); + when(userReadService.findById(any())).thenReturn(userData); + + when(userService.createUser(any())).thenReturn(user); + + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); + when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty()); + + Map param = prepareRegisterParameter(email, username); + + given() + .contentType("application/json") + .body(param) + .when() + .post("/api/users") + .then() + .statusCode(201) + .body("user.email", equalTo(email)) + .body("user.username", equalTo(username)) + .body("user.bio", equalTo("")) + .body("user.image", equalTo(defaultAvatar)) + .body("user.token", equalTo("123")); + + verify(userService).createUser(any()); + } + + @Test + public void should_show_error_message_for_blank_username() throws Exception { + + String email = "john@jacob.com"; + String username = ""; + + Map param = prepareRegisterParameter(email, username); + + given() + .contentType("application/json") + .body(param) + .when() + .post("/api/users") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.username[0]", equalTo("can't be empty")); + } + + @Test + public void should_show_error_message_for_invalid_email() throws Exception { + String email = "johnxjacob.com"; + String username = "johnjacob"; + + Map param = prepareRegisterParameter(email, username); + + given() + .contentType("application/json") + .body(param) + .when() + .post("/api/users") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.email[0]", equalTo("should be an email")); + } + + @Test + public void should_show_error_for_duplicated_username() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob"; + + when(userRepository.findByUsername(eq(username))) + .thenReturn(Optional.of(new User(email, username, "123", "bio", ""))); + when(userRepository.findByEmail(any())).thenReturn(Optional.empty()); + + Map param = prepareRegisterParameter(email, username); + + given() + .contentType("application/json") + .body(param) + .when() + .post("/api/users") + .prettyPeek() + .then() + .statusCode(422) + .body("errors.username[0]", equalTo("duplicated username")); + } + + @Test + public void should_show_error_for_duplicated_email() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob2"; + + when(userRepository.findByEmail(eq(email))) + .thenReturn(Optional.of(new User(email, username, "123", "bio", ""))); + + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); + + Map param = prepareRegisterParameter(email, username); + + given() + .contentType("application/json") + .body(param) + .when() + .post("/api/users") + .then() + .statusCode(422) + .body("errors.email[0]", equalTo("duplicated email")); + } + + private HashMap prepareRegisterParameter( + final String email, final String username) { + return new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", email); + put("password", "johnnyjacob"); + put("username", username); + } + }); + } + }; + } + + @Test + public void should_login_success() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob2"; + String password = "123"; + + User user = new User(email, username, passwordEncoder.encode(password), "", defaultAvatar); + UserData userData = new UserData("123", email, username, "", defaultAvatar); + + when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); + when(userReadService.findById(eq(user.getId()))).thenReturn(userData); + when(jwtService.toToken(any())).thenReturn("123"); + + Map param = + new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", email); + put("password", password); + } + }); + } + }; + + given() + .contentType("application/json") + .body(param) + .when() + .post("/api/users/login") + .then() + .statusCode(200) + .body("user.email", equalTo(email)) + .body("user.username", equalTo(username)) + .body("user.bio", equalTo("")) + .body("user.image", equalTo(defaultAvatar)) + .body("user.token", equalTo("123")); + ; + } + + @Test + public void should_fail_login_with_wrong_password() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob2"; + String password = "123"; + + User user = new User(email, username, password, "", defaultAvatar); + UserData userData = new UserData(user.getId(), email, username, "", defaultAvatar); + + when(userRepository.findByEmail(eq(email))).thenReturn(Optional.of(user)); + when(userReadService.findByUsername(eq(username))).thenReturn(userData); + + Map param = + new HashMap() { + { + put( + "user", + new HashMap() { + { + put("email", email); + put("password", "123123"); + } + }); + } + }; + + given() + .contentType("application/json") + .body(param) + .when() + .post("/api/users/login") + .prettyPeek() + .then() + .statusCode(422) + .body("message", equalTo("invalid email or password")); + } +} diff --git a/backend/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java b/backend/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java new file mode 100644 index 0000000..e36bc35 --- /dev/null +++ b/backend/src/test/java/io/spring/application/comment/CommentQueryServiceTest.java @@ -0,0 +1,76 @@ +package io.spring.application.comment; + +import io.spring.application.CommentQueryService; +import io.spring.application.data.CommentData; +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisCommentRepository; +import io.spring.infrastructure.repository.MyBatisItemRepository; +import io.spring.infrastructure.repository.MyBatisUserRepository; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({ + MyBatisCommentRepository.class, + MyBatisUserRepository.class, + CommentQueryService.class, + MyBatisItemRepository.class +}) +public class CommentQueryServiceTest extends DbTestBase { + @Autowired private CommentRepository commentRepository; + + @Autowired private UserRepository userRepository; + + @Autowired private CommentQueryService commentQueryService; + + @Autowired private ItemRepository itemRepository; + + private User user; + + @BeforeEach + public void setUp() { + user = new User("aisensiy@test.com", "aisensiy", "123", "", ""); + userRepository.save(user); + } + + @Test + public void should_read_comment_success() { + Comment comment = new Comment("content", user.getId(), "123"); + commentRepository.save(comment); + + Optional optional = commentQueryService.findById(comment.getId(), user); + Assertions.assertTrue(optional.isPresent()); + CommentData commentData = optional.get(); + Assertions.assertEquals(commentData.getProfileData().getUsername(), user.getUsername()); + } + + @Test + public void should_read_comments_of_item() { + Item item = new Item("title", "desc", "image", Arrays.asList("java"), user.getId()); + itemRepository.save(item); + + User user2 = new User("user2@email.com", "user2", "123", "", ""); + userRepository.save(user2); + userRepository.saveRelation(new FollowRelation(user.getId(), user2.getId())); + + Comment comment1 = new Comment("content1", user.getId(), item.getId()); + commentRepository.save(comment1); + Comment comment2 = new Comment("content2", user2.getId(), item.getId()); + commentRepository.save(comment2); + + List comments = commentQueryService.findByItemId(item.getId(), user); + Assertions.assertEquals(comments.size(), 2); + } +} diff --git a/backend/src/test/java/io/spring/application/item/ItemQueryServiceTest.java b/backend/src/test/java/io/spring/application/item/ItemQueryServiceTest.java new file mode 100644 index 0000000..7338ea6 --- /dev/null +++ b/backend/src/test/java/io/spring/application/item/ItemQueryServiceTest.java @@ -0,0 +1,226 @@ +package io.spring.application.item; + +import io.spring.application.CursorPageParameter; +import io.spring.application.CursorPager; +import io.spring.application.CursorPager.Direction; +import io.spring.application.DateTimeCursor; +import io.spring.application.ItemQueryService; +import io.spring.application.Page; +import io.spring.application.data.ItemData; +import io.spring.application.data.ItemDataList; +import io.spring.core.favorite.ItemFavorite; +import io.spring.core.favorite.ItemFavoriteRepository; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisItemFavoriteRepository; +import io.spring.infrastructure.repository.MyBatisItemRepository; +import io.spring.infrastructure.repository.MyBatisUserRepository; +import java.util.Arrays; +import java.util.Optional; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({ + ItemQueryService.class, + MyBatisUserRepository.class, + MyBatisItemRepository.class, + MyBatisItemFavoriteRepository.class +}) +public class ItemQueryServiceTest extends DbTestBase { + @Autowired private ItemQueryService queryService; + + @Autowired private ItemRepository itemRepository; + + @Autowired private UserRepository userRepository; + + @Autowired private ItemFavoriteRepository itemFavoriteRepository; + + private User user; + private Item item; + + @BeforeEach + public void setUp() { + user = new User("aisensiy@gmail.com", "aisensiy", "123", "", ""); + userRepository.save(user); + item = + new Item( + "test", "desc", "image", Arrays.asList("java", "spring"), user.getId(), new DateTime()); + itemRepository.save(item); + } + + @Test + public void should_fetch_item_success() { + Optional optional = queryService.findById(item.getId(), user); + Assertions.assertTrue(optional.isPresent()); + + ItemData fetched = optional.get(); + Assertions.assertEquals(fetched.getFavoritesCount(), 0); + Assertions.assertFalse(fetched.isFavorited()); + Assertions.assertNotNull(fetched.getCreatedAt()); + Assertions.assertNotNull(fetched.getUpdatedAt()); + Assertions.assertTrue(fetched.getTagList().contains("java")); + } + + @Test + public void should_get_item_with_right_favorite_and_favorite_count() { + User anotherUser = new User("other@test.com", "other", "123", "", ""); + userRepository.save(anotherUser); + itemFavoriteRepository.save(new ItemFavorite(item.getId(), anotherUser.getId())); + + Optional optional = queryService.findById(item.getId(), anotherUser); + Assertions.assertTrue(optional.isPresent()); + + ItemData itemData = optional.get(); + Assertions.assertEquals(itemData.getFavoritesCount(), 1); + Assertions.assertTrue(itemData.isFavorited()); + } + + @Test + public void should_get_default_item_list() { + Item anotherItem = + new Item( + "new item", + "desc", + "image", + Arrays.asList("test"), + user.getId(), + new DateTime().minusHours(1)); + itemRepository.save(anotherItem); + + ItemDataList recentItems = queryService.findRecentItems(null, null, null, new Page(), user); + Assertions.assertEquals(recentItems.getCount(), 2); + Assertions.assertEquals(recentItems.getItemDatas().size(), 2); + Assertions.assertEquals(recentItems.getItemDatas().get(0).getId(), item.getId()); + + ItemDataList nodata = queryService.findRecentItems(null, null, null, new Page(2, 10), user); + Assertions.assertEquals(nodata.getCount(), 2); + Assertions.assertEquals(nodata.getItemDatas().size(), 0); + } + + @Test + public void should_get_default_item_list_by_cursor() { + Item anotherItem = + new Item( + "new item", + "desc", + "image", + Arrays.asList("test"), + user.getId(), + new DateTime().minusHours(1)); + itemRepository.save(anotherItem); + + CursorPager recentItems = + queryService.findRecentItemsWithCursor( + null, null, null, new CursorPageParameter<>(null, 20, Direction.NEXT), user); + Assertions.assertEquals(recentItems.getData().size(), 2); + Assertions.assertEquals(recentItems.getData().get(0).getId(), item.getId()); + + CursorPager nodata = + queryService.findRecentItemsWithCursor( + null, + null, + null, + new CursorPageParameter( + DateTimeCursor.parse(recentItems.getEndCursor().toString()), 20, Direction.NEXT), + user); + Assertions.assertEquals(nodata.getData().size(), 0); + Assertions.assertEquals(nodata.getStartCursor(), null); + + CursorPager prevItems = + queryService.findRecentItemsWithCursor( + null, null, null, new CursorPageParameter<>(null, 20, Direction.PREV), user); + Assertions.assertEquals(prevItems.getData().size(), 2); + } + + @Test + public void should_query_item_by_seller() { + User anotherUser = new User("other@email.com", "other", "123", "", ""); + userRepository.save(anotherUser); + + Item anotherItem = + new Item("new item", "desc", "image", Arrays.asList("test"), anotherUser.getId()); + itemRepository.save(anotherItem); + + ItemDataList recentItems = + queryService.findRecentItems(null, user.getUsername(), null, new Page(), user); + Assertions.assertEquals(recentItems.getItemDatas().size(), 1); + Assertions.assertEquals(recentItems.getCount(), 1); + } + + @Test + public void should_query_item_by_favorite() { + User anotherUser = new User("other@email.com", "other", "123", "", ""); + userRepository.save(anotherUser); + + Item anotherItem = + new Item("new item", "desc", "image", Arrays.asList("test"), anotherUser.getId()); + itemRepository.save(anotherItem); + + ItemFavorite itemFavorite = new ItemFavorite(item.getId(), anotherUser.getId()); + itemFavoriteRepository.save(itemFavorite); + + ItemDataList recentItems = + queryService.findRecentItems( + null, null, anotherUser.getUsername(), new Page(), anotherUser); + Assertions.assertEquals(recentItems.getItemDatas().size(), 1); + Assertions.assertEquals(recentItems.getCount(), 1); + ItemData itemData = recentItems.getItemDatas().get(0); + Assertions.assertEquals(itemData.getId(), item.getId()); + Assertions.assertEquals(itemData.getFavoritesCount(), 1); + Assertions.assertTrue(itemData.isFavorited()); + } + + @Test + public void should_query_item_by_tag() { + Item anotherItem = new Item("new item", "desc", "image", Arrays.asList("test"), user.getId()); + itemRepository.save(anotherItem); + + ItemDataList recentItems = queryService.findRecentItems("spring", null, null, new Page(), user); + Assertions.assertEquals(recentItems.getItemDatas().size(), 1); + Assertions.assertEquals(recentItems.getCount(), 1); + Assertions.assertEquals(recentItems.getItemDatas().get(0).getId(), item.getId()); + + ItemDataList notag = queryService.findRecentItems("notag", null, null, new Page(), user); + Assertions.assertEquals(notag.getCount(), 0); + } + + @Test + public void should_show_following_if_user_followed_seller() { + User anotherUser = new User("other@email.com", "other", "123", "", ""); + userRepository.save(anotherUser); + + FollowRelation followRelation = new FollowRelation(anotherUser.getId(), user.getId()); + userRepository.saveRelation(followRelation); + + ItemDataList recentItems = + queryService.findRecentItems(null, null, null, new Page(), anotherUser); + Assertions.assertEquals(recentItems.getCount(), 1); + ItemData itemData = recentItems.getItemDatas().get(0); + Assertions.assertTrue(itemData.getProfileData().isFollowing()); + } + + @Test + public void should_get_user_feed() { + User anotherUser = new User("other@email.com", "other", "123", "", ""); + userRepository.save(anotherUser); + + FollowRelation followRelation = new FollowRelation(anotherUser.getId(), user.getId()); + userRepository.saveRelation(followRelation); + + ItemDataList userFeed = queryService.findUserFeed(user, new Page()); + Assertions.assertEquals(userFeed.getCount(), 0); + + ItemDataList anotherUserFeed = queryService.findUserFeed(anotherUser, new Page()); + Assertions.assertEquals(anotherUserFeed.getCount(), 1); + ItemData itemData = anotherUserFeed.getItemDatas().get(0); + Assertions.assertTrue(itemData.getProfileData().isFollowing()); + } +} diff --git a/backend/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java b/backend/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java new file mode 100644 index 0000000..34ce502 --- /dev/null +++ b/backend/src/test/java/io/spring/application/profile/ProfileQueryServiceTest.java @@ -0,0 +1,30 @@ +package io.spring.application.profile; + +import io.spring.application.ProfileQueryService; +import io.spring.application.data.ProfileData; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisUserRepository; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({ProfileQueryService.class, MyBatisUserRepository.class}) +public class ProfileQueryServiceTest extends DbTestBase { + @Autowired private ProfileQueryService profileQueryService; + @Autowired private UserRepository userRepository; + + @Test + public void should_fetch_profile_success() { + User currentUser = new User("a@test.com", "a", "123", "", ""); + User profileUser = new User("p@test.com", "p", "123", "", ""); + userRepository.save(profileUser); + + Optional optional = + profileQueryService.findByUsername(profileUser.getUsername(), currentUser); + Assertions.assertTrue(optional.isPresent()); + } +} diff --git a/backend/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java b/backend/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java new file mode 100644 index 0000000..084cae8 --- /dev/null +++ b/backend/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java @@ -0,0 +1,25 @@ +package io.spring.application.tag; + +import io.spring.application.TagsQueryService; +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisItemRepository; +import java.util.Arrays; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({TagsQueryService.class, MyBatisItemRepository.class}) +public class TagsQueryServiceTest extends DbTestBase { + @Autowired private TagsQueryService tagsQueryService; + + @Autowired private ItemRepository itemRepository; + + @Test + public void should_get_all_tags() { + itemRepository.save(new Item("test", "test", "image", Arrays.asList("java"), "123")); + Assertions.assertTrue(tagsQueryService.allTags().contains("java")); + } +} diff --git a/backend/src/test/java/io/spring/core/item/ItemTest.java b/backend/src/test/java/io/spring/core/item/ItemTest.java new file mode 100644 index 0000000..e7cdd5c --- /dev/null +++ b/backend/src/test/java/io/spring/core/item/ItemTest.java @@ -0,0 +1,40 @@ +package io.spring.core.item; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +public class ItemTest { + + @Test + public void should_get_right_slug() { + Item item = new Item("a new title", "desc", "image", Arrays.asList("java"), "123"); + assertThat(item.getSlug(), is("a-new-title")); + } + + @Test + public void should_get_right_slug_with_number_in_title() { + Item item = new Item("a new title 2", "desc", "image", Arrays.asList("java"), "123"); + assertThat(item.getSlug(), is("a-new-title-2")); + } + + @Test + public void should_get_lower_case_slug() { + Item item = new Item("A NEW TITLE", "desc", "image", Arrays.asList("java"), "123"); + assertThat(item.getSlug(), is("a-new-title")); + } + + @Test + public void should_handle_other_language() { + Item item = new Item("中文:标题", "desc", "image", Arrays.asList("java"), "123"); + assertThat(item.getSlug(), is("中文-标题")); + } + + @Test + public void should_handle_commas() { + Item item = new Item("what?the.hell,w", "desc", "image", Arrays.asList("java"), "123"); + assertThat(item.getSlug(), is("what-the-hell-w")); + } +} diff --git a/backend/src/test/java/io/spring/infrastructure/DbTestBase.java b/backend/src/test/java/io/spring/infrastructure/DbTestBase.java new file mode 100644 index 0000000..80ed81c --- /dev/null +++ b/backend/src/test/java/io/spring/infrastructure/DbTestBase.java @@ -0,0 +1,11 @@ +package io.spring.infrastructure; + +import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = Replace.NONE) +@MybatisTest +public abstract class DbTestBase {} diff --git a/backend/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java b/backend/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java new file mode 100644 index 0000000..8cc9a66 --- /dev/null +++ b/backend/src/test/java/io/spring/infrastructure/comment/MyBatisCommentRepositoryTest.java @@ -0,0 +1,26 @@ +package io.spring.infrastructure.comment; + +import io.spring.core.comment.Comment; +import io.spring.core.comment.CommentRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisCommentRepository; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({MyBatisCommentRepository.class}) +public class MyBatisCommentRepositoryTest extends DbTestBase { + @Autowired private CommentRepository commentRepository; + + @Test + public void should_create_and_fetch_comment_success() { + Comment comment = new Comment("content", "123", "456"); + commentRepository.save(comment); + + Optional optional = commentRepository.findById("456", comment.getId()); + Assertions.assertTrue(optional.isPresent()); + Assertions.assertEquals(optional.get(), comment); + } +} diff --git a/backend/src/test/java/io/spring/infrastructure/favorite/MyBatisItemFavoriteRepositoryTest.java b/backend/src/test/java/io/spring/infrastructure/favorite/MyBatisItemFavoriteRepositoryTest.java new file mode 100644 index 0000000..d94e73f --- /dev/null +++ b/backend/src/test/java/io/spring/infrastructure/favorite/MyBatisItemFavoriteRepositoryTest.java @@ -0,0 +1,33 @@ +package io.spring.infrastructure.favorite; + +import io.spring.core.favorite.ItemFavorite; +import io.spring.core.favorite.ItemFavoriteRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisItemFavoriteRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({MyBatisItemFavoriteRepository.class}) +public class MyBatisItemFavoriteRepositoryTest extends DbTestBase { + @Autowired private ItemFavoriteRepository itemFavoriteRepository; + + @Autowired private io.spring.infrastructure.mybatis.mapper.ItemFavoriteMapper itemFavoriteMapper; + + @Test + public void should_save_and_fetch_itemFavorite_success() { + ItemFavorite itemFavorite = new ItemFavorite("123", "456"); + itemFavoriteRepository.save(itemFavorite); + Assertions.assertNotNull( + itemFavoriteMapper.find(itemFavorite.getItemId(), itemFavorite.getUserId())); + } + + @Test + public void should_remove_favorite_success() { + ItemFavorite itemFavorite = new ItemFavorite("123", "456"); + itemFavoriteRepository.save(itemFavorite); + itemFavoriteRepository.remove(itemFavorite); + Assertions.assertFalse(itemFavoriteRepository.find("123", "456").isPresent()); + } +} diff --git a/backend/src/test/java/io/spring/infrastructure/item/ItemRepositoryTransactionTest.java b/backend/src/test/java/io/spring/infrastructure/item/ItemRepositoryTransactionTest.java new file mode 100644 index 0000000..63c441e --- /dev/null +++ b/backend/src/test/java/io/spring/infrastructure/item/ItemRepositoryTransactionTest.java @@ -0,0 +1,40 @@ +package io.spring.infrastructure.item; + +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.mybatis.mapper.ItemMapper; +import java.util.Arrays; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class ItemRepositoryTransactionTest { + @Autowired private ItemRepository itemRepository; + + @Autowired private UserRepository userRepository; + + @Autowired private ItemMapper itemMapper; + + @Test + public void transactional_test() { + User user = new User("aisensiy@gmail.com", "aisensiy", "123", "bio", "default"); + userRepository.save(user); + Item item = new Item("test", "desc", "image", Arrays.asList("java", "spring"), user.getId()); + itemRepository.save(item); + Item anotherItem = + new Item("test", "desc", "image", Arrays.asList("java", "spring", "other"), user.getId()); + try { + itemRepository.save(anotherItem); + } catch (Exception e) { + Assertions.assertNull(itemMapper.findTag("other")); + } + } +} diff --git a/backend/src/test/java/io/spring/infrastructure/item/MyBatisItemRepositoryTest.java b/backend/src/test/java/io/spring/infrastructure/item/MyBatisItemRepositoryTest.java new file mode 100644 index 0000000..8bdc808 --- /dev/null +++ b/backend/src/test/java/io/spring/infrastructure/item/MyBatisItemRepositoryTest.java @@ -0,0 +1,72 @@ +package io.spring.infrastructure.item; + +import io.spring.core.item.Item; +import io.spring.core.item.ItemRepository; +import io.spring.core.item.Tag; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisItemRepository; +import io.spring.infrastructure.repository.MyBatisUserRepository; +import java.util.Arrays; +import java.util.Optional; +import java.util.Random; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import({MyBatisItemRepository.class, MyBatisUserRepository.class}) +public class MyBatisItemRepositoryTest extends DbTestBase { + @Autowired private ItemRepository itemRepository; + + @Autowired private UserRepository userRepository; + + private Item item; + + @BeforeEach + public void setUp() { + Random random = new Random(); + int randomNumber = random.nextInt(); + User user = + new User( + "aisensiy" + randomNumber + "@gmail.com", "aisensiy" + randomNumber, "123", "", ""); + userRepository.save(user); + item = + new Item( + "test-" + randomNumber, "desc", "image", Arrays.asList("java", "spring"), user.getId()); + } + + @Test + public void should_create_and_fetch_item_success() { + itemRepository.save(item); + Optional optional = itemRepository.findById(item.getId()); + Assertions.assertTrue(optional.isPresent()); + Assertions.assertEquals(optional.get(), item); + Assertions.assertTrue(optional.get().getTags().contains(new Tag("java"))); + Assertions.assertTrue(optional.get().getTags().contains(new Tag("spring"))); + } + + @Test + public void should_update_and_fetch_item_success() { + itemRepository.save(item); + + String newTitle = "new test 2"; + item.update(newTitle, "", ""); + itemRepository.save(item); + System.out.println(item.getSlug()); + Optional optional = itemRepository.findBySlug(item.getSlug()); + Assertions.assertTrue(optional.isPresent()); + Item fetched = optional.get(); + Assertions.assertEquals(fetched.getTitle(), newTitle); + } + + @Test + public void should_delete_item() { + itemRepository.save(item); + + itemRepository.remove(item); + Assertions.assertFalse(itemRepository.findById(item.getId()).isPresent()); + } +} diff --git a/backend/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java b/backend/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java new file mode 100644 index 0000000..b226170 --- /dev/null +++ b/backend/src/test/java/io/spring/infrastructure/service/DefaultJwtServiceTest.java @@ -0,0 +1,42 @@ +package io.spring.infrastructure.service; + +import io.spring.core.service.JwtService; +import io.spring.core.user.User; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class DefaultJwtServiceTest { + + private JwtService jwtService; + + @BeforeEach + public void setUp() { + jwtService = + new DefaultJwtService("123123123123123123123123123123123123123123123123123123123123", 3600); + } + + @Test + public void should_generate_and_parse_token() { + User user = new User("email@email.com", "username", "123", "", ""); + String token = jwtService.toToken(user); + Assertions.assertNotNull(token); + Optional optional = jwtService.getSubFromToken(token); + Assertions.assertTrue(optional.isPresent()); + Assertions.assertEquals(optional.get(), user.getId()); + } + + @Test + public void should_get_null_with_wrong_jwt() { + Optional optional = jwtService.getSubFromToken("123"); + Assertions.assertFalse(optional.isPresent()); + } + + @Test + public void should_get_null_with_expired_jwt() { + String token = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhaXNlbnNpeSIsImV4cCI6MTUwMjE2MTIwNH0.SJB-U60WzxLYNomqLo4G3v3LzFxJKuVrIud8D8Lz3-mgpo9pN1i7C8ikU_jQPJGm8HsC1CquGMI-rSuM7j6LDA"; + Assertions.assertFalse(jwtService.getSubFromToken(token).isPresent()); + } +} diff --git a/backend/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java b/backend/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java new file mode 100644 index 0000000..8d4aaa2 --- /dev/null +++ b/backend/src/test/java/io/spring/infrastructure/user/MyBatisUserRepositoryTest.java @@ -0,0 +1,73 @@ +package io.spring.infrastructure.user; + +import io.spring.core.user.FollowRelation; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.DbTestBase; +import io.spring.infrastructure.repository.MyBatisUserRepository; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; + +@Import(MyBatisUserRepository.class) +public class MyBatisUserRepositoryTest extends DbTestBase { + @Autowired private UserRepository userRepository; + private User user; + + @BeforeEach + public void setUp() { + user = new User("aisensiy@16322.com", "aisensiy999", "1234", "", ""); + } + + @Test + public void should_save_and_fetch_user_success() { + userRepository.save(user); + Optional userOptional = userRepository.findByUsername("aisensiy999"); + Assertions.assertEquals(userOptional.get(), user); + Optional userOptional2 = userRepository.findByEmail("aisensiy@16322.com"); + Assertions.assertEquals(userOptional2.get(), user); + } + + @Test + public void should_update_user_success() { + String newEmail = "newemail@email.com"; + user.update(newEmail, "aa", "bb", "cc", "dd"); + userRepository.save(user); + Optional optional = userRepository.findByUsername(user.getUsername()); + Assertions.assertTrue(optional.isPresent()); + Assertions.assertEquals(optional.get().getEmail(), newEmail); + + String newUsername = "newUsername"; + user.update("", newUsername, "", "", ""); + userRepository.save(user); + optional = userRepository.findByEmail(user.getEmail()); + Assertions.assertTrue(optional.isPresent()); + Assertions.assertEquals(optional.get().getUsername(), newUsername); + Assertions.assertEquals(optional.get().getImage(), user.getImage()); + } + + @Test + public void should_create_new_user_follow_success() { + User other = new User("other@example.com", "other", "123", "", ""); + userRepository.save(other); + + FollowRelation followRelation = new FollowRelation(user.getId(), other.getId()); + userRepository.saveRelation(followRelation); + Assertions.assertTrue(userRepository.findRelation(user.getId(), other.getId()).isPresent()); + } + + @Test + public void should_unfollow_user_success() { + User other = new User("other@example.com", "other", "123", "", ""); + userRepository.save(other); + + FollowRelation followRelation = new FollowRelation(user.getId(), other.getId()); + userRepository.saveRelation(followRelation); + + userRepository.removeRelation(followRelation); + Assertions.assertFalse(userRepository.findRelation(user.getId(), other.getId()).isPresent()); + } +} diff --git a/backend/start.sh b/backend/start.sh deleted file mode 100755 index bca7355..0000000 --- a/backend/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -yarn start diff --git a/backend/tests/api-tests.postman.json b/backend/tests/api-tests.postman.json deleted file mode 100644 index fcfaf1a..0000000 --- a/backend/tests/api-tests.postman.json +++ /dev/null @@ -1,1900 +0,0 @@ -{ - "variables": [], - "info": { - "name": "Anythink-Market API Tests", - "_postman_id": "dda3e595-02d7-bf12-2a43-3daea0970192", - "description": "Collection for testing the Anythink-Market API\n\nhttps://github.com/gothinkster/realworld", - "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" - }, - "item": [{ - "name": "Auth", - "description": "", - "item": [{ - "name": "Register", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "if (!(environment.isIntegrationTest)) {", - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", - "", - "var user = responseJSON.user || {};", - "", - "tests['User has \"email\" property'] = user.hasOwnProperty('email');", - "tests['User has \"username\" property'] = user.hasOwnProperty('username');", - "tests['User has \"token\" property'] = user.hasOwnProperty('token');", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/users", - "method": "POST", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\", \"username\":\"johnjacob\"}}" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Login", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", - "", - "var user = responseJSON.user || {};", - "", - "tests['User has \"email\" property'] = user.hasOwnProperty('email');", - "tests['User has \"username\" property'] = user.hasOwnProperty('username');", - "tests['User has \"token\" property'] = user.hasOwnProperty('token');", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/users/login", - "method": "POST", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\"}}" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Login and Remember Token", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", - "", - "var user = responseJSON.user || {};", - "", - "tests['User has \"email\" property'] = user.hasOwnProperty('email');", - "tests['User has \"username\" property'] = user.hasOwnProperty('username');", - "tests['User has \"token\" property'] = user.hasOwnProperty('token');", - "", - "if(tests['User has \"token\" property']){", - " postman.setEnvironmentVariable('token', user.token);", - "}", - "", - "tests['Environment variable \"token\" has been set'] = environment.token === user.token;", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/users/login", - "method": "POST", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"user\":{\"email\":\"john@jacob.com\", \"password\":\"johnnyjacob\"}}" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Current User", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", - "", - "var user = responseJSON.user || {};", - "", - "tests['User has \"email\" property'] = user.hasOwnProperty('email');", - "tests['User has \"username\" property'] = user.hasOwnProperty('username');", - "tests['User has \"token\" property'] = user.hasOwnProperty('token');", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/user", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Update User", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", - "", - "var user = responseJSON.user || {};", - "", - "tests['User has \"email\" property'] = user.hasOwnProperty('email');", - "tests['User has \"username\" property'] = user.hasOwnProperty('username');", - "tests['User has \"token\" property'] = user.hasOwnProperty('token');", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/user", - "method": "PUT", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"user\":{\"email\":\"john@jacob.com\"}}" - }, - "description": "" - }, - "response": [] - } - ] - }, - { - "name": "Items with authentication", - "description": "", - "item": [{ - "name": "Feed", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/feed", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "All Items", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "All Items with auth", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Items by Author", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?seller=johnjacob", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "seller", - "value": "johnjacob" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Items by Author with auth", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?seller=johnjacob", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "seller", - "value": "johnjacob", - "equals": true, - "description": "" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Items Favorited by Username", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - " ", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?favorited=jane", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "favorited", - "value": "jane" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Items Favorited by Username with auth", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - " ", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?favorited=jane", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "favorited", - "value": "jane" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Items by Tag", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?tag=dragons", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "tag", - "value": "dragons" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Create Item", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", - "", - "var item = responseJSON.item || {};", - "", - "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - "if(tests['Item has \"slug\" property']){", - " postman.setEnvironmentVariable('slug', item.slug);", - "}", - "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items", - "method": "POST", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"item\":{\"title\":\"How to train your dragon\", \"description\":\"Ever wonder how?\", \"body\":\"Very carefully.\", \"tagList\":[\"dragons\",\"training\"]}}" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Single Item by slug", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", - "", - "var item = responseJSON.item || {};", - "", - "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/{{slug}}", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Update Item", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "if (!(environment.isIntegrationTest)) {", - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", - "", - "var item = responseJSON.item || {};", - "", - "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/{{slug}}", - "method": "PUT", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"item\":{\"body\":\"With two hands\"}}" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Favorite Item", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", - "", - "var item = responseJSON.item || {};", - "", - "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - "tests[\"Item's 'favorited' property is true\"] = item.favorited === true;", - "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - "tests[\"Item's 'favoritesCount' property is greater than 0\"] = item.favoritesCount > 0;", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/{{slug}}/favorite", - "method": "POST", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Unfavorite Item", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", - "", - "var item = responseJSON.item || {};", - "", - "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - "tests[\"Item's \\\"favorited\\\" property is true\"] = item.favorited === false;", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/{{slug}}/favorite", - "method": "DELETE", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - } - ] - }, - { - "name": "Items", - "description": "", - "item": [{ - "name": "All Items", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Items by Author", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?seller=johnjacob", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "seller", - "value": "johnjacob" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Items Favorited by Username", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - " ", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?favorited=jane", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "favorited", - "value": "jane" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Items by Tag", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"items\" property'] = responseJSON.hasOwnProperty('items');", - " tests['Response contains \"itemsCount\" property'] = responseJSON.hasOwnProperty('itemsCount');", - " tests['itemsCount is an integer'] = Number.isInteger(responseJSON.itemsCount);", - "", - " if(responseJSON.items.length){", - " var item = responseJSON.items[0];", - "", - " tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - " tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - " tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - " tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - " tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - " tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - " tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - " tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - " tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - " tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - " tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - " tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - " tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - " tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - " } else {", - " tests['itemsCount is 0 when feed is empty'] = responseJSON.itemsCount === 0;", - " }", - "}", - "" - ] - } - }], - "request": { - "url": { - "raw": "{{apiUrl}}/items?tag=dragons", - "host": [ - "{{apiUrl}}" - ], - "path": [ - "items" - ], - "query": [{ - "key": "tag", - "value": "dragons" - }], - "variable": [] - }, - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Single Item by slug", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"item\" property'] = responseJSON.hasOwnProperty('item');", - "", - "var item = responseJSON.item || {};", - "", - "tests['Item has \"title\" property'] = item.hasOwnProperty('title');", - "tests['Item has \"slug\" property'] = item.hasOwnProperty('slug');", - "tests['Item has \"body\" property'] = item.hasOwnProperty('body');", - "tests['Item has \"createdAt\" property'] = item.hasOwnProperty('createdAt');", - "tests['Item\\'s \"createdAt\" property is an ISO 8601 timestamp'] = new Date(item.createdAt).toISOString() === item.createdAt;", - "tests['Item has \"updatedAt\" property'] = item.hasOwnProperty('updatedAt');", - "tests['Item\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = new Date(item.updatedAt).toISOString() === item.updatedAt;", - "tests['Item has \"description\" property'] = item.hasOwnProperty('description');", - "tests['Item has \"tagList\" property'] = item.hasOwnProperty('tagList');", - "tests['Item\\'s \"tagList\" property is an Array'] = Array.isArray(item.tagList);", - "tests['Item has \"seller\" property'] = item.hasOwnProperty('seller');", - "tests['Item has \"favorited\" property'] = item.hasOwnProperty('favorited');", - "tests['Item has \"favoritesCount\" property'] = item.hasOwnProperty('favoritesCount');", - "tests['favoritesCount is an integer'] = Number.isInteger(item.favoritesCount);", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/{{slug}}", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - } - ] - }, - { - "name": "Comments", - "description": "", - "item": [{ - "name": "All Comments for Item", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"comments\" property'] = responseJSON.hasOwnProperty('comments');", - "", - " if(responseJSON.comments.length){", - " var comment = responseJSON.comments[0];", - "", - " tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');", - " tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');", - " tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');", - " tests['\"createdAt\" property is an ISO 8601 timestamp'] = new Date(comment.createdAt).toISOString() === comment.createdAt;", - " tests['Comment has \"updatedAt\" property'] = comment.hasOwnProperty('updatedAt');", - " tests['\"updatedAt\" property is an ISO 8601 timestamp'] = new Date(comment.updatedAt).toISOString() === comment.updatedAt;", - " tests['Comment has \"seller\" property'] = comment.hasOwnProperty('seller');", - " }", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/{{slug}}/comments", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Create Comment for Item", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var responseJSON = JSON.parse(responseBody);", - "", - "tests['Response contains \"comment\" property'] = responseJSON.hasOwnProperty('comment');", - "", - "var comment = responseJSON.comment || {};", - "", - "tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');", - "tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');", - "tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');", - "tests['\"createdAt\" property is an ISO 8601 timestamp'] = new Date(comment.createdAt).toISOString() === comment.createdAt;", - "tests['Comment has \"seller\" property'] = comment.hasOwnProperty('seller');", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/items/{{slug}}/comments", - "method": "POST", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"comment\":{\"body\":\"Thank you so much!\"}}" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Delete Comment for Item", - "request": { - "url": "{{apiUrl}}/items/{{slug}}/comments/1", - "method": "DELETE", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - } - ] - }, - { - "name": "Profiles", - "description": "", - "item": [{ - "name": "Profile", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "if (!(environment.isIntegrationTest)) {", - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", - " ", - " var profile = responseJSON.profile || {};", - " ", - " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", - " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", - " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", - "}", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/profiles/johnjacob", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Follow Profile", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "if (!(environment.isIntegrationTest)) {", - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", - " ", - " var profile = responseJSON.profile || {};", - " ", - " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", - " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", - " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", - " tests['Profile\\'s \"following\" property is true'] = profile.following === true;", - "}", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/profiles/johnjacob/follow", - "method": "POST", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "{\"user\":{\"email\":\"john@jacob.com\"}}" - }, - "description": "" - }, - "response": [] - }, - { - "name": "Unfollow Profile", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "if (!(environment.isIntegrationTest)) {", - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - "", - " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", - " ", - " var profile = responseJSON.profile || {};", - " ", - " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", - " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", - " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", - " tests['Profile\\'s \"following\" property is false'] = profile.following === false;", - "}", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/profiles/johnjacob/follow", - "method": "DELETE", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - } - ] - }, - { - "name": "Tags", - "description": "", - "item": [{ - "name": "All Tags", - "event": [{ - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var is200Response = responseCode.code === 200;", - "", - "tests['Response code is 200 OK'] = is200Response;", - "", - "if(is200Response){", - " var responseJSON = JSON.parse(responseBody);", - " ", - " tests['Response contains \"tags\" property'] = responseJSON.hasOwnProperty('tags');", - " tests['\"tags\" property returned as array'] = Array.isArray(responseJSON.tags);", - "}", - "" - ] - } - }], - "request": { - "url": "{{apiUrl}}/tags", - "method": "GET", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }] - }, - { - "name": "Cleanup", - "description": "", - "item": [{ - "name": "Delete Item", - "request": { - "url": "{{apiUrl}}/items/{{slug}}", - "method": "DELETE", - "header": [{ - "key": "Content-Type", - "value": "application/json", - "description": "" - }, - { - "key": "X-Requested-With", - "value": "XMLHttpRequest", - "description": "" - }, - { - "key": "Authorization", - "value": "Token {{token}}", - "description": "" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "description": "" - }, - "response": [] - }] - } - ] -} diff --git a/backend/tests/env-api-tests.postman.json b/backend/tests/env-api-tests.postman.json deleted file mode 100644 index 3ba2ebf..0000000 --- a/backend/tests/env-api-tests.postman.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "4aa60b52-97fc-456d-4d4f-14a350e95dff", - "name": "Anythink-Market API Tests - Environment", - "values": [{ - "enabled": true, - "key": "apiUrl", - "value": "http://localhost:3000/api", - "type": "text" - }], - "timestamp": 1505871382668, - "_postman_variable_scope": "environment", - "_postman_exported_at": "2017-09-20T01:36:34.835Z", - "_postman_exported_using": "Postman/5.2.0" -} diff --git a/backend/yarn.lock b/backend/yarn.lock deleted file mode 100644 index d00f449..0000000 --- a/backend/yarn.lock +++ /dev/null @@ -1,3648 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/bson@*": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.2.0.tgz#a2f71e933ff54b2c3bf267b67fa221e295a33337" - integrity sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg== - dependencies: - bson "*" - -"@types/mongodb@^3.5.27": - version "3.6.20" - resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2" - integrity sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ== - dependencies: - "@types/bson" "*" - "@types/node" "*" - -"@types/node@*": - version "17.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.0.tgz#62797cee3b8b497f6547503b2312254d4fe3c2bb" - integrity sha512-eMhwJXc931Ihh4tkU+Y7GiLzT/y/DBNpNtr4yU9O2w3SYBsr9NaOPhQlLKRmoWtI54uNwuo0IOUFQjVOTZYRvw== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.2.12: - version "1.2.13" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.2.13.tgz#e5f1f3928c6d95fd96558c36ec3d9d0de4a6ecea" - integrity sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo= - dependencies: - mime-types "~2.1.6" - negotiator "0.5.3" - -accepts@~1.3.0: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -ajv@^5.1.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - -ansi-align@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" - integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= - dependencies: - string-width "^2.0.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-uniq@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== - dependencies: - lodash "^4.17.10" - -async@^0.9.0: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - -async@^1.4.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= - -async@^2.0.1: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8= - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" - integrity sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w== - -aws4@^1.2.1, aws4@^1.6.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base64-url@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-1.2.1.tgz#199fd661702a0e7b7dcae6e0698bb089c52f6d78" - integrity sha1-GZ/WYXAqDnt9yubgaYuwicUvbXg= - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -basic-auth@~1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.0.4.tgz#030935b01de7c9b94a824b29f3fccb750d3a5290" - integrity sha1-Awk1sB3nyblKgksp8/zLdQ06UpA= - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" - integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - -bl@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.0.3.tgz#fc5421a28fd4226036c3b3891a66a25bc64d226e" - integrity sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4= - dependencies: - readable-stream "~2.0.5" - -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== - -bluebird@^2.6.2: - version "2.11.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" - integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= - -body-parser@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.0.tgz#8168abaeaf9e77e300f7b3aef4df4b46e9b21b35" - integrity sha1-gWirrq+ed+MA97Ou9N9LRumyGzU= - dependencies: - bytes "2.2.0" - content-type "~1.0.1" - debug "~2.2.0" - depd "~1.1.0" - http-errors "~1.4.0" - iconv-lite "0.4.13" - on-finished "~2.3.0" - qs "6.1.0" - raw-body "~2.1.5" - type-is "~1.6.11" - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= - dependencies: - hoek "2.x.x" - -boom@4.x.x: - version "4.3.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" - integrity sha1-T4owBctKfjiJ90kDD9JbluAdLjE= - dependencies: - hoek "4.x.x" - -boom@5.x.x: - version "5.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" - integrity sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw== - dependencies: - hoek "4.x.x" - -boxen@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" - integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== - dependencies: - ansi-align "^2.0.0" - camelcase "^4.0.0" - chalk "^2.0.1" - cli-boxes "^1.0.0" - string-width "^2.0.0" - term-size "^1.2.0" - widest-line "^2.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -bson@*: - version "4.6.0" - resolved "https://registry.yarnpkg.com/bson/-/bson-4.6.0.tgz#15c3b39ba3940c3d915a0c44d51459f4b4fbf1b2" - integrity sha512-8jw1NU1hglS+Da1jDOUYuNcBJ4cNHCFIqzlwoFNnsTOg2R/ox0aTYcTiBN4dzRa9q7Cvy6XErh3L8ReTEb9AQQ== - dependencies: - buffer "^5.6.0" - -bson@^1.1.4: - version "1.1.6" - resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" - integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== - -btoa@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" - integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - -buffer@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -bytes@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588" - integrity sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg= - -bytes@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" - integrity sha1-fZcZb51br39pNeJZhVSe3SpsIzk= - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= - -camelcase@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -capture-stack-trace@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" - integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== - -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" - integrity sha1-cVuW6phBWTzDMGeSP17GDr2k99c= - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@^1.1.0, chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -charset@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/charset/-/charset-1.0.1.tgz#8d59546c355be61049a8fa9164747793319852bd" - integrity sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg== - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -ci-info@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - -circular-json@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" - integrity sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0= - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cli-boxes@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" - integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= - -cli-progress@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-1.8.0.tgz#5e8afc310f2058fbe33e9006e31c71c1c3b5da7f" - integrity sha1-Xor8MQ8gWPvjPpAG4xxxwcO12n8= - dependencies: - colors "^1.1.2" - -cli-table3@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.4.0.tgz#a7fd50f011d734e3f16403cfcbedbea97659e417" - integrity sha512-o0slI6EFJNI2aKE9jG1bVN6jXJG2vjzYsGhyd9RqRV/YiiEmzSwNNXb5qJmfLDSOdvfA6sUvdKVvi3p3Y1apxA== - dependencies: - kind-of "^3.0.4" - object-assign "^4.1.0" - string-width "^1.0.1" - optionalDependencies: - colors "^1.1.2" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -colors@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.0.tgz#5f20c9fef6945cb1134260aab33bfbdc8295e04e" - integrity sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw== - -colors@^1.1.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@~1.0.5: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" - integrity sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew== - -commander@^2.9.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -configstore@^3.0.0: - version "3.1.5" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.5.tgz#e9af331fadc14dabd544d3e7e76dc446a09a530f" - integrity sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA== - dependencies: - dot-prop "^4.2.1" - graceful-fs "^4.1.2" - make-dir "^1.0.0" - unique-string "^1.0.0" - write-file-atomic "^2.0.0" - xdg-basedir "^3.0.0" - -content-disposition@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" - integrity sha1-h0dsamfI2qh+Muh2Ft+IO6f7Bxs= - -content-type@~1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.1.5.tgz#6ab9948a4b1ae21952cd2588530a4722d4044d7c" - integrity sha1-armUiksa4hlSzSWIUwpHItQETXw= - -cookie@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.2.3.tgz#1a59536af68537a21178a01346f87cb059d2ae5c" - integrity sha1-GllTavaFN6IReKATRvh8sFnSrlw= - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cors@2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.7.1.tgz#3c2e50a58af9ef8c89bee21226b099be1f02739b" - integrity sha1-PC5QpYr574yJvuISJrCZvh8Cc5s= - dependencies: - vary "^1" - -crc@3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.0.tgz#4258e351613a74ef1153dfcb05e820c3e9715d7f" - integrity sha1-QljjUWE6dO8RU9/LBeggw+lxXX8= - -create-error-class@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" - integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= - dependencies: - capture-stack-trace "^1.0.0" - -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= - dependencies: - boom "2.x.x" - -cryptiles@3.x.x: - version "3.1.4" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.4.tgz#769a68c95612b56faadfcebf57ac86479cbe8322" - integrity sha512-8I1sgZHfVwcSOY6mSGpVU3lw/GSIZvusg8dD2+OGehCJpOhQRLNcH0qb9upQnOH4XhgxxFJSg6E2kx95deb1Tw== - dependencies: - boom "5.x.x" - -crypto-js@3.1.9-1: - version "3.1.9-1" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8" - integrity sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg= - -crypto-random-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" - integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= - -csv-parse@1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-1.3.3.tgz#d1cfd8743c2f849a0abb2fd544db56695d19a490" - integrity sha1-0c/YdDwvhJoKuy/VRNtWaV0ZpJA= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -dbug@~0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/dbug/-/dbug-0.4.2.tgz#32b4b3105e8861043a6f9ac755d80e542d365b31" - integrity sha1-MrSzEF6IYQQ6b5rHVdgOVC02WzE= - -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.2.6: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= - dependencies: - ms "0.7.1" - -decamelize@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -denque@^1.4.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== - -depd@~1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -domelementtype@1, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -dot-prop@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" - integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ== - dependencies: - is-obj "^1.0.0" - -dotenv@^8.2.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -ejs@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.4.1.tgz#82e15b1b2a1f948b18097476ba2bd7c66f4d1566" - integrity sha1-guFbGyoflIsYCXR2uivXxm9NFWY= - -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -errorhandler@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.4.3.tgz#b7b70ed8f359e9db88092f2d20c0f831420ad83f" - integrity sha1-t7cO2PNZ6duICS8tIMD4MUIK2D8= - dependencies: - accepts "~1.3.0" - escape-html "~1.0.3" - -escape-html@1.0.3, escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -etag@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" - integrity sha1-A9MLX2fdbmMtKUXTDWZScxo01dg= - -eventemitter3@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" - integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== - -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -express-async-handler@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/express-async-handler/-/express-async-handler-1.2.0.tgz#ffc9896061d90f8d2e71a2d2b8668db5b0934391" - integrity sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w== - -express-jwt@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/express-jwt/-/express-jwt-3.3.0.tgz#d10e17244225b1968d20137ff77fc7488c88f494" - integrity sha1-0Q4XJEIlsZaNIBN/93/HSIyI9JQ= - dependencies: - async "^0.9.0" - express-unless "^0.3.0" - jsonwebtoken "^5.0.0" - lodash "~3.10.1" - -express-session@1.13.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.13.0.tgz#8ac3b5c0188b48382851d88207b8e7746efb4011" - integrity sha1-isO1wBiLSDgoUdiCB7jndG77QBE= - dependencies: - cookie "0.2.3" - cookie-signature "1.0.6" - crc "3.4.0" - debug "~2.2.0" - depd "~1.1.0" - on-headers "~1.0.1" - parseurl "~1.3.0" - uid-safe "~2.0.0" - utils-merge "1.0.0" - -express-unless@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-0.3.1.tgz#2557c146e75beb903e2d247f9b5ba01452696e20" - integrity sha1-JVfBRudb65A+LSR/m1ugFFJpbiA= - -express@4.13.4: - version "4.13.4" - resolved "https://registry.yarnpkg.com/express/-/express-4.13.4.tgz#3c0b76f3c77590c8345739061ec0bd3ba067ec24" - integrity sha1-PAt288d1kMg0VzkGHsC9O6Bn7CQ= - dependencies: - accepts "~1.2.12" - array-flatten "1.1.1" - content-disposition "0.5.1" - content-type "~1.0.1" - cookie "0.1.5" - cookie-signature "1.0.6" - debug "~2.2.0" - depd "~1.1.0" - escape-html "~1.0.3" - etag "~1.7.0" - finalhandler "0.4.1" - fresh "0.3.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.1" - path-to-regexp "0.1.7" - proxy-addr "~1.0.10" - qs "4.0.0" - range-parser "~1.0.3" - send "0.13.1" - serve-static "~1.10.2" - type-is "~1.6.6" - utils-merge "1.0.0" - vary "~1.0.1" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.0, extend@~3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -file-type@3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" - integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -filesize@3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" - integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -finalhandler@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.4.1.tgz#85a17c6c59a94717d262d61230d4b0ebe3d4a14d" - integrity sha1-haF8bFmpRxfSYtYSMNSw6+PUoU0= - dependencies: - debug "~2.2.0" - escape-html "~1.0.3" - on-finished "~2.3.0" - unpipe "~1.0.0" - -follow-redirects@^1.14.7: - version "1.14.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~1.0.0-rc3: - version "1.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" - integrity sha1-rjFduaSQf6BlUCMEpm13M0de43w= - dependencies: - async "^2.0.1" - combined-stream "^1.0.5" - mime-types "^2.1.11" - -form-data@~2.3.1: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@~0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" - integrity sha1-ZR+DjiJCTnVm3hYdg1jKoZn4PU8= - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -generate-function@^2.0.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" - integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== - dependencies: - is-property "^1.0.2" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= - dependencies: - is-property "^1.0.0" - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-dirs@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= - dependencies: - ini "^1.3.4" - -got@^6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" - integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= - dependencies: - create-error-class "^3.0.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - is-redirect "^1.0.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - safe-buffer "^5.0.1" - timed-out "^4.0.0" - unzip-response "^2.0.1" - url-parse-lax "^1.0.0" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -handlebars@4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" - integrity sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw= - dependencies: - async "^1.4.0" - optimist "^0.6.1" - source-map "^0.4.4" - optionalDependencies: - uglify-js "^2.6" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" - integrity sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0= - dependencies: - chalk "^1.1.1" - commander "^2.9.0" - is-my-json-valid "^2.12.4" - pinkie-promise "^2.0.0" - -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - integrity sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0= - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hawk@6.0.2, hawk@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" - integrity sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ== - dependencies: - boom "4.x.x" - cryptiles "3.x.x" - hoek "4.x.x" - sntp "2.x.x" - -hawk@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= - -hoek@4.x.x: - version "4.2.1" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" - integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== - -htmlparser2@^3.9.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -http-errors@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942" - integrity sha1-GX4izevUGYWF6GlO9nhhl7ke2UI= - dependencies: - inherits "~2.0.1" - statuses "1" - -http-errors@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf" - integrity sha1-bAJC3qaz33r9oVPHEImzHG6Cqr8= - dependencies: - inherits "2.0.1" - statuses ">= 1.2.1 < 2" - -http-reasons@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/http-reasons/-/http-reasons-0.1.0.tgz#a953ca670078669dde142ce899401b9d6e85d3b4" - integrity sha1-qVPKZwB4Zp3eFCzomUAbnW6F07Q= - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8= - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -httpntlm@1.7.6: - version "1.7.6" - resolved "https://registry.yarnpkg.com/httpntlm/-/httpntlm-1.7.6.tgz#6991e8352836007d67101b83db8ed0f915f906d0" - integrity sha1-aZHoNSg2AH1nEBuD247Q+RX5BtA= - dependencies: - httpreq ">=0.4.22" - underscore "~1.7.0" - -httpreq@>=0.4.22: - version "0.5.2" - resolved "https://registry.yarnpkg.com/httpreq/-/httpreq-0.5.2.tgz#be6777292fa1038d7771d7c01d9a5e1219de951c" - integrity sha512-2Jm+x9WkExDOeFRrdBCBSpLPT5SokTcRHkunV3pjKmX/cx6av8zQ0WtHUMDrYb6O4hBFzNU6sxJEypvRUVYKnw== - -iconv-lite@0.4.13: - version "0.4.13" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" - integrity sha1-H4irpKsLFQjoMSrMOTRfNumS4vI= - -iconv-lite@0.4.22: - version "0.4.22" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.22.tgz#c6b16b9d05bc6c307dc9303a820412995d2eea95" - integrity sha512-1AinFBeDTnsvVEP+V1QBlHpM1UZZl7gWB6fcz7B1Ho+LI1dUh2sSrxoCfVt2PinRHzXAziSniEV3P7JbTDHcXA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.4, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -intel@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/intel/-/intel-1.2.0.tgz#11d1147eb6b3f4582bdf5337b37d541584e9e41e" - integrity sha1-EdEUfraz9Fgr31M3s31UFYTp5B4= - dependencies: - chalk "^1.1.0" - dbug "~0.4.2" - stack-trace "~0.0.9" - strftime "~0.10.0" - symbol "~0.3.1" - utcstring "~0.1.0" - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -ipaddr.js@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.0.5.tgz#5fa78cf301b825c78abc3042d812723049ea23c7" - integrity sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c= - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-ci@^1.0.10: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== - dependencies: - ci-info "^1.5.0" - -is-core-module@^2.2.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" - integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" - integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= - dependencies: - global-dirs "^0.1.0" - is-path-inside "^1.0.0" - -is-my-ip-valid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" - integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== - -is-my-json-valid@^2.12.4: - version "2.20.6" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz#a9d89e56a36493c77bda1440d69ae0dc46a08387" - integrity sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw== - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - is-my-ip-valid "^1.0.0" - jsonpointer "^5.0.0" - xtend "^4.0.0" - -is-npm@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" - integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-property@^1.0.0, is-property@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= - -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" - integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= - -is-retry-allowed@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - -is-stream@^1.0.0, is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isemail@1.x.x: - version "1.2.0" - resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" - integrity sha1-vgPfjMPineTSxd9lASY/H6RZXpo= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -joi@^6.10.1: - version "6.10.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" - integrity sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY= - dependencies: - hoek "2.x.x" - isemail "1.x.x" - moment "2.x.x" - topo "1.x.x" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= - -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -jsonpointer@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" - integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== - -jsonwebtoken@7.1.9: - version "7.1.9" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.1.9.tgz#847804e5258bec5a9499a8dc4a5e7a3bae08d58a" - integrity sha1-hHgE5SWL7FqUmajcSl56O64I1Yo= - dependencies: - joi "^6.10.1" - jws "^3.1.3" - lodash.once "^4.0.0" - ms "^0.7.1" - xtend "^4.0.1" - -jsonwebtoken@^5.0.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-5.7.0.tgz#1c90f9a86ce5b748f5f979c12b70402b4afcddb4" - integrity sha1-HJD5qGzlt0j1+XnBK3BAK0r83bQ= - dependencies: - jws "^3.0.0" - ms "^0.7.1" - xtend "^4.0.1" - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.0.0, jws@^3.1.3: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -kareem@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93" - integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.0.4, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -latest-version@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" - integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU= - dependencies: - package-json "^4.0.0" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= - -liquid-json@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/liquid-json/-/liquid-json-0.3.1.tgz#9155a18136d8a6b2615e5f16f9a2448ab6b50eea" - integrity sha1-kVWhgTbYprJhXl8W+aJEira1Duo= - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.escaperegexp@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" - integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= - -lodash.foreach@^4.1.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" - integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= - -lodash.get@^4.0.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.mergewith@^4.6.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - -lodash@4.17.10: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" - integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== - -lodash@4.17.9: - version "4.17.9" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.9.tgz#9c056579af0bdbb4322e23c836df13ef2b271cb7" - integrity sha512-vuRLquvot5sKUldMBumG0YqLvX6m/RGBBOmqb3CWR/MC/QvvD1cTH1fOqxz2FJAQeoExeUdX5Gu9vP2EP6ik+Q== - -lodash@^4.17.10, lodash@^4.17.14: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -lodash@~3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= - -lowercase-keys@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -marked@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66" - integrity sha512-tMsdNBgOsrUophCAFQl0XPe6Zqk/uy9gnue+jIIKhykO51hxyu6uNx7zBPy0+y/WKYVZZMspV9YeXLNdKk+iYw== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -memory-pager@^1.0.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" - integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -method-override@2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/method-override/-/method-override-2.3.5.tgz#2cd5cdbff00c3673d7ae345119a812a5d95b8c8e" - integrity sha1-LNXNv/AMNnPXrjRRGagSpdlbjI4= - dependencies: - debug "~2.2.0" - methods "~1.1.1" - parseurl "~1.3.0" - vary "~1.0.1" - -methods@1.1.2, methods@~1.1.1, methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -mime-db@1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-format@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mime-format/-/mime-format-2.0.0.tgz#e29f8891e284d78270246f0050d6834bdbbe1332" - integrity sha1-4p+IkeKE14JwJG8AUNaDS9u+EzI= - dependencies: - charset "^1.0.0" - -mime-types@2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.6, mime-types@~2.1.7: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== - dependencies: - mime-db "1.51.0" - -mime@1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" - integrity sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM= - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -moment@2.x.x: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - -mongodb@3.6.6: - version "3.6.6" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.6.tgz#92e3658f45424c34add3003e3046c1535c534449" - integrity sha512-WlirMiuV1UPbej5JeCMqE93JRfZ/ZzqE7nJTwP85XzjAF4rRSeq2bGCb1cjfoHLOF06+HxADaPGqT0g3SbVT1w== - dependencies: - bl "^2.2.1" - bson "^1.1.4" - denque "^1.4.1" - optional-require "^1.0.2" - safe-buffer "^5.1.2" - optionalDependencies: - saslprep "^1.0.0" - -mongoose-legacy-pluralize@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" - integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== - -mongoose-unique-validator@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mongoose-unique-validator/-/mongoose-unique-validator-3.1.0.tgz#10d6fa10ccf5515461e3b5693f193d227546d60b" - integrity sha512-UsBBlFapip8gc8x1h+nLWnkOy+GTy9Z+zmTyZ35icLV3EoLIVz180vJzepfMM9yBy2AJh+maeuoM8CWtqejGUg== - dependencies: - lodash.foreach "^4.1.0" - lodash.get "^4.0.2" - lodash.merge "^4.6.2" - -mongoose@5.12.5: - version "5.12.5" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.12.5.tgz#70d11d3e68a3aeeb6960262633e1ba80cb620385" - integrity sha512-VVoqiELZcoI2HhHDuPpfN3qmExrtIeXSWNb1nihf4w1SJoWGXilU/g2cQgeeSMc2vAHSZd5Nv2sNPvbZHFw+pg== - dependencies: - "@types/mongodb" "^3.5.27" - bson "^1.1.4" - kareem "2.3.2" - mongodb "3.6.6" - mongoose-legacy-pluralize "1.0.2" - mpath "0.8.3" - mquery "3.2.5" - ms "2.1.2" - regexp-clone "1.0.0" - safe-buffer "5.2.1" - sift "7.0.1" - sliced "1.0.1" - -morgan@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.7.0.tgz#eb10ca8e50d1abe0f8d3dad5c0201d052d981c62" - integrity sha1-6xDKjlDRq+D409rVwCAdBS2YHGI= - dependencies: - basic-auth "~1.0.3" - debug "~2.2.0" - depd "~1.1.0" - on-finished "~2.3.0" - on-headers "~1.0.1" - -mpath@0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.8.3.tgz#828ac0d187f7f42674839d74921970979abbdd8f" - integrity sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA== - -mquery@3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.5.tgz#8f2305632e4bb197f68f60c0cffa21aaf4060c51" - integrity sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A== - dependencies: - bluebird "3.5.1" - debug "3.1.0" - regexp-clone "^1.0.0" - safe-buffer "5.1.2" - sliced "1.0.1" - -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - integrity sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg= - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^0.7.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.3.tgz#708155a5e44e33f5fd0fc53e81d0d40a91be1fff" - integrity sha1-cIFVpeROM/X9D8U+gdDUCpG+H/8= - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -nan@^2.12.1: - version "2.15.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -negotiator@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.5.3.tgz#269d5c476810ec92edbe7b6c2f28316384f9a7e8" - integrity sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g= - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -newman@^3.8.2: - version "3.10.0" - resolved "https://registry.yarnpkg.com/newman/-/newman-3.10.0.tgz#24bb43963e25bb79a4fc158cd76bf20eaa179f06" - integrity sha512-8dr3kUedx/D4a/tiysvEjEQ+D+lLA/sgPASN33AiRyTKtdqzeVFuuBZYb3Jb+0TBd84Y3Qk8t24GuTY22HJN4g== - dependencies: - async "2.6.1" - cli-progress "1.8.0" - cli-table3 "0.4.0" - colors "1.3.0" - commander "2.16.0" - csv-parse "1.3.3" - eventemitter3 "3.1.0" - filesize "3.6.1" - handlebars "4.0.11" - lodash "4.17.9" - mkdirp "0.5.1" - parse-json "4.0.0" - postman-collection "3.1.1" - postman-collection-transformer "2.5.10" - postman-request "2.86.1-postman.1" - postman-runtime "7.2.0" - pretty-ms "3.2.0" - semver "5.5.0" - serialised-error "1.1.3" - shelljs "0.8.2" - word-wrap "1.2.3" - xmlbuilder "10.0.0" - -node-oauth1@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/node-oauth1/-/node-oauth1-1.2.2.tgz#fffb2813a88c2770711332ad0e5487b4927644a4" - integrity sha512-f2XC7Y68wJq6+s+LJn/yUq5Gqg9Y9zwIz2zY6vUyS8xzawnSWhXKOMJepLwvptjPl8IjVxtWh7iI9dbdKGSw4g== - dependencies: - crypto-js "3.1.9-1" - -node-uuid@~1.4.7: - version "1.4.8" - resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" - integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= - -nodemon@^1.11.0: - version "1.19.4" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.4.tgz#56db5c607408e0fdf8920d2b444819af1aae0971" - integrity sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ== - dependencies: - chokidar "^2.1.8" - debug "^3.2.6" - ignore-by-default "^1.0.1" - minimatch "^3.0.4" - pstree.remy "^1.1.7" - semver "^5.7.1" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.2" - update-notifier "^2.5.0" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -oauth-sign@~0.8.0, oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-hash@^1.1.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" - integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optional-require@^1.0.2: - version "1.1.8" - resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.1.8.tgz#16364d76261b75d964c482b2406cb824d8ec44b7" - integrity sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA== - dependencies: - require-at "^1.0.6" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -package-json@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" - integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0= - dependencies: - got "^6.7.1" - registry-auth-token "^3.0.1" - registry-url "^3.0.3" - semver "^5.1.0" - -parse-json@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-ms@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-1.0.1.tgz#56346d4749d78f23430ca0c713850aef91aa361d" - integrity sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0= - -parseurl@~1.3.0, parseurl@~1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -passport-local@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" - integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= - dependencies: - passport-strategy "1.x.x" - -passport-strategy@1.x.x: - version "1.0.0" - resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" - integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= - -passport@0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/passport/-/passport-0.3.2.tgz#9dd009f915e8fe095b0124a01b8f82da07510102" - integrity sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI= - dependencies: - passport-strategy "1.x.x" - pause "0.0.1" - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -pause@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" - integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss@^6.0.14: - version "6.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postman-collection-transformer@2.5.10: - version "2.5.10" - resolved "https://registry.yarnpkg.com/postman-collection-transformer/-/postman-collection-transformer-2.5.10.tgz#cecf07b7cdac58b09d7a3e7eae0af3e47c6f7cc4" - integrity sha512-2Pm0Z6v9IfqYhZciYW9i3ZUqOkLIf/AO2Ll389G0LlHJ/qg82sFhL0V4wUI1JQE6nd4eLBiUwhdPEPlHPQIWjQ== - dependencies: - commander "2.16.0" - inherits "2.0.3" - intel "1.2.0" - lodash "4.17.10" - semver "5.5.0" - strip-json-comments "2.0.1" - -postman-collection@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-3.1.1.tgz#9042c1e7891f3f319566fd05f6f2aeeb51bc8d45" - integrity sha512-0Q9BpVVdquv4Wf/Kpvf8LgLADsnZW8g4lGouBncD2pn+mHzL72oWJmD9/kV56wp4SuQl0a1OZNuUYkK9fYPxOA== - dependencies: - escape-html "1.0.3" - file-type "3.9.0" - http-reasons "0.1.0" - iconv-lite "0.4.22" - liquid-json "0.3.1" - lodash "4.17.10" - marked "0.4.0" - mime-format "2.0.0" - mime-types "2.1.18" - postman-url-encoder "1.0.1" - sanitize-html "1.18.2" - semver "5.5.0" - uuid "3.3.2" - -postman-request@2.86.1-postman.1: - version "2.86.1-postman.1" - resolved "https://registry.yarnpkg.com/postman-request/-/postman-request-2.86.1-postman.1.tgz#bc43b753771e8fdcbad95f1436881f81e6c5bef2" - integrity sha512-HzzRbCLcOItaFhhvYiv0/LWShEZ4Lir8ZCL2OiQ8pkpirKM9u7BUQ4OgqNzTExt3m8NWg60f19eQ0hk1cNphLg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - hawk "~6.0.2" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - postman-url-encoder "1.0.1" - qs "~6.5.1" - safe-buffer "^5.1.1" - stream-length "^1.0.2" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - -postman-runtime@7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/postman-runtime/-/postman-runtime-7.2.0.tgz#9d7796fd6981826b8abb887a02370059a02a04e2" - integrity sha512-penzRSjXckHeGXMP6NxvJVLbhxDa47Uei8RIbzf4gEV+1qTZ5qp9QppW2yWPNb5SSW1Z113t6LGKlpVR+plZMQ== - dependencies: - async "2.6.1" - aws4 "1.7.0" - btoa "1.2.1" - crypto-js "3.1.9-1" - eventemitter3 "3.1.0" - hawk "6.0.2" - http-reasons "0.1.0" - httpntlm "1.7.6" - inherits "2.0.3" - lodash "4.17.10" - node-oauth1 "1.2.2" - postman-collection "3.1.1" - postman-request "2.86.1-postman.1" - postman-sandbox "3.1.1" - resolve-from "4.0.0" - serialised-error "1.1.3" - uuid "3.3.2" - -postman-sandbox@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/postman-sandbox/-/postman-sandbox-3.1.1.tgz#31ed0a97e9a2c803166a2080fe879a3377470e0f" - integrity sha512-bch46g1LfPnCeCTYQXKlYDmrnTljAPS74a12z5XCS2lJ4veIitX8y4b+mBZSxzMZ05tIZrUTDv+XoyZbRlpagw== - dependencies: - inherits "2.0.3" - lodash "4.17.10" - uuid "3.3.2" - uvm "1.7.3" - -postman-url-encoder@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/postman-url-encoder/-/postman-url-encoder-1.0.1.tgz#a094a42e9415ff0bbfdce0eaa8e6011d449ee83c" - integrity sha1-oJSkLpQV/wu/3ODqqOYBHUSe6Dw= - -prepend-http@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= - -pretty-ms@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-3.2.0.tgz#87a8feaf27fc18414d75441467d411d6e6098a25" - integrity sha512-ZypexbfVUGTFxb0v+m1bUyy92DHe5SyYlnyY0msyms5zd3RwyvNgyxZZsXXgoyzlxjx5MiqtXUdhUfvQbe0A2Q== - dependencies: - parse-ms "^1.0.0" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -proxy-addr@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.0.10.tgz#0d40a82f801fc355567d2ecb65efe3f077f121c5" - integrity sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU= - dependencies: - forwarded "~0.1.0" - ipaddr.js "1.0.5" - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -pstree.remy@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -qs@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607" - integrity sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc= - -qs@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.1.0.tgz#ec1d1626b24278d99f0fdf4549e524e24eceeb26" - integrity sha1-7B0WJrJCeNmfD99FSeUk4k7O6yY= - -qs@~6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.0.4.tgz#51019d84720c939b82737e84556a782338ecea7b" - integrity sha1-UQGdhHIMk5uCc36EVWp4Izjs6ns= - -qs@~6.5.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -range-parser@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.0.3.tgz#6872823535c692e2c2a0103826afd82c2e0ff175" - integrity sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU= - -raw-body@~2.1.5: - version "2.1.7" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" - integrity sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q= - dependencies: - bytes "2.4.0" - iconv-lite "0.4.13" - unpipe "1.0.0" - -rc@^1.0.1, rc@^1.1.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^2.0.2, readable-stream@^2.3.5: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" - integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= - dependencies: - resolve "^1.1.6" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp-clone@1.0.0, regexp-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" - integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== - -registry-auth-token@^3.0.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" - integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== - dependencies: - rc "^1.1.6" - safe-buffer "^5.0.1" - -registry-url@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" - integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= - dependencies: - rc "^1.0.1" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -request@2.69.0: - version "2.69.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.69.0.tgz#cf91d2e000752b1217155c005241911991a2346a" - integrity sha1-z5HS4AB1KxIXFVwAUkGRGZGiNGo= - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - bl "~1.0.0" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~1.0.0-rc3" - har-validator "~2.0.6" - hawk "~3.1.0" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - node-uuid "~1.4.7" - oauth-sign "~0.8.0" - qs "~6.0.2" - stringstream "~0.0.4" - tough-cookie "~2.2.0" - tunnel-agent "~0.4.1" - -require-at@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a" - integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g== - -resolve-from@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@^1.1.6: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - dependencies: - align-text "^0.1.1" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sanitize-html@1.18.2: - version "1.18.2" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.18.2.tgz#61877ba5a910327e42880a28803c2fbafa8e4642" - integrity sha512-52ThA+Z7h6BnvpSVbURwChl10XZrps5q7ytjTwWcIe9bmJwnVP6cpEVK2NvDOUhGupoqAvNbUz3cpnJDp4+/pg== - dependencies: - chalk "^2.3.0" - htmlparser2 "^3.9.0" - lodash.clonedeep "^4.5.0" - lodash.escaperegexp "^4.1.2" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.mergewith "^4.6.0" - postcss "^6.0.14" - srcset "^1.0.0" - xtend "^4.0.0" - -saslprep@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" - integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== - dependencies: - sparse-bitfield "^3.0.3" - -semver-diff@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" - integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= - dependencies: - semver "^5.0.3" - -semver@5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== - -semver@^5.0.3, semver@^5.1.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -send@0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.13.1.tgz#a30d5f4c82c8a9bae9ad00a1d9b1bdbe6f199ed7" - integrity sha1-ow1fTILIqbrprQCh2bG9vm8Zntc= - dependencies: - debug "~2.2.0" - depd "~1.1.0" - destroy "~1.0.4" - escape-html "~1.0.3" - etag "~1.7.0" - fresh "0.3.0" - http-errors "~1.3.1" - mime "1.3.4" - ms "0.7.1" - on-finished "~2.3.0" - range-parser "~1.0.3" - statuses "~1.2.1" - -send@0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.13.2.tgz#765e7607c8055452bba6f0b052595350986036de" - integrity sha1-dl52B8gFVFK7pvCwUllTUJhgNt4= - dependencies: - debug "~2.2.0" - depd "~1.1.0" - destroy "~1.0.4" - escape-html "~1.0.3" - etag "~1.7.0" - fresh "0.3.0" - http-errors "~1.3.1" - mime "1.3.4" - ms "0.7.1" - on-finished "~2.3.0" - range-parser "~1.0.3" - statuses "~1.2.1" - -serialised-error@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/serialised-error/-/serialised-error-1.1.3.tgz#8a4c466b29c26ff11016eaf1b5fa2b87ca4cd8b5" - integrity sha512-vybp3GItaR1ZtO2nxZZo8eOo7fnVaNtP3XE2vJKgzkKR2bagCkdJ1EpYYhEMd3qu/80DwQk9KjsNSxE3fXWq0g== - dependencies: - object-hash "^1.1.2" - stack-trace "0.0.9" - uuid "^3.0.0" - -serve-static@~1.10.2: - version "1.10.3" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.10.3.tgz#ce5a6ecd3101fed5ec09827dac22a9c29bfb0535" - integrity sha1-zlpuzTEB/tXsCYJ9rCKpwpv7BTU= - dependencies: - escape-html "~1.0.3" - parseurl "~1.3.1" - send "0.13.2" - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shelljs@0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" - integrity sha512-pRXeNrCA2Wd9itwhvLp5LZQvPJ0wU6bcjaTMywHHGX5XWhVN2nzSu7WV0q+oUY7mGK3mgSkDDzP3MgjqdyIgbQ== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -sift@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" - integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.6" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" - integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== - -sliced@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" - integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= - -slug@0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.1.tgz#af08f608a7c11516b61778aa800dce84c518cfda" - integrity sha1-rwj2CKfBFRa2F3iqgA3OhMUYz9o= - dependencies: - unicode ">= 0.3.1" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= - dependencies: - hoek "2.x.x" - -sntp@2.x.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" - integrity sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg== - dependencies: - hoek "4.x.x" - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -source-map@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.6, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sparse-bitfield@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" - integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= - dependencies: - memory-pager "^1.0.2" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -srcset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" - integrity sha1-pWad4StC87HV6D7QPHEEb8SPQe8= - dependencies: - array-uniq "^1.0.2" - number-is-nan "^1.0.0" - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -stack-trace@0.0.9: - version "0.0.9" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" - integrity sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU= - -stack-trace@~0.0.9: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -statuses@1, "statuses@>= 1.2.1 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -statuses@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.2.1.tgz#dded45cc18256d51ed40aec142489d5c61026d28" - integrity sha1-3e1FzBglbVHtQK7BQkidXGECbSg= - -stream-length@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stream-length/-/stream-length-1.0.2.tgz#8277f3cbee49a4daabcfdb4e2f4a9b5e9f2c9f00" - integrity sha1-gnfzy+5JpNqrz9tOL0qbXp8snwA= - dependencies: - bluebird "^2.6.2" - -strftime@~0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/strftime/-/strftime-0.10.1.tgz#108af1176a7d5252cfbddbdb2af044dfae538389" - integrity sha512-nVvH6JG8KlXFPC0f8lojLgEsPA18lRpLZ+RrJh/NkQV2tqOgZfbas8gcU8SFgnnqR3rWzZPYu6N2A3xzs/8rQg== - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.0.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringstream@~0.0.4: - version "0.0.6" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" - integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA== - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-json-comments@2.0.1, strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -symbol@~0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/symbol/-/symbol-0.3.1.tgz#b6f9a900d496a57f02408f22198c109dda063041" - integrity sha1-tvmpANSWpX8CQI8iGYwQndoGMEE= - -term-size@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" - integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= - dependencies: - execa "^0.7.0" - -timed-out@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -topo@1.x.x: - version "1.1.0" - resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" - integrity sha1-6ddRYV0buH3IZdsYL6HKCl71NtU= - dependencies: - hoek "2.x.x" - -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - -tough-cookie@~2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.2.2.tgz#c83a1830f4e5ef0b93ef2a3488e724f8de016ac7" - integrity sha1-yDoYMPTl7wuT7yo0iOck+N4Basc= - -tough-cookie@~2.3.3: - version "2.3.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" - integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA== - dependencies: - punycode "^1.4.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - integrity sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us= - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-is@~1.6.11, type-is@~1.6.6: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -uglify-js@^2.6: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= - -uid-safe@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.0.0.tgz#a7f3c6ca64a1f6a5d04ec0ef3e4c3d5367317137" - integrity sha1-p/PGymSh9qXQTsDvPkw9U2cxcTc= - dependencies: - base64-url "1.2.1" - -undefsafe@^2.0.2: - version "2.0.5" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -underscore@1.8.3: - version "1.8.3" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" - integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= - -underscore@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" - integrity sha1-a7rwh3UA02vjTsqlhODbn+8DUgk= - -"unicode@>= 0.3.1": - version "13.0.0" - resolved "https://registry.yarnpkg.com/unicode/-/unicode-13.0.0.tgz#0775fe86cdbb1fa30e8d060afe194f71aa0c5306" - integrity sha512-osNPLT4Lqna/sV6DQikrB8m4WxR61/k0fnhfKnkPGcZImczW3IysRXvWxfdqGUjh0Ju2o/tGGgu46mlfc/cpZw== - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unique-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" - integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= - dependencies: - crypto-random-string "^1.0.0" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -unzip-response@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" - integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -update-notifier@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" - integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== - dependencies: - boxen "^1.2.1" - chalk "^2.0.1" - configstore "^3.0.0" - import-lazy "^2.1.0" - is-ci "^1.0.10" - is-installed-globally "^0.1.0" - is-npm "^1.0.0" - latest-version "^3.0.0" - semver-diff "^2.0.0" - xdg-basedir "^3.0.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-parse-lax@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" - integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= - dependencies: - prepend-http "^1.0.1" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -utcstring@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/utcstring/-/utcstring-0.1.0.tgz#430fd510ab7fc95b5d5910c902d79880c208436b" - integrity sha1-Qw/VEKt/yVtdWRDJAteYgMIIQ2s= - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -utils-merge@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" - integrity sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg= - -uuid@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" - integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA== - -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -uuid@^3.0.0, uuid@^3.1.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uvm@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/uvm/-/uvm-1.7.3.tgz#57b37b218a158fa5c059de8527cd67ab64d82663" - integrity sha512-aKnLDcsr/qSYyiF9p049Kqatk/tHxT/gNanpbDzmdQ+XYo0E8lkCYwf478daiu8rXE3+TznBB8Sw/TKakJ6H1A== - dependencies: - circular-json "0.3.1" - inherits "2.0.3" - lodash "4.17.10" - uuid "3.2.1" - -vary@^1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -vary@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.0.1.tgz#99e4981566a286118dfb2b817357df7993376d10" - integrity sha1-meSYFWaihhGN+yuBc1ffeZM3bRA= - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -widest-line@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" - integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== - dependencies: - string-width "^2.1.1" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= - -word-wrap@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^2.0.0: - version "2.4.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" - integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - -xdg-basedir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" - integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= - -xmlbuilder@10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.0.0.tgz#c64e52f8ae097fe5fd46d1c38adaade071ee1b55" - integrity sha512-7RWHlmF1yU/E++BZkRQTEv8ZFAhZ+YHINUAxiZ5LQTKRQq//igpiY8rh7dJqPzgb/IzeC5jH9P7OaCERfM9DwA== - -xtend@^4.0.0, xtend@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" diff --git a/charts/templates/anythink-backend-deployment.yaml b/charts/templates/anythink-backend-deployment.yaml index 260d252..7b209f5 100644 --- a/charts/templates/anythink-backend-deployment.yaml +++ b/charts/templates/anythink-backend-deployment.yaml @@ -21,14 +21,16 @@ spec: - args: - sh - -c - - "yarn seeds && yarn start" + - "gradlew bootRun" env: - - name: MONGODB_URI - value: "{{ .Values.database.connectionProtocol }}{{ .Values.database.serviceName }}:{{ .Values.database.servicePort }}/{{ .Values.database.databaseName }}" - - name: NODE_ENV - value: development - - name: PORT - value: "{{ .Values.backend.containerPort }}" + - name: APP_ENV + value: dev + - name: SECRET_KEY + value: secret + - name: DEBUG + value: "True" + - name: DATABASE_URL + value: "{{ .Values.database.connectionProtocol }}{{ .Values.database.env.password }}:@{{ .Values.database.serviceName }}:{{ .Values.database.servicePort }}/{{ .Values.database.databaseName }}" image: "{{ include "anythink-tenant.backendRepository" .}}:{{ .Values.backend.image.tag }}" imagePullPolicy: {{ .Values.backend.image.pullPolicy }} name: {{ .Values.backend.serviceName }} @@ -52,4 +54,17 @@ spec: port: http resources: {{- toYaml .Values.backend.resources | nindent 12 }} + initContainers: + env: + - name: APP_ENV + value: dev + - name: SECRET_KEY + value: secret + - name: DEBUG + value: "True" + - name: DATABASE_URL + value: "{{ .Values.database.connectionProtocol}}{{ .Values.database.env.password}}:@{{ .Values.database.serviceName }}:{{ .Values.database.servicePort }}/{{ .Values.database.databaseName }}" + image: "{{ include "anythink-tenant.backendRepository" .}}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + name: db-migrations restartPolicy: Always diff --git a/charts/templates/database-deployment.yaml b/charts/templates/database-deployment.yaml index 62deccc..19752ee 100644 --- a/charts/templates/database-deployment.yaml +++ b/charts/templates/database-deployment.yaml @@ -20,6 +20,15 @@ spec: containers: - image: "{{ .Values.database.image.repository }}:{{ .Values.database.image.tag }}" name: {{ .Values.database.serviceName }} + env: + - name: POSTGRES_HOST_AUTH_METHOD + value: trust + - name: POSTGRES_USER + value: {{ .Values.database.env.userName }} + - name: POSTGRES_PASSWORD + value: {{ .Values.database.env.password }} + - name: POSTGRES_DB + value: {{ .Values.database.databaseName }} imagePullPolicy: {{ .Values.database.image.pullPolicy }} ports: - containerPort: {{ .Values.database.containerPort }} diff --git a/charts/values.yaml b/charts/values.yaml index 1fa785d..0464a04 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -46,17 +46,19 @@ frontend: database: deploy: true - serviceName: mongodb-node - containerPort: 27017 - servicePort: 27017 - connectionProtocol: mongodb:// + connectionProtocol: postgresql:// + serviceName: postgres-python + containerPort: 5433 + servicePort: 5432 databaseName: anythink-market replicaCount: 1 + env: + password: postgres service: type: ClusterIP port: 80 image: - repository: mongo + repository: postgres pullPolicy: IfNotPresent tag: "latest" resources: diff --git a/docker-compose.yml b/docker-compose.yml index 0d2be3a..e0b6bbb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,12 @@ version: "3.8" services: - anythink-backend-node: + anythink-backend-java: build: ./backend - container_name: anythink-backend-node - command: sh -c "cd backend && yarn install && /wait-for-it.sh mongodb-node:27017 -q -t 60 && yarn dev" + container_name: anythink-backend-java + command: sh -c "cd backend && /wait-for-it.sh postgres-java:5432 -q -t 60 && ./gradlew bootRun" environment: - - NODE_ENV=development - PORT=3000 - - MONGODB_URI=mongodb://mongodb-node:27017/anythink-market - GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN=${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN} working_dir: /usr/src volumes: @@ -17,12 +15,12 @@ services: ports: - "3000:3000" depends_on: - - "mongodb-node" + - "postgres-java" anythink-frontend-react: build: ./frontend container_name: anythink-frontend-react - command: sh -c "cd frontend && yarn install && /wait-for-it.sh anythink-backend-node:3000 -t 120 --strict -- curl --head -X GET --retry 30 --retry-connrefused --retry-delay 1 anythink-backend-node:3000/api/ping && yarn start" + command: sh -c "cd frontend && yarn install && /wait-for-it.sh anythink-backend-java:3000 -t 120 --strict -- curl --head -X GET --retry 30 --retry-connrefused --retry-delay 1 anythink-backend-java:3000/api/ping && yarn start" environment: - NODE_ENV=development - PORT=3001 @@ -35,18 +33,22 @@ services: ports: - "3001:3001" depends_on: - - "anythink-backend-node" + - "anythink-backend-java" - mongodb-node: - container_name: mongodb-node - restart: always - image: mongo + postgres-java: + container_name: postgres-java + restart: on-failure + image: postgres logging: driver: none + environment: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PASSWORD: postgres + POSTGRES_DB: anythink-market volumes: - - ~/mongo/data:/data/db + - ~/postgres/data:/data/db ports: - - '27017:27017' + - '5433:5432' anythink-ack: build: ./frontend @@ -56,4 +58,4 @@ services: volumes: - ./:/usr/src/ depends_on: - - "anythink-frontend-react" + - "anythink-frontend-react"