├── EBook.Web ├── README.md ├── src │ ├── App.css │ ├── components │ │ ├── ebooks │ │ │ ├── index.css │ │ │ └── index.js │ │ ├── ebook │ │ │ ├── index.css │ │ │ └── index.js │ │ ├── category │ │ │ └── index.js │ │ ├── search │ │ │ ├── index.css │ │ │ └── index.js │ │ └── categories │ │ │ └── index.js │ ├── config │ │ └── index.js │ ├── App.js │ ├── setupTests.js │ ├── client │ │ └── axios │ │ │ └── index.js │ ├── redux │ │ ├── reducers │ │ │ ├── index.js │ │ │ ├── ebooks.js │ │ │ └── categories.js │ │ ├── actions │ │ │ ├── categories.js │ │ │ └── ebooks.js │ │ └── store │ │ │ └── index.js │ ├── App.test.js │ ├── pages │ │ └── search │ │ │ ├── index.css │ │ │ └── index.js │ ├── index.js │ ├── index.css │ ├── logo.svg │ └── serviceWorker.js ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── .gitignore └── package.json ├── docs └── content.png ├── EBook.Domain ├── EBook.Domain.csproj ├── File.cs ├── Category.cs ├── Language.cs ├── Book.cs └── User.cs ├── EBook.Services.Contracts ├── Convert │ ├── IPdfConverter.cs │ ├── IFilePdfConverter.cs │ └── IConverter.cs ├── Query │ ├── IElasticQueryable.cs │ ├── IEBookElasticQueryable.cs │ ├── IHighlightable.cs │ ├── ISpecification.cs │ ├── IPaginable.cs │ └── IEBookSearchOptions.cs ├── ITokenService.cs ├── IAuthService.cs ├── EBook.Services.Contracts.csproj ├── IEBookServicesWrapper.cs ├── IEBooksSearchService.cs ├── IEBooksFilterService.cs ├── IEBookRepositoryService.cs └── Filter │ └── IEBookFilterOptions.cs ├── EBook.API ├── appsettings.Development.json ├── Models │ ├── Dto │ │ ├── LoginInfoDto.cs │ │ ├── FileDto.cs │ │ ├── RegisterInfoDto.cs │ │ ├── CategoryDto.cs │ │ ├── EBookElasticQueryableDto.cs │ │ ├── LanguageDto.cs │ │ ├── UserDto.cs │ │ └── BookDto.cs │ ├── EBookFilterOptions.cs │ └── EBookSearchOptions.cs ├── Mapper │ ├── Profiles │ │ ├── UserProfile.cs │ │ ├── CategoryProfile.cs │ │ ├── LanguageProfile.cs │ │ └── EBookProfile.cs │ └── Mapping.cs ├── appsettings.json ├── Program.cs ├── Exceptions │ └── ExceptionHandler.cs ├── Properties │ └── launchSettings.json ├── ConfigurationSettings.cs ├── Extensions │ ├── ElasticsearchMappingExtensions.cs │ ├── ElasticsearchExtensions.cs │ └── ServiceExtensions.cs ├── Controllers │ ├── ValuesController.cs │ ├── AuthController.cs │ ├── LanguagesController.cs │ ├── CategoriesController.cs │ └── EBooksController.cs ├── Elasticsearch │ ├── Index │ │ └── DefaultIndexConfiguration.cs │ └── Mappings │ │ └── EBookMapping.cs ├── EBook.API.csproj └── Startup.cs ├── EBook.Persistence.Contracts ├── IEBooksRepository.cs ├── ISearchable.cs ├── IUsersRepository.cs ├── EBook.Persistence.Contracts.csproj └── IRepositoryBase.cs ├── EBook.Services ├── Queries │ ├── SearchRequestSpecification.cs │ ├── Match │ │ ├── EBookTitleQuery.cs │ │ ├── EBookAuthorQuery.cs │ │ ├── EBookContentQuery.cs │ │ ├── EBookKeywordsQuery.cs │ │ ├── EBookCategoryQuery.cs │ │ └── EBookLanguageQuery.cs │ ├── Fuzzy │ │ ├── EBookContenctFuzzyQuery.cs │ │ ├── EBookKeywordsFuzzyQuery.cs │ │ ├── EBookCategoryFuzzyQuery.cs │ │ ├── EBookLanguageFuzzyQuery.cs │ │ ├── EBookAuthorFuzzyQuery.cs │ │ └── EBookTitleFuzzyQuery.cs │ ├── PaginatedSearchRequestSpecification.cs │ ├── AndSearchRequestSpecification.cs │ ├── OrSearchRequestSpecification.cs │ └── HighlightSearchRequestSpecification.cs ├── Models │ ├── HighlightableEBook.cs │ ├── ElasticQueryable.cs │ └── EBookElasticQueryable.cs ├── EBookServicesWrapper.cs ├── EBook.Services.csproj ├── AuthenticationService.cs ├── EBookRepositoryService.cs ├── Convert │ └── PdfConverter.cs ├── TokenService.cs ├── EBooksFilterService.cs └── EBooksSearchService.cs ├── EBook.Persistence ├── EBook.Persistence.csproj ├── EBooksRepository.cs └── UsersRepository.cs ├── scripts ├── elastic_bulk_formatter.py ├── ebooks-bulk.json └── ebooks.json ├── README.md ├── EBook.sln └── .gitignore /EBook.Web/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /EBook.Web/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mladjo97/ebook/HEAD/docs/content.png -------------------------------------------------------------------------------- /EBook.Web/src/components/ebooks/index.css: -------------------------------------------------------------------------------- 1 | .ebooks-list { 2 | display: inline-block; 3 | } -------------------------------------------------------------------------------- /EBook.Web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mladjo97/ebook/HEAD/EBook.Web/public/favicon.ico -------------------------------------------------------------------------------- /EBook.Web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mladjo97/ebook/HEAD/EBook.Web/public/logo192.png -------------------------------------------------------------------------------- /EBook.Web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mladjo97/ebook/HEAD/EBook.Web/public/logo512.png -------------------------------------------------------------------------------- /EBook.Web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /EBook.Web/src/components/ebook/index.css: -------------------------------------------------------------------------------- 1 | .ebook-card { 2 | width: 350px; 3 | margin: 20px 0px; 4 | text-align: left; 5 | } -------------------------------------------------------------------------------- /EBook.Web/src/config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | api: { 3 | baseUrl: 'http://localhost:4321/api' 4 | } 5 | }; 6 | 7 | export default config; -------------------------------------------------------------------------------- /EBook.Domain/EBook.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Convert/IPdfConverter.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Convert 2 | { 3 | public interface IPdfConverter : IConverter 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /EBook.Web/src/components/category/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Category = ({ name }) => { 4 | return ( 5 |

{ name }

6 | ); 7 | } 8 | 9 | export default Category; -------------------------------------------------------------------------------- /EBook.Services.Contracts/Query/IElasticQueryable.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Query 2 | { 3 | public interface IElasticQueryable : IPaginable where T : class 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /EBook.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Convert/IFilePdfConverter.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Convert 2 | { 3 | using EBook.Domain; 4 | 5 | public interface IFilePdfConverter : IPdfConverter 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/LoginInfoDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | public class LoginInfoDto 4 | { 5 | public string Username { get; set; } 6 | public string Password { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/ITokenService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts 2 | { 3 | using System.Threading.Tasks; 4 | 5 | public interface ITokenService 6 | { 7 | string GenerateToken(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.Persistence.Contracts/IEBooksRepository.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Persistence.Contracts 2 | { 3 | using EBook.Domain; 4 | 5 | public interface IEBooksRepository : IRepositoryBase, ISearchable 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Query/IEBookElasticQueryable.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Query 2 | { 3 | using EBook.Domain; 4 | 5 | public interface IEBookElasticQueryable : IElasticQueryable 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Convert/IConverter.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Convert 2 | { 3 | using System.Threading.Tasks; 4 | 5 | public interface IConverter 6 | { 7 | Task Convert(TIn data); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/FileDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | public class FileDto 4 | { 5 | public string Path { get; set; } 6 | public string Mime { get; set; } 7 | public string Filename { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.Web/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchPage from './pages/search'; 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /EBook.Web/src/components/search/index.css: -------------------------------------------------------------------------------- 1 | .search-form { 2 | vertical-align: middle; 3 | display: inline-block; 4 | width: 100%; 5 | } 6 | 7 | .search-form > Button { 8 | height: 35px; 9 | width: 120px; 10 | margin-left: 50px; 11 | margin-top: 10px; 12 | } -------------------------------------------------------------------------------- /EBook.Services.Contracts/IAuthService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts 2 | { 3 | using System.Threading.Tasks; 4 | 5 | public interface IAuthService 6 | { 7 | public Task Authenticate(string username, string password); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.Domain/File.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Domain 2 | { 3 | public class File 4 | { 5 | public string Path { get; set; } 6 | public string Mime { get; set; } 7 | public string Content { get; set; } 8 | public string Filename { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EBook.Web/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /EBook.Persistence.Contracts/ISearchable.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Persistence.Contracts 2 | { 3 | using Nest; 4 | using System.Threading.Tasks; 5 | 6 | public interface ISearchable where T : class 7 | { 8 | Task> Search(ISearchRequest query); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Query/IHighlightable.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Query 2 | { 3 | using System.Collections.Generic; 4 | 5 | public interface IHighlightable 6 | { 7 | IReadOnlyDictionary> Highlights { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Query/ISpecification.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Query 2 | { 3 | public interface ISpecification where T : class 4 | { 5 | // ISearchRequest ? 6 | // but then Nest would be installed on this lib 7 | T IsSatisfiedBy(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.Web/src/client/axios/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from '../../config'; 3 | 4 | const instance = axios.create({ 5 | baseURL: config.api.baseUrl, 6 | timeout: 10000, 7 | headers: { 8 | 'Content-Type': 'application/json' 9 | } 10 | }); 11 | 12 | export default instance; -------------------------------------------------------------------------------- /EBook.Web/src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import categoriesReducer from './categories'; 4 | import eBooksReducer from './ebooks'; 5 | 6 | const rootReducer = combineReducers({ 7 | categoriesReducer, 8 | eBooksReducer 9 | }); 10 | 11 | export default rootReducer; -------------------------------------------------------------------------------- /EBook.Persistence.Contracts/IUsersRepository.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Persistence.Contracts 2 | { 3 | using EBook.Domain; 4 | using System.Threading.Tasks; 5 | 6 | public interface IUsersRepository : IRepositoryBase 7 | { 8 | Task GetByUsername(string username); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/RegisterInfoDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | public class RegisterInfoDto 4 | { 5 | public string FirstName { get; set; } 6 | public string LastName { get; set; } 7 | public string Username { get; set; } 8 | public string Type { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/EBook.Services.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/IEBookServicesWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts 2 | { 3 | public interface IEBookServicesWrapper 4 | { 5 | IEBooksSearchService SearchService { get; } 6 | IEBooksFilterService FilterService { get; } 7 | IEBookRepositoryService RepositoryService { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EBook.Web/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /EBook.API/Mapper/Profiles/UserProfile.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Mapper.Profiles 2 | { 3 | using AutoMapper; 4 | using EBook.API.Models.Dto; 5 | using EBook.Domain; 6 | 7 | public class UserProfile : Profile 8 | { 9 | public UserProfile() 10 | { 11 | CreateMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.Services/Queries/SearchRequestSpecification.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries 2 | { 3 | using EBook.Services.Contracts.Query; 4 | using Nest; 5 | 6 | public abstract class SearchRequestSpecification : ISpecification> where T : class 7 | { 8 | public abstract ISearchRequest IsSatisfiedBy(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EBook.Domain/Category.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Domain 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class Category 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | 10 | public IEnumerable EBooks { get; set; } 11 | 12 | public Category() => EBooks = new List(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.Domain/Language.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Domain 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class Language 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | 10 | public IEnumerable EBooks { get; set; } 11 | 12 | public Language() => EBooks = new List(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.Web/src/pages/search/index.css: -------------------------------------------------------------------------------- 1 | .search-page { 2 | border: 1px solid black; 3 | border-radius: 5px; 4 | height: 600px; 5 | width: 450px; 6 | text-align: center; 7 | background-color: white; 8 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 9 | padding: 15px; 10 | } 11 | 12 | .search-page > EBooks { 13 | margin-top: 20px; 14 | } -------------------------------------------------------------------------------- /EBook.Services.Contracts/Query/IPaginable.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Query 2 | { 3 | using System.Collections.Generic; 4 | 5 | public interface IPaginable where T : class 6 | { 7 | int Total { get; set; } 8 | int Page { get; set; } 9 | int Size { get; set; } 10 | 11 | IEnumerable Items { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /EBook.API/Mapper/Profiles/CategoryProfile.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Mapper.Profiles 2 | { 3 | using AutoMapper; 4 | using EBook.API.Models.Dto; 5 | using EBook.Domain; 6 | 7 | public class CategoryProfile : Profile 8 | { 9 | public CategoryProfile() 10 | { 11 | CreateMap().ReverseMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.API/Mapper/Profiles/LanguageProfile.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Mapper.Profiles 2 | { 3 | using AutoMapper; 4 | using EBook.API.Models.Dto; 5 | using EBook.Domain; 6 | 7 | public class LanguageProfile : Profile 8 | { 9 | public LanguageProfile() 10 | { 11 | CreateMap().ReverseMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/CategoryDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class CategoryDto 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | 10 | public IEnumerable EBooks { get; set; } 11 | 12 | public CategoryDto() => EBooks = new List(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/EBookElasticQueryableDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class EBookElasticQueryableDto 6 | { 7 | public int Total { get; set; } 8 | public int Page { get; set; } 9 | public int Size { get; set; } 10 | public IEnumerable Items { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/LanguageDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class LanguageDto 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | 10 | public IEnumerable EBooks { get; set; } 11 | 12 | public LanguageDto() => EBooks = new List(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.Services/Models/HighlightableEBook.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Models 2 | { 3 | using EBook.Domain; 4 | using EBook.Services.Contracts.Query; 5 | using System.Collections.Generic; 6 | 7 | public class HighlightableEBook : Book, IHighlightable 8 | { 9 | public IReadOnlyDictionary> Highlights { get; set; } 10 | } 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/IEBooksSearchService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts 2 | { 3 | using EBook.Services.Contracts.Query; 4 | using System.Threading.Tasks; 5 | 6 | public interface IEBooksSearchService 7 | { 8 | Task Search(IEBookSearchOptions options); 9 | Task FuzzySearch(IEBookSearchOptions options); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EBook.Web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /EBook.Persistence.Contracts/EBook.Persistence.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/IEBooksFilterService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts 2 | { 3 | using EBook.Services.Contracts.Filter; 4 | using EBook.Services.Contracts.Query; 5 | using System.Threading.Tasks; 6 | 7 | public interface IEBooksFilterService 8 | { 9 | Task Filter(IEBookFilterOptions options); 10 | Task FuzzyFilter(IEBookFilterOptions options); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EBook.Web/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | import App from './App'; 8 | import store from './redux/store'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /EBook.Services/Models/ElasticQueryable.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Models 2 | { 3 | using EBook.Services.Contracts.Query; 4 | using System.Collections.Generic; 5 | 6 | public abstract class ElasticQueryable : IElasticQueryable where T : class 7 | { 8 | public int Total { get; set; } 9 | public int Page { get; set; } 10 | public int Size { get; set; } 11 | public IEnumerable Items { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /EBook.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "jwt": { 9 | "secret": "suchsecretmuchwow", 10 | "audience": "http://localhost:4321", 11 | "issuer": "http://localhost:4321" 12 | }, 13 | "elasticsearch": { 14 | "url": "http://localhost:9200/", 15 | "defaultIndex": "ebooks", 16 | "usersIndex": "users", 17 | "ebooksIndex": "ebooks" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /EBook.Services/Models/EBookElasticQueryable.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Models 2 | { 3 | using EBook.Domain; 4 | using EBook.Services.Contracts.Query; 5 | using System.Collections.Generic; 6 | 7 | public class EBookElasticQueryable : IEBookElasticQueryable 8 | { 9 | public int Total { get; set; } 10 | public int Page { get; set; } 11 | public int Size { get; set; } 12 | public IEnumerable Items { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.Domain/Book.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Domain 2 | { 3 | public class Book 4 | { 5 | public int Id { get; set; } 6 | public string Title { get; set; } 7 | public string Author { get; set; } 8 | public string Keywords { get; set; } 9 | public int PublicationYear { get; set; } 10 | 11 | public File File { get; set; } 12 | public Category Category { get; set; } 13 | public Language Language { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/IEBookRepositoryService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts 2 | { 3 | using EBook.Domain; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | public interface IEBookRepositoryService 8 | { 9 | Task> GetAll(); 10 | Task Get(int id); 11 | Task Create(Book book); 12 | Task Update(int id, Book book); 13 | Task Delete(int id); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Query/IEBookSearchOptions.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Query 2 | { 3 | public interface IEBookSearchOptions 4 | { 5 | string Title { get; set; } 6 | string Author { get; set; } 7 | string Keywords { get; set; } 8 | string Language { get; set; } 9 | string Category { get; set; } 10 | string Content { get; set; } 11 | 12 | int Page { get; set; } 13 | int Size { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EBook.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace EBook.API 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EBook.Services.Contracts/Filter/IEBookFilterOptions.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Contracts.Filter 2 | { 3 | public interface IEBookFilterOptions 4 | { 5 | string Title { get; set; } 6 | string Author { get; set; } 7 | string Keywords { get; set; } 8 | string Language { get; set; } 9 | string Category { get; set; } 10 | string Content { get; set; } 11 | 12 | int Page { get; set; } 13 | int Size { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EBook.Persistence.Contracts/IRepositoryBase.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Persistence.Contracts 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | public interface IRepositoryBase where TEntity : class 7 | { 8 | Task> GetAll(); 9 | Task Get(TPKey primaryKey); 10 | Task Create(TEntity entity); 11 | Task Update(TPKey primaryKey, TEntity entity); 12 | Task Delete(TPKey primaryKey); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /EBook.Web/src/components/ebooks/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import EBook from '../ebook'; 5 | import { eBooksSelector } from '../../redux/reducers/ebooks'; 6 | import './index.css'; 7 | 8 | const EBooks = () => { 9 | const eBooks = useSelector(eBooksSelector); 10 | 11 | return ( 12 |
13 | { 14 | eBooks.map(eBook => 15 | ) 16 | } 17 |
18 | ) 19 | }; 20 | 21 | export default EBooks; -------------------------------------------------------------------------------- /EBook.Web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 50px 35px; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: #43c3ff; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | .app-container { 17 | max-width: 500px; 18 | margin: auto; 19 | } 20 | -------------------------------------------------------------------------------- /EBook.API/Models/EBookFilterOptions.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models 2 | { 3 | using EBook.Services.Contracts.Filter; 4 | 5 | public class EBookFilterOptions : IEBookFilterOptions 6 | { 7 | public string Title { get; set; } 8 | public string Author { get; set; } 9 | public string Keywords { get; set; } 10 | public string Language { get; set; } 11 | public string Category { get; set; } 12 | public string Content { get; set; } 13 | 14 | public int Page { get; set; } 15 | public int Size { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EBook.API/Models/EBookSearchOptions.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models 2 | { 3 | using EBook.Services.Contracts.Query; 4 | 5 | public class EBookSearchOptions : IEBookSearchOptions 6 | { 7 | public string Title { get; set; } 8 | public string Author { get; set; } 9 | public string Keywords { get; set; } 10 | public string Language { get; set; } 11 | public string Category { get; set; } 12 | public string Content { get; set; } 13 | 14 | public int Page { get; set; } 15 | public int Size { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/UserDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class UserDto 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public string Username { get; set; } 11 | public string Type { get; set; } 12 | 13 | public CategoryDto Category { get; set; } 14 | public IEnumerable EBooks { get; set; } 15 | 16 | public UserDto() => EBooks = new List(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /EBook.Domain/User.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Domain 2 | { 3 | using System.Collections.Generic; 4 | 5 | public class User 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public string Username { get; set; } 11 | public string Password { get; set; } 12 | public string Type { get; set; } 13 | 14 | public Category Category { get; set; } 15 | public IEnumerable EBooks { get; set; } 16 | 17 | public User() => EBooks = new List(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /EBook.Web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /EBook.Persistence/EBook.Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/elastic_bulk_formatter.py: -------------------------------------------------------------------------------- 1 | # @desc: script that formats json array data to elastic bulk data 2 | # @author: mladen milosevic 3 | # @date: 25.02.2020. 4 | 5 | import json 6 | import time 7 | 8 | inputFile = 'ebooks.json' 9 | outputFile = 'ebooks-bulk.json' 10 | 11 | start = time.process_time() 12 | with open(inputFile, 'r', encoding = "utf8") as moviesFile: 13 | movies = json.load(moviesFile) 14 | with open(outputFile, 'w+') as bulkFile: 15 | for i in range(len(movies)): 16 | index = f'{{"index": {{ "_id":"{i+1}" }} }}\n' 17 | bulkFile.write(index) 18 | bulkFile.write(f'{json.dumps(movies[i])}\n') 19 | 20 | print(time.process_time() - start) -------------------------------------------------------------------------------- /EBook.Web/src/redux/actions/categories.js: -------------------------------------------------------------------------------- 1 | import { RSAA } from 'redux-api-middleware'; 2 | 3 | import config from '../../config'; 4 | 5 | export const GET_CATEGORIES_REQUEST = 'GET_CATEGORIES_REQUEST'; 6 | export const GET_CATEGORIES_SUCCESS = 'GET_CATEGORIES_SUCCESS'; 7 | export const GET_CATEGORIES_FAILURE = 'GET_CATEGORIES_FAILURE'; 8 | 9 | export const getCategories = () => dispatch => { 10 | return dispatch({ 11 | [RSAA]: { 12 | endpoint: `${config.api.baseUrl}/categories`, 13 | method: 'GET', 14 | types: [ 15 | GET_CATEGORIES_REQUEST, 16 | GET_CATEGORIES_SUCCESS, 17 | GET_CATEGORIES_FAILURE 18 | ] 19 | } 20 | }); 21 | } -------------------------------------------------------------------------------- /EBook.Web/src/redux/actions/ebooks.js: -------------------------------------------------------------------------------- 1 | import { RSAA } from "redux-api-middleware"; 2 | import config from "../../config"; 3 | 4 | export const SEARCH_EBOOKS_REQUEST = 'SEARCH_EBOOKS_REQUEST'; 5 | export const SEARCH_EBOOKS_SUCCESS = 'SEARCH_EBOOKS_SUCCESS'; 6 | export const SEARCH_EBOOKS_FAILURE = 'SEARCH_EBOOKS_FAILURE'; 7 | 8 | export const searchEBooks = content => dispatch => { 9 | return dispatch({ 10 | [RSAA]: { 11 | endpoint: `${config.api.baseUrl}/ebooks/search?content=${content}&fuzzy=true`, 12 | method: 'GET', 13 | types: [ 14 | SEARCH_EBOOKS_REQUEST, 15 | SEARCH_EBOOKS_SUCCESS, 16 | SEARCH_EBOOKS_FAILURE 17 | ] 18 | } 19 | }); 20 | } -------------------------------------------------------------------------------- /EBook.Web/src/redux/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { apiMiddleware } from 'redux-api-middleware'; 3 | import { createLogger } from 'redux-logger'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | 6 | import rootReducer from '../reducers'; 7 | 8 | const configureStore = (initialState = {}) => { 9 | const middlewares = [thunkMiddleware, apiMiddleware]; 10 | 11 | const loggerMiddleware = createLogger(); 12 | middlewares.push(loggerMiddleware); 13 | 14 | return createStore( 15 | rootReducer, 16 | initialState, 17 | applyMiddleware(...middlewares) 18 | ) 19 | } 20 | 21 | const store = configureStore(); 22 | export default store; -------------------------------------------------------------------------------- /EBook.Web/src/pages/search/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import Search from '../../components/search'; 5 | import EBooks from '../../components/ebooks'; 6 | import { searchEBooks } from '../../redux/actions/ebooks'; 7 | 8 | import './index.css'; 9 | 10 | const SearchPage = () => { 11 | 12 | const dispatch = useDispatch(); 13 | 14 | const onSubmitHandler = query => { 15 | if(!query) return; 16 | 17 | dispatch(searchEBooks(query)); 18 | } 19 | 20 | return ( 21 |
22 | 23 | 24 |
25 | ) 26 | }; 27 | 28 | export default SearchPage; -------------------------------------------------------------------------------- /EBook.API/Exceptions/ExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Exceptions 2 | { 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using Newtonsoft.Json; 6 | 7 | public class ExceptionHandler : ExceptionFilterAttribute 8 | { 9 | public override void OnException(ExceptionContext context) 10 | { 11 | var exception = context.Exception; 12 | 13 | // @TODO: 14 | // - Add custom exception handlers 15 | // - Change to custom messages, not exception errors 16 | 17 | var result = JsonConvert.SerializeObject(new { context.HttpContext.Response.StatusCode, Error = exception.Message }); 18 | 19 | context.Result = new ObjectResult(result); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /EBook.Web/src/components/categories/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { categoriesSelector } from '../../redux/reducers/categories'; 4 | import { getCategories } from '../../redux/actions/categories'; 5 | import Category from '../category'; 6 | 7 | const Categories = () => { 8 | const categories = useSelector(categoriesSelector); 9 | 10 | const dispatch = useDispatch(); 11 | useEffect(() => { dispatch(getCategories()) }, [ dispatch ]); 12 | 13 | return ( 14 | 15 |

Categories

16 | { 17 | categories.map(cat => ) 18 | } 19 |
20 | ); 21 | }; 22 | 23 | 24 | export default Categories; -------------------------------------------------------------------------------- /EBook.Services/Queries/Match/EBookTitleQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Match 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookTitleQuery : SearchRequestSpecification 8 | { 9 | private readonly string _title; 10 | 11 | public EBookTitleQuery(string title) 12 | => _title = title ?? throw new ArgumentNullException($"{nameof(title)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Title) 19 | .Query(_title) 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Match/EBookAuthorQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Match 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookAuthorQuery : SearchRequestSpecification 8 | { 9 | private readonly string _author; 10 | 11 | public EBookAuthorQuery(string author) 12 | => _author = author ?? throw new ArgumentNullException($"{nameof(author)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Author) 19 | .Query(_author) 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Match/EBookContentQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Match 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookContentQuery : SearchRequestSpecification 8 | { 9 | private readonly string _content; 10 | 11 | public EBookContentQuery(string content) 12 | => _content = content ?? throw new ArgumentNullException($"{nameof(content)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.File.Content) 19 | .Query(_content) 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Match/EBookKeywordsQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Match 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookKeywordsQuery : SearchRequestSpecification 8 | { 9 | private readonly string _keywords; 10 | 11 | public EBookKeywordsQuery(string keywords) 12 | => _keywords = keywords ?? throw new ArgumentNullException($"{nameof(keywords)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Keywords) 19 | .Query(_keywords) 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Match/EBookCategoryQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Match 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookCategoryQuery : SearchRequestSpecification 8 | { 9 | private readonly string _category; 10 | 11 | public EBookCategoryQuery(string category) 12 | => _category = category ?? throw new ArgumentNullException($"{nameof(category)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Category.Name) 19 | .Query(_category) 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Match/EBookLanguageQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Match 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookLanguageQuery : SearchRequestSpecification 8 | { 9 | private readonly string _language; 10 | 11 | public EBookLanguageQuery(string language) 12 | => _language = language ?? throw new ArgumentNullException($"{nameof(language)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Language.Name) 19 | .Query(_language) 20 | ) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EBook.Services/EBookServicesWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services 2 | { 3 | using EBook.Services.Contracts; 4 | 5 | public class EBookServicesWrapper : IEBookServicesWrapper 6 | { 7 | public IEBooksSearchService SearchService { get; private set; } 8 | public IEBooksFilterService FilterService { get; private set; } 9 | public IEBookRepositoryService RepositoryService { get; private set; } 10 | 11 | public EBookServicesWrapper( 12 | IEBooksSearchService searchService, 13 | IEBooksFilterService filterService, 14 | IEBookRepositoryService repositoryServices 15 | ) 16 | { 17 | SearchService = searchService; 18 | FilterService = filterService; 19 | RepositoryService = repositoryServices; 20 | } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EBook.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:4321", 7 | "sslPort": 0 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "EBook.API": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "http://localhost:5000" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /EBook.API/ConfigurationSettings.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API 2 | { 3 | public static class ConfigurationSettings 4 | { 5 | /// 6 | /// Elasticsearch configuration sections 7 | /// 8 | public const string ElasticsearchSectionKey = "elasticsearch"; 9 | public const string UrlKey = "url"; 10 | public const string DefaultIndexKey = "defaultIndex"; 11 | public const string EBooksIndexKey = "ebooksIndex"; 12 | public const string UsersIndexKey = "usersIndex"; 13 | 14 | /// 15 | /// JWT configuration sections 16 | /// 17 | public static string JwtConfigKey = "jwt"; 18 | public static string IssuerConfigKey = "issuer"; 19 | public static string SecretConfigKey = "secret"; 20 | public static string AudienceConfigKey = "audience"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Fuzzy/EBookContenctFuzzyQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Fuzzy 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookContentFuzzyQuery : SearchRequestSpecification 8 | { 9 | private readonly string _content; 10 | 11 | public EBookContentFuzzyQuery(string content) 12 | => _content = content ?? throw new ArgumentNullException($"{nameof(content)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.File.Content) 19 | .Query(_content) 20 | .Fuzziness(Fuzziness.Auto) 21 | ) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Fuzzy/EBookKeywordsFuzzyQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Fuzzy 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookKeywordsFuzzyQuery : SearchRequestSpecification 8 | { 9 | private readonly string _keywords; 10 | 11 | public EBookKeywordsFuzzyQuery(string keywords) 12 | => _keywords = keywords ?? throw new ArgumentNullException($"{nameof(keywords)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Keywords) 19 | .Query(_keywords) 20 | .Fuzziness(Fuzziness.Auto) 21 | ) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Fuzzy/EBookCategoryFuzzyQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Fuzzy 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookCategoryFuzzyQuery : SearchRequestSpecification 8 | { 9 | private readonly string _category; 10 | 11 | public EBookCategoryFuzzyQuery(string category) 12 | => _category = category ?? throw new ArgumentNullException($"{nameof(category)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Category.Name) 19 | .Query(_category) 20 | .Fuzziness(Fuzziness.Auto) 21 | ) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Fuzzy/EBookLanguageFuzzyQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Fuzzy 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookLanguageFuzzyQuery : SearchRequestSpecification 8 | { 9 | private readonly string _language; 10 | 11 | public EBookLanguageFuzzyQuery(string language) 12 | => _language = language ?? throw new ArgumentNullException($"{nameof(language)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Language.Name) 19 | .Query(_language) 20 | .Fuzziness(Fuzziness.Auto) 21 | ) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /EBook.Web/src/components/search/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import './index.css'; 5 | 6 | const Search = ({ label, submitHandler }) => { 7 | const [query, setQuery] = useState(''); 8 | 9 | const onQueryChangeHandler = e => { 10 | setQuery(e.target.value); 11 | } 12 | 13 | const onSubmit = () => { 14 | submitHandler(query); 15 | } 16 | 17 | return ( 18 |
19 | 23 | 24 | 30 |
31 | ); 32 | }; 33 | 34 | export default Search; -------------------------------------------------------------------------------- /EBook.API/Extensions/ElasticsearchMappingExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Extensions 2 | { 3 | using Nest; 4 | using EBook.API.Elasticsearch.Mappings; 5 | using EBook.Domain; 6 | using Microsoft.Extensions.Configuration; 7 | 8 | public static class ElasticsearchMappingExtensions 9 | { 10 | public static IElasticClient ConfigureMappings(this IElasticClient client, IConfiguration config) 11 | { 12 | var indexSettings = new IndexSettings() 13 | { 14 | NumberOfReplicas = 0, 15 | NumberOfShards = 1 16 | }; 17 | 18 | // @TODO: 19 | // - Research mappings between multiple indexes 20 | client.ConfigureEBookMapping(config); 21 | 22 | // testing elasticsearch dynamic mapping 23 | client.Indices.Create("users", c => c.Map(m => m.AutoMap())); 24 | 25 | return client; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EBook.Services/EBook.Services.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Fuzzy/EBookAuthorFuzzyQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Fuzzy 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookAuthorFuzzyQuery : SearchRequestSpecification 8 | { 9 | private readonly string _author; 10 | 11 | public EBookAuthorFuzzyQuery(string author) 12 | => _author = author ?? throw new ArgumentNullException($"{nameof(author)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Author) 19 | .Query(_author) 20 | // @Reference: https://qbox.io/blog/elasticsearch-optimization-fuzziness-performance 21 | .Fuzziness(Fuzziness.Auto) 22 | ) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /EBook.Services/Queries/Fuzzy/EBookTitleFuzzyQuery.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries.Fuzzy 2 | { 3 | using EBook.Domain; 4 | using Nest; 5 | using System; 6 | 7 | public class EBookTitleFuzzyQuery : SearchRequestSpecification 8 | { 9 | private readonly string _title; 10 | 11 | public EBookTitleFuzzyQuery(string title) 12 | => _title = title ?? throw new ArgumentNullException($"{nameof(title)} cannot be null."); 13 | 14 | public override ISearchRequest IsSatisfiedBy() 15 | => new SearchDescriptor() 16 | .Query(q => q 17 | .Match(m => m 18 | .Field(f => f.Title) 19 | .Query(_title) 20 | // @Reference: https://qbox.io/blog/elasticsearch-optimization-fuzziness-performance 21 | .Fuzziness(Fuzziness.EditDistance(3)) 22 | ) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /EBook.API/Mapper/Mapping.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Mapper 2 | { 3 | using AutoMapper; 4 | using EBook.API.Mapper.Profiles; 5 | using System; 6 | 7 | public class Mapping 8 | { 9 | private static readonly Lazy Lazy = new Lazy(() => 10 | { 11 | MapperConfiguration config = new MapperConfiguration(cfg => 12 | { 13 | cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.GetMethod.IsAssembly; 14 | cfg.AddProfile(); 15 | cfg.AddProfile(); 16 | cfg.AddProfile(); 17 | cfg.AddProfile(); 18 | }); 19 | 20 | IMapper mapper = config.CreateMapper(); 21 | return mapper; 22 | }); 23 | 24 | /// 25 | /// Mapper for usage in class libraries 26 | /// 27 | public static IMapper Mapper => Lazy.Value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /EBook.Services/Queries/PaginatedSearchRequestSpecification.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries 2 | { 3 | using Nest; 4 | using System; 5 | 6 | public class PaginatedSearchRequestSpecification 7 | : SearchRequestSpecification where T : class 8 | { 9 | private readonly SearchRequestSpecification _query; 10 | private readonly int _page; 11 | private readonly int _size; 12 | 13 | public PaginatedSearchRequestSpecification(SearchRequestSpecification query, int page, int size) 14 | { 15 | _query = query ?? throw new ArgumentNullException($"{nameof(query)} cannot be null."); 16 | _page = page > 0 ? page : 1; 17 | _size = size > 0 ? size : 10; 18 | } 19 | 20 | public override ISearchRequest IsSatisfiedBy() 21 | => new SearchDescriptor() 22 | .Query(q => _query 23 | .IsSatisfiedBy() 24 | .Query 25 | ) 26 | .From(_page) 27 | .Size(_size); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /EBook.API/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Collections.Generic; 3 | 4 | namespace EBook.API.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | [ApiController] 8 | public class ValuesController : ControllerBase 9 | { 10 | // GET api/values 11 | [HttpGet] 12 | public ActionResult> Get() 13 | { 14 | return new string[] { "value1", "value2" }; 15 | } 16 | 17 | // GET api/values/5 18 | [HttpGet("{id}")] 19 | public ActionResult Get(int id) 20 | { 21 | return "value"; 22 | } 23 | 24 | // POST api/values 25 | [HttpPost] 26 | public void Post([FromBody] string value) 27 | { 28 | } 29 | 30 | // PUT api/values/5 31 | [HttpPut("{id}")] 32 | public void Put(int id, [FromBody] string value) 33 | { 34 | } 35 | 36 | // DELETE api/values/5 37 | [HttpDelete("{id}")] 38 | public void Delete(int id) 39 | { 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /EBook.Services/AuthenticationService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services 2 | { 3 | using EBook.Persistence.Contracts; 4 | using EBook.Services.Contracts; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | public class AuthenticationService : IAuthService 9 | { 10 | private readonly IUsersRepository _usersRepository; 11 | 12 | public AuthenticationService(IUsersRepository usersRepository) 13 | => _usersRepository = usersRepository; 14 | 15 | public async Task Authenticate(string username, string password) 16 | { 17 | // @TODO: 18 | // - Add password hashing 19 | // - Add hash comparison and validation 20 | try 21 | { 22 | var user = await _usersRepository.GetByUsername(username); 23 | 24 | if (user == null) 25 | return false; 26 | 27 | return user.Password == password; 28 | } 29 | catch (Exception) 30 | { 31 | throw; 32 | } 33 | } 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /EBook.Services/Queries/AndSearchRequestSpecification.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries 2 | { 3 | using Nest; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | public class AndSearchRequestSpecification : SearchRequestSpecification where T : class 9 | { 10 | private readonly IEnumerable> _queries; 11 | 12 | public AndSearchRequestSpecification(IEnumerable> queries) 13 | => _queries = queries ?? throw new ArgumentNullException($"{nameof(queries)} cannot be null."); 14 | 15 | public override ISearchRequest IsSatisfiedBy() 16 | => new SearchDescriptor() 17 | .Query(q => q 18 | .Bool(b => b 19 | .Must( 20 | _queries.Select(qu => qu 21 | .IsSatisfiedBy() 22 | .Query 23 | ).ToArray() 24 | ) 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EBook.Services/Queries/OrSearchRequestSpecification.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries 2 | { 3 | using Nest; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | public class OrSearchRequestSpecification : SearchRequestSpecification where T : class 9 | { 10 | private readonly IEnumerable> _queries; 11 | 12 | public OrSearchRequestSpecification(IEnumerable> queries) 13 | => _queries = queries ?? throw new ArgumentNullException($"{nameof(queries)} cannot be null."); 14 | 15 | public override ISearchRequest IsSatisfiedBy() 16 | => new SearchDescriptor() 17 | .Query(q => q 18 | .Bool(b => b 19 | .Should( 20 | _queries.Select(qu => qu 21 | .IsSatisfiedBy() 22 | .Query 23 | ).ToArray() 24 | ) 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/ebooks-bulk.json: -------------------------------------------------------------------------------- 1 | {"index": { "_id":"1" } } 2 | {"title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "category": {"id": 1, "name": "Drama"}, "keywords": "the great gatsby classic novel", "language": {"id": 1, "name": "English"}} 3 | {"index": { "_id":"2" } } 4 | {"title": "Catch-22", "author": "Joseph Heller", "category": {"id": 2, "name": "War"}, "keywords": "war catch 22 comedy drama", "language": {"id": 1, "name": "English"}} 5 | {"index": { "_id":"3" } } 6 | {"title": "The Lord of the Rings", "author": "J.R.R. Tolkien", "category": {"id": 3, "name": "Fantasy"}, "keywords": "lord of the rings fantasy gandalf classic", "language": {"id": 1, "name": "English"}} 7 | {"index": { "_id":"4" } } 8 | {"title": "Clash of Kings", "author": "George R.R. Martin", "category": {"id": 3, "name": "Fantasy"}, "keywords": "song of ice and fire fantasy clash of kings winter is coming", "language": {"id": 1, "name": "English"}} 9 | {"index": { "_id":"5" } } 10 | {"title": "War and Peace", "author": "Leo Tolstoy", "category": {"id": 1, "name": "Drama"}, "keywords": "war and peace russian author", "language": {"id": 1, "name": "English"}} 11 | -------------------------------------------------------------------------------- /EBook.Web/src/redux/reducers/ebooks.js: -------------------------------------------------------------------------------- 1 | import { 2 | SEARCH_EBOOKS_REQUEST, 3 | SEARCH_EBOOKS_SUCCESS, 4 | SEARCH_EBOOKS_FAILURE 5 | } from '../actions/ebooks'; 6 | 7 | 8 | const initialState = { 9 | eBooks: [], 10 | isFetched: false, 11 | error: false 12 | } 13 | 14 | const reducer = (state = initialState, action) => { 15 | switch(action.type) { 16 | case SEARCH_EBOOKS_REQUEST: 17 | return { 18 | ...state, 19 | isFetched: false, 20 | error: false 21 | } 22 | case SEARCH_EBOOKS_SUCCESS: 23 | return { 24 | ...state, 25 | eBooks: action.payload.items, 26 | isFetched: true, 27 | error: false 28 | } 29 | case SEARCH_EBOOKS_FAILURE: 30 | return { 31 | ...state, 32 | error: true 33 | } 34 | default: 35 | return { 36 | ...state 37 | } 38 | } 39 | }; 40 | 41 | export default reducer; 42 | 43 | export const eBooksSelector = state => state.eBooksReducer.eBooks; 44 | export const isFetchedSelector = state => state.eBooksReducer.isFetched; 45 | export const errorSelector = state => state.eBooksReducer.error; -------------------------------------------------------------------------------- /EBook.API/Elasticsearch/Index/DefaultIndexConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Elasticsearch.Index 2 | { 3 | using EBook.Domain; 4 | using Microsoft.Extensions.Configuration; 5 | using Nest; 6 | 7 | public static class DefaultIndexConfiguration 8 | { 9 | public static ConnectionSettings ConfigureDefaultTypeIndexes(this ConnectionSettings settings, IConfiguration config) 10 | { 11 | var elasticsearchConfigSection = config.GetSection(ConfigurationSettings.ElasticsearchSectionKey); 12 | var defaultIndex = elasticsearchConfigSection.GetValue(ConfigurationSettings.DefaultIndexKey); 13 | var eBooksIndex = elasticsearchConfigSection.GetValue(ConfigurationSettings.EBooksIndexKey); 14 | var usersIndex = elasticsearchConfigSection.GetValue(ConfigurationSettings.UsersIndexKey); 15 | 16 | settings.DefaultIndex(defaultIndex); 17 | settings.DefaultMappingFor(m => m.IndexName(eBooksIndex)); 18 | settings.DefaultMappingFor(m => m.IndexName(usersIndex)); 19 | 20 | return settings; 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /EBook.Web/src/redux/reducers/categories.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_CATEGORIES_REQUEST, 3 | GET_CATEGORIES_SUCCESS, 4 | GET_CATEGORIES_FAILURE 5 | } from '../actions/categories'; 6 | 7 | const initialState = { 8 | categories: [], 9 | isFetched: false, 10 | error: false 11 | }; 12 | 13 | const reducer = (state = initialState, action) => { 14 | switch (action.type) { 15 | case GET_CATEGORIES_REQUEST: 16 | return { 17 | ...state, 18 | isFetched: false, 19 | error: false 20 | } 21 | 22 | case GET_CATEGORIES_SUCCESS: 23 | return { 24 | ...state, 25 | categories: action.payload, 26 | isFetched: true, 27 | error: false 28 | } 29 | 30 | case GET_CATEGORIES_FAILURE: 31 | return { 32 | ...state, 33 | error: true // assign object here? 34 | } 35 | 36 | default: 37 | return { 38 | ...state 39 | } 40 | } 41 | } 42 | 43 | export const categoriesSelector = state => state.categoriesReducer.categories; 44 | export const isFetchedSelector = state => state.categoriesReducer.isFetched; 45 | export const errorSelector = state => state.categoriesReducer.error; 46 | 47 | export default reducer; -------------------------------------------------------------------------------- /EBook.API/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Controllers 2 | { 3 | using EBook.API.Models.Dto; 4 | using EBook.Services.Contracts; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System.Threading.Tasks; 7 | 8 | [ApiController] 9 | [Route("api/[controller]")] 10 | public class AuthController : ControllerBase 11 | { 12 | private readonly IAuthService _authService; 13 | private readonly ITokenService _tokenService; 14 | 15 | public AuthController(IAuthService authService, ITokenService tokenService) 16 | { 17 | _authService = authService; 18 | _tokenService = tokenService; 19 | } 20 | 21 | [Route("login")] 22 | public async Task Login([FromBody]LoginInfoDto loginInfo) 23 | { 24 | if (!ModelState.IsValid) 25 | return BadRequest("Login information is not valid."); 26 | 27 | var auth = await _authService.Authenticate(loginInfo.Username, loginInfo.Password); 28 | if (!auth) 29 | return Unauthorized(); 30 | 31 | var token = _tokenService.GenerateToken(); 32 | 33 | return Ok(token); 34 | } 35 | 36 | } 37 | } -------------------------------------------------------------------------------- /EBook.Services/Queries/HighlightSearchRequestSpecification.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Queries 2 | { 3 | using Nest; 4 | using System; 5 | 6 | public class HighlightSearchRequestSpecification : SearchRequestSpecification where T : class 7 | { 8 | private readonly SearchRequestSpecification _query; 9 | 10 | public HighlightSearchRequestSpecification(SearchRequestSpecification query) 11 | => _query = query ?? throw new ArgumentNullException($"{nameof(query)} cannot be null."); 12 | 13 | public override ISearchRequest IsSatisfiedBy() 14 | => new SearchDescriptor() 15 | .Query(q => _query 16 | .IsSatisfiedBy() 17 | .Query 18 | ) 19 | // This will highlight all queried fields 20 | .Highlight(h => h 21 | .PreTags("") 22 | .PostTags("") 23 | .FragmentSize(150) 24 | .NumberOfFragments(2) 25 | .Fields(f => f 26 | .Field("*") 27 | .ForceSource() 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /EBook.Web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ebook-search", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.9.5", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "axios": "^0.19.2", 11 | "react": "^16.13.0", 12 | "react-dom": "^16.13.0", 13 | "react-highlight-words": "^0.16.0", 14 | "react-redux": "^7.2.0", 15 | "react-router": "^5.1.2", 16 | "react-router-dom": "^5.1.2", 17 | "react-scripts": "3.4.0", 18 | "redux": "^4.0.5", 19 | "redux-api-middleware": "^3.2.0", 20 | "redux-logger": "^3.0.6", 21 | "redux-thunk": "^2.3.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /EBook.API/Models/Dto/BookDto.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Models.Dto 2 | { 3 | using EBook.Services.Contracts.Query; 4 | using Microsoft.AspNetCore.Http; 5 | using System.Collections.Generic; 6 | 7 | public class BookDto 8 | { 9 | public int Id { get; set; } 10 | public string Title { get; set; } 11 | public string Author { get; set; } 12 | public string Keywords { get; set; } 13 | public int PublicationYear { get; set; } 14 | 15 | public FileDto File { get; set; } 16 | public CategoryDto Category { get; set; } 17 | public LanguageDto Language { get; set; } 18 | } 19 | 20 | public class HighlightableBookDto : BookDto, IHighlightable 21 | { 22 | public IReadOnlyDictionary> Highlights { get; set; } 23 | } 24 | 25 | // for testing pdf upload - not the actual model 26 | public class PostBookDto 27 | { 28 | public int Id { get; set; } 29 | public string Title { get; set; } 30 | public string Author { get; set; } 31 | public string Keywords { get; set; } 32 | public int PublicationYear { get; set; } 33 | public CategoryDto Category { get; set; } 34 | public LanguageDto Language { get; set; } 35 | public IFormFile File { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /EBook.API/EBook.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | InProcess 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /scripts/ebooks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "The Great Gatsby", 4 | "author": "F. Scott Fitzgerald", 5 | "category": { 6 | "id": 1, 7 | "name": "Drama" 8 | }, 9 | "keywords": "the great gatsby classic novel", 10 | "language": { 11 | "id": 1, 12 | "name": "English" 13 | } 14 | }, 15 | { 16 | "title": "Catch-22", 17 | "author": "Joseph Heller", 18 | "category": { 19 | "id": 2, 20 | "name": "War" 21 | }, 22 | "keywords": "war catch 22 comedy drama", 23 | "language": { 24 | "id": 1, 25 | "name": "English" 26 | } 27 | }, 28 | { 29 | "title": "The Lord of the Rings", 30 | "author": "J.R.R. Tolkien", 31 | "category": { 32 | "id": 3, 33 | "name": "Fantasy" 34 | }, 35 | "keywords": "lord of the rings fantasy gandalf classic", 36 | "language": { 37 | "id": 1, 38 | "name": "English" 39 | } 40 | }, 41 | { 42 | "title": "Clash of Kings", 43 | "author": "George R.R. Martin", 44 | "category": { 45 | "id": 3, 46 | "name": "Fantasy" 47 | }, 48 | "keywords": "song of ice and fire fantasy clash of kings winter is coming", 49 | "language": { 50 | "id": 1, 51 | "name": "English" 52 | } 53 | }, 54 | { 55 | "title": "The War and Peace", 56 | "author": "Leo Tolstoy", 57 | "category": { 58 | "id": 1, 59 | "name": "Drama" 60 | }, 61 | "keywords": "war and peace russian author", 62 | "language": { 63 | "id": 1, 64 | "name": "English" 65 | } 66 | } 67 | ] -------------------------------------------------------------------------------- /EBook.Services/EBookRepositoryService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services 2 | { 3 | using EBook.Domain; 4 | using EBook.Persistence.Contracts; 5 | using EBook.Services.Contracts; 6 | using EBook.Services.Contracts.Convert; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | 11 | public class EBookRepositoryService : IEBookRepositoryService 12 | { 13 | private IEBooksRepository _eBookRepository; 14 | private IFilePdfConverter _pdfConverter; 15 | 16 | public EBookRepositoryService(IEBooksRepository eBooksRepository, IFilePdfConverter pdfConverter) 17 | { 18 | _eBookRepository = eBooksRepository; 19 | _pdfConverter = pdfConverter; 20 | } 21 | 22 | public async Task Create(Book book) 23 | { 24 | var file = await _pdfConverter.Convert(book.File.Path); 25 | 26 | book.File.Content = file.Content; 27 | var createdBook = await _eBookRepository.Create(book); 28 | 29 | return createdBook; 30 | } 31 | 32 | public Task Delete(int id) 33 | { 34 | throw new NotImplementedException(); 35 | } 36 | 37 | public Task Get(int id) 38 | { 39 | throw new NotImplementedException(); 40 | } 41 | 42 | public Task> GetAll() 43 | { 44 | throw new NotImplementedException(); 45 | } 46 | 47 | public Task Update(int id, Book book) 48 | { 49 | throw new NotImplementedException(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /EBook.Services/Convert/PdfConverter.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services.Convert 2 | { 3 | using EBook.Domain; 4 | using EBook.Services.Contracts.Convert; 5 | using iText.Kernel.Pdf; 6 | using iText.Kernel.Pdf.Canvas.Parser; 7 | using iText.Kernel.Pdf.Canvas.Parser.Listener; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | public class PdfConverter : IFilePdfConverter 12 | { 13 | public Task Convert(string path) 14 | { 15 | return Task.Run(() => 16 | { 17 | var file = new File 18 | { 19 | Path = path, 20 | Mime = "application/pdf" 21 | }; 22 | 23 | using (var document = new PdfDocument(new PdfReader(path))) 24 | { 25 | int numOfPages = document.GetNumberOfPages(); 26 | 27 | var listener = new FilteredEventListener(); 28 | var extractionStrategy = listener 29 | .AttachEventListener(new LocationTextExtractionStrategy()); 30 | 31 | var processor = new PdfCanvasProcessor(listener); 32 | var content = new StringBuilder(); 33 | 34 | for (int i = 1; i <= numOfPages; i++) 35 | { 36 | processor.ProcessPageContent(document.GetPage(i)); 37 | content.Append(extractionStrategy.GetResultantText()); 38 | 39 | processor.Reset(); 40 | } 41 | 42 | file.Content = content.ToString(); 43 | } 44 | 45 | return file; 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /EBook.API/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API 2 | { 3 | using AutoMapper; 4 | using Extensions; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddAutoMapper(typeof(Startup)); 24 | 25 | services.AddMvc(o => o.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_3_0); 26 | 27 | // custom service configuration 28 | services.ConfigureCors(); 29 | services.ConfigureAuthorization(Configuration); 30 | services.ConfigureElasticsearch(Configuration); 31 | services.AddRepositories(); 32 | services.AddServices(); 33 | } 34 | 35 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 36 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 37 | { 38 | if (env.IsDevelopment()) 39 | { 40 | app.UseDeveloperExceptionPage(); 41 | } 42 | 43 | app.UseCors("CorsPolicy"); 44 | app.UseStaticFiles(); 45 | app.UseMvc(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /EBook.Web/src/components/ebook/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Card from '@material-ui/core/Card'; 3 | import CardActions from '@material-ui/core/CardActions'; 4 | import CardContent from '@material-ui/core/CardContent'; 5 | import Button from '@material-ui/core/Button'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import Chip from '@material-ui/core/Chip'; 8 | import Highlighter from 'react-highlight-words'; 9 | 10 | import './index.css'; 11 | 12 | const EBook = ({ eBook }) => { 13 | const { author, title, highlights, keywords } = eBook; 14 | 15 | console.log(keywords); 16 | 17 | const highlightedContent = highlights["file.content"][0]; 18 | const content = highlightedContent.replace(/<\/?strong>/g, ''); 19 | const highlightTerms = highlightedContent.match(/(.*?)<\/strong>/g).map(val => 20 | val.replace(/<\/?strong>/g,'')); 21 | 22 | return ( 23 | 24 | 25 | 26 | {author} 27 | 28 | 29 | {title} 30 | 31 | 32 | 37 | 38 | { 39 | keywords.split(' ').map(keyword => 40 | ) 41 | } 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default EBook; 51 | -------------------------------------------------------------------------------- /EBook.API/Extensions/ElasticsearchExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Extensions 2 | { 3 | using EBook.API.Elasticsearch.Index; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Nest; 7 | using System; 8 | using System.Text; 9 | 10 | public static class ElasticsearchExtensions 11 | { 12 | public static void ConfigureElasticsearch(this IServiceCollection services, IConfiguration config) 13 | { 14 | var url = config 15 | .GetSection(ConfigurationSettings.ElasticsearchSectionKey) 16 | .GetValue(ConfigurationSettings.UrlKey); 17 | 18 | var settings = new ConnectionSettings(new Uri(url)) 19 | .ConfigureDefaultTypeIndexes(config) 20 | // testing 21 | .OnRequestCompleted((handler) => 22 | { 23 | if(handler.RequestBodyInBytes != null) 24 | { 25 | // logger here 26 | Console.WriteLine($"{handler.HttpMethod} {handler.Uri}"); 27 | Console.WriteLine($"{Encoding.UTF8.GetString(handler.RequestBodyInBytes)}"); 28 | } 29 | 30 | if (handler.ResponseBodyInBytes != null) 31 | { 32 | // logger here 33 | Console.WriteLine($"{handler.HttpMethod} {handler.Uri}"); 34 | Console.WriteLine($"{Encoding.UTF8.GetString(handler.ResponseBodyInBytes)}"); 35 | } 36 | }); 37 | 38 | var client = new ElasticClient(settings) 39 | .ConfigureMappings(config); 40 | 41 | services.AddSingleton(client); 42 | } 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /EBook.Services/TokenService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services 2 | { 3 | using EBook.Services.Contracts; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.IdentityModel.Tokens; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IdentityModel.Tokens.Jwt; 9 | using System.Security.Claims; 10 | using System.Text; 11 | 12 | public class TokenService : ITokenService 13 | { 14 | private const string JwtSectionKey = "jwt"; 15 | private const string JwtSecretKey = "secret"; 16 | private const string JwtIssuerKey = "issuer"; 17 | private const string JwtAudienceKey = "audience"; 18 | 19 | private readonly IConfiguration _config; 20 | 21 | public TokenService(IConfiguration config) 22 | => _config = config; 23 | 24 | public string GenerateToken() 25 | { 26 | var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GetJwtSecretKey())); 27 | var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); 28 | 29 | var tokenOptions = new JwtSecurityToken( 30 | issuer: GetJwtIssuer(), 31 | claims: new List(), 32 | audience: GetJwtAudience(), 33 | expires: DateTime.Now.AddDays(7), 34 | signingCredentials: signinCredentials 35 | ); 36 | 37 | var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions); 38 | return tokenString; 39 | } 40 | 41 | private string GetJwtSecretKey() => _config.GetSection(JwtSectionKey).GetValue(JwtSecretKey); 42 | private string GetJwtIssuer() => _config.GetSection(JwtSectionKey).GetValue(JwtIssuerKey); 43 | private string GetJwtAudience() => _config.GetSection(JwtSectionKey).GetValue(JwtAudienceKey); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /EBook.Persistence/EBooksRepository.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Persistence 2 | { 3 | using EBook.Domain; 4 | using EBook.Persistence.Contracts; 5 | using Nest; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | 10 | public class EBooksRepository : IEBooksRepository 11 | { 12 | private readonly IElasticClient _client; 13 | 14 | public EBooksRepository(IElasticClient client) 15 | { 16 | _client = client; 17 | } 18 | 19 | public async Task> Search(ISearchRequest query) 20 | { 21 | var response = await _client.SearchAsync(query); 22 | 23 | if (!response.IsValid) 24 | throw new Exception(response.DebugInformation, response.OriginalException); 25 | 26 | return response; 27 | } 28 | 29 | public async Task Create(Book entity) 30 | { 31 | var response = await _client.CreateAsync( 32 | entity, 33 | s => s 34 | ); 35 | 36 | if (!response.IsValid) 37 | throw new Exception(response.DebugInformation, response.OriginalException); 38 | 39 | // @TODO: 40 | // - Check how to get newly created ebook 41 | return entity; 42 | } 43 | 44 | public Task Delete(int primaryKey) 45 | { 46 | throw new NotImplementedException(); 47 | } 48 | 49 | public Task Get(int primaryKey) 50 | { 51 | throw new NotImplementedException(); 52 | } 53 | 54 | public Task> GetAll() 55 | { 56 | throw new NotImplementedException(); 57 | } 58 | 59 | 60 | public Task Update(int primaryKey, Book entity) 61 | { 62 | throw new NotImplementedException(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /EBook.Web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | React App 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /EBook.API/Controllers/LanguagesController.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Controllers 2 | { 3 | using AutoMapper; 4 | using EBook.API.Models; 5 | using EBook.API.Models.Dto; 6 | using EBook.Services.Contracts; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | 11 | [ApiController] 12 | [Route("api/[controller]")] 13 | public class LanguagesController : ControllerBase 14 | { 15 | private readonly List _languages; 16 | 17 | private readonly IMapper _mapper; 18 | private readonly IEBooksSearchService _eBooksService; 19 | 20 | public LanguagesController(IMapper mapper, IEBooksSearchService eBooksService) 21 | { 22 | _mapper = mapper; 23 | _eBooksService = eBooksService; 24 | 25 | _languages = new List 26 | { 27 | new LanguageDto { Id = 1, Name = "English" }, 28 | new LanguageDto { Id = 2, Name = "English" } 29 | }; 30 | } 31 | 32 | [HttpGet] 33 | public async Task Get() 34 | { 35 | foreach (var lang in _languages) 36 | lang.EBooks = _mapper.Map>( 37 | await _eBooksService.Search( 38 | new EBookSearchOptions { Language = lang.Name } 39 | ) 40 | ); 41 | 42 | return Ok(_languages); 43 | } 44 | 45 | [HttpGet] 46 | [Route("{id}")] 47 | public async Task Get(int id) 48 | { 49 | var languages = _languages.FindAll(lang => lang.Id == id); 50 | 51 | foreach(var lang in languages) 52 | lang.EBooks = _mapper.Map>( 53 | await _eBooksService.Search( 54 | new EBookSearchOptions { Language = lang.Name } 55 | ) 56 | ); 57 | 58 | return Ok(languages); 59 | } 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /EBook.API/Controllers/CategoriesController.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Controllers 2 | { 3 | using AutoMapper; 4 | using EBook.API.Models; 5 | using EBook.API.Models.Dto; 6 | using EBook.Services.Contracts; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | 11 | [ApiController] 12 | [Route("api/[controller]")] 13 | public class CategoriesController : ControllerBase 14 | { 15 | private readonly List _categories; 16 | 17 | private readonly IMapper _mapper; 18 | private readonly IEBooksSearchService _eBooksService; 19 | 20 | public CategoriesController(IMapper mapper, IEBooksSearchService eBooksService) 21 | { 22 | _mapper = mapper; 23 | _eBooksService = eBooksService; 24 | 25 | _categories = new List 26 | { 27 | new CategoryDto { Id = 1, Name = "Drama" }, 28 | new CategoryDto { Id = 2, Name = "War" }, 29 | new CategoryDto { Id = 3, Name = "Fantasy" } 30 | }; 31 | } 32 | 33 | [HttpGet] 34 | public async Task Get() 35 | { 36 | foreach (var category in _categories) 37 | { 38 | var searchResult = await _eBooksService.Search( 39 | new EBookSearchOptions { Category = category.Name } 40 | ); 41 | 42 | category.EBooks = _mapper.Map>(searchResult.Items); 43 | } 44 | return Ok(_categories); 45 | } 46 | 47 | [HttpGet] 48 | [Route("{id}")] 49 | public async Task Get(int id) 50 | { 51 | var categories = _categories.FindAll(cat => cat.Id == id); 52 | 53 | foreach (var category in categories) 54 | category.EBooks = _mapper.Map>( 55 | await _eBooksService.Search( 56 | new EBookSearchOptions { Category = category.Name } 57 | ) 58 | ); 59 | 60 | return Ok(categories); 61 | } 62 | 63 | } 64 | } -------------------------------------------------------------------------------- /EBook.API/Elasticsearch/Mappings/EBookMapping.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Elasticsearch.Mappings 2 | { 3 | using EBook.Domain; 4 | using Microsoft.Extensions.Configuration; 5 | using Nest; 6 | 7 | public static class EBookMapping 8 | { 9 | public static IElasticClient ConfigureEBookMapping(this IElasticClient client, IConfiguration config) 10 | { 11 | var eBooksIndex = config 12 | .GetSection(ConfigurationSettings.ElasticsearchSectionKey) 13 | .GetValue(ConfigurationSettings.EBooksIndexKey); 14 | 15 | client.Indices.Create(eBooksIndex, c => c 16 | .Settings(s => s 17 | .Analysis(a => a 18 | .Analyzers(aa => aa 19 | .Standard("standard_english", sa => sa 20 | .StopWords("_english_") 21 | ) 22 | ) 23 | ) 24 | ) 25 | .Map(m => m 26 | .AutoMap() 27 | .Properties(p => p 28 | .Text(t => t 29 | // should we exclude 'The', 'and' etc. from ebook title? 30 | .Name(n => n.Title) 31 | .Analyzer("standard_english") 32 | ) 33 | .Text(t => t 34 | .Name(n => n.Author) 35 | .Analyzer("standard_english") 36 | ) 37 | .Text(t => t 38 | .Name(n => n.Keywords) 39 | .Analyzer("standard_english") 40 | ) 41 | .Number(n => n 42 | .Name(na => na.Id) 43 | .Type(NumberType.Integer) 44 | ) 45 | .Text(t => t 46 | .Name(n => n.File.Content) 47 | .Analyzer("standard_english") 48 | ) 49 | ) 50 | ) 51 | ); 52 | 53 | return client; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /EBook.Web/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EBook.API/Mapper/Profiles/EBookProfile.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Mapper.Profiles 2 | { 3 | using AutoMapper; 4 | using EBook.API.Models.Dto; 5 | using EBook.Domain; 6 | using EBook.Services.Models; 7 | 8 | public class EBookProfile : Profile 9 | { 10 | public EBookProfile() 11 | { 12 | CreateMap() 13 | .ForPath(dest => dest.File.Filename, opts => opts.MapFrom(src => src.File.Filename)) 14 | .ForPath(dest => dest.File.Mime, opts => opts.MapFrom(src => src.File.Mime)) 15 | .ForPath(dest => dest.File.Path, opts => opts.MapFrom(src => src.File.Path)) 16 | .ReverseMap(); 17 | 18 | CreateMap() 19 | .ForPath(dest => dest.File.Filename, opts => opts.MapFrom(src => src.File.FileName)) 20 | .ForPath(dest => dest.File.Mime, opts => opts.MapFrom(src => src.File.ContentType)); 21 | 22 | CreateMap() 23 | .ForPath(dest => dest.File.Filename, opts => opts.MapFrom(src => src.File.Filename)) 24 | .ForPath(dest => dest.File.Mime, opts => opts.MapFrom(src => src.File.Mime)) 25 | .ForPath(dest => dest.File.Path, opts => opts.MapFrom(src => src.File.Path)) 26 | .ForPath(dest => dest.Highlights, opts => opts.MapFrom(src => (src as HighlightableBookDto).Highlights)) 27 | .ReverseMap(); 28 | 29 | CreateMap() 30 | .ForPath(dest => dest.File.Filename, opts => opts.MapFrom(src => src.File.Filename)) 31 | .ForPath(dest => dest.File.Mime, opts => opts.MapFrom(src => src.File.Mime)) 32 | .ForPath(dest => dest.File.Path, opts => opts.MapFrom(src => src.File.Path)) 33 | .ForPath(dest => dest.Highlights, opts => opts.MapFrom(src => (src as HighlightableEBook).Highlights)) 34 | .ReverseMap(); 35 | 36 | CreateMap() 37 | .ForPath(dest => dest.File.Filename, opts => opts.MapFrom(src => src.File.Filename)) 38 | .ForPath(dest => dest.File.Mime, opts => opts.MapFrom(src => src.File.Mime)) 39 | .ForPath(dest => dest.File.Path, opts => opts.MapFrom(src => src.File.Path)) 40 | .ForPath(dest => dest.Highlights, opts => opts.MapFrom(src => (src as HighlightableEBook).Highlights)) 41 | .ReverseMap(); 42 | 43 | CreateMap() 44 | .ForPath(dest => dest.File.Filename, opts => opts.MapFrom(src => src.File.Filename)) 45 | .ForPath(dest => dest.File.Mime, opts => opts.MapFrom(src => src.File.Mime)) 46 | .ForPath(dest => dest.File.Path, opts => opts.MapFrom(src => src.File.Path)) 47 | .ReverseMap(); 48 | 49 | CreateMap().ReverseMap(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /EBook.Persistence/UsersRepository.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Persistence 2 | { 3 | using EBook.Domain; 4 | using EBook.Persistence.Contracts; 5 | using Nest; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | public class UsersRepository : IUsersRepository 12 | { 13 | private readonly IElasticClient _client; 14 | 15 | public UsersRepository(IElasticClient client) 16 | => _client = client; 17 | 18 | public async Task Create(User entity) 19 | { 20 | var response = await _client.CreateAsync( 21 | entity, 22 | s => s 23 | ); 24 | 25 | if (!response.IsValid) 26 | throw new Exception(response.DebugInformation, response.OriginalException); 27 | 28 | // @TODO: 29 | // - Check how to get newly created user 30 | return entity; 31 | } 32 | 33 | public Task> GetAll() 34 | { 35 | throw new NotImplementedException(); 36 | } 37 | 38 | public async Task Get(int primaryKey) 39 | { 40 | var response = await _client.SearchAsync(s => s 41 | .Query(q => q 42 | .Bool(b => b 43 | .Must(mu => mu 44 | .Match(m => m 45 | .Field(f => f.Id) 46 | .Query(primaryKey.ToString()) 47 | ) 48 | ) 49 | ) 50 | ) 51 | ); 52 | 53 | if (!response.IsValid) 54 | throw new Exception(response.DebugInformation, response.OriginalException); 55 | 56 | return response.Documents.FirstOrDefault(); 57 | } 58 | 59 | // @Question: 60 | // - username unique? 61 | public async Task GetByUsername(string username) 62 | { 63 | var response = await _client.SearchAsync(s => s 64 | .Query(q => q 65 | .Bool(b => b 66 | .Must(mu => mu 67 | .Match(m => m 68 | .Field(f => f.Username) 69 | .Query(username) 70 | ) 71 | ) 72 | ) 73 | ) 74 | ); 75 | 76 | if (!response.IsValid) 77 | throw new Exception(response.DebugInformation, response.OriginalException); 78 | 79 | return response.Documents.FirstOrDefault(); 80 | } 81 | 82 | public Task Update(int primaryKey, User entity) 83 | { 84 | throw new NotImplementedException(); 85 | } 86 | 87 | public Task Delete(int primaryKey) 88 | { 89 | throw new NotImplementedException(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /EBook.API/Controllers/EBooksController.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Controllers 2 | { 3 | using AutoMapper; 4 | using EBook.API.Models; 5 | using EBook.API.Models.Dto; 6 | using EBook.Domain; 7 | using EBook.Services.Contracts; 8 | using Microsoft.AspNetCore.Mvc; 9 | using System.IO; 10 | using System.Threading.Tasks; 11 | 12 | [ApiController] 13 | [Route("api/[controller]")] 14 | public class EBooksController : ControllerBase 15 | { 16 | private readonly IMapper _mapper; 17 | private readonly IEBookServicesWrapper _eBookServices; 18 | 19 | public EBooksController(IMapper mapper, IEBookServicesWrapper eBookServices) 20 | { 21 | _mapper = mapper; 22 | _eBookServices = eBookServices; 23 | } 24 | 25 | [HttpPost] 26 | public async Task Post([FromForm]PostBookDto model) 27 | { 28 | if (!ModelState.IsValid) 29 | return BadRequest(); 30 | 31 | // @TODO: 32 | // - move this logic elsewhere 33 | var filePath = $"{this.Request.Scheme}://{this.Request.Host}/ebooks/{model.File.FileName.Replace(" ", "").Trim()}"; 34 | var serverFilePath = Path.Combine("wwwroot/ebooks", model.File.FileName); 35 | 36 | using (var fileStream = new FileStream(serverFilePath, FileMode.Create)) 37 | { 38 | model.File.CopyTo(fileStream); 39 | } 40 | 41 | var book = _mapper.Map(model); 42 | book.File.Path = serverFilePath; 43 | 44 | var createdBook = await _eBookServices.RepositoryService.Create(book); 45 | 46 | var bookDto = _mapper.Map(createdBook); 47 | bookDto.File.Path = filePath; 48 | 49 | return Ok(bookDto); 50 | } 51 | 52 | [HttpGet] 53 | [Route("filter")] 54 | public async Task Filter([FromQuery]EBookFilterOptions options, [FromQuery]bool fuzzy = false) 55 | { 56 | if (options == null) 57 | return BadRequest(); 58 | 59 | // @TODO: 60 | // - Replace if statement with a map 61 | var books = fuzzy 62 | ? await _eBookServices.FilterService.FuzzyFilter(options) 63 | : await _eBookServices.FilterService.Filter(options); 64 | 65 | var booksDto = _mapper.Map(books); 66 | 67 | return Ok(booksDto); 68 | } 69 | 70 | [HttpGet] 71 | [Route("search")] 72 | public async Task Search([FromQuery]EBookSearchOptions options, [FromQuery]bool fuzzy = false) 73 | { 74 | if (options == null) 75 | return BadRequest(); 76 | 77 | // @TODO: 78 | // - Replace if statement with a map 79 | var books = fuzzy 80 | ? await _eBookServices.SearchService.FuzzySearch(options) 81 | : await _eBookServices.SearchService.Search(options); 82 | 83 | var booksDto = _mapper.Map(books); 84 | 85 | return Ok(booksDto); 86 | } 87 | 88 | } 89 | } -------------------------------------------------------------------------------- /EBook.API/Extensions/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.API.Extensions 2 | { 3 | using EBook.Persistence; 4 | using EBook.Persistence.Contracts; 5 | using EBook.Services; 6 | using EBook.Services.Contracts; 7 | using EBook.Services.Contracts.Convert; 8 | using EBook.Services.Convert; 9 | using Microsoft.AspNetCore.Authentication.JwtBearer; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.IdentityModel.Tokens; 14 | using System.Text; 15 | 16 | public static class ServiceExtensions 17 | { 18 | 19 | public static void AddServices(this IServiceCollection services) 20 | { 21 | services.AddScoped(); 22 | services.AddScoped(); 23 | services.AddScoped(); 24 | services.AddScoped(); 25 | services.AddScoped(); 26 | services.AddScoped(); 27 | 28 | services.AddScoped(); 29 | } 30 | 31 | public static void AddRepositories(this IServiceCollection services) 32 | { 33 | services.AddScoped(); 34 | services.AddScoped(); 35 | } 36 | 37 | public static void ConfigureCors(this IServiceCollection services) 38 | => services.AddCors(options => 39 | { 40 | options.AddPolicy("CorsPolicy", 41 | builder => builder.AllowAnyOrigin() 42 | .AllowAnyMethod() 43 | .AllowAnyHeader()); 44 | }); 45 | 46 | public static void ConfigureAuthorization(this IServiceCollection services, IConfiguration config) 47 | { 48 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 49 | .AddJwtBearer(options => 50 | { 51 | options.TokenValidationParameters = new TokenValidationParameters 52 | { 53 | ValidateIssuer = true, 54 | ValidIssuer = config 55 | .GetSection(ConfigurationSettings.JwtConfigKey) 56 | .GetValue(ConfigurationSettings.IssuerConfigKey), 57 | 58 | ValidateAudience = true, 59 | ValidAudience = config 60 | .GetSection(ConfigurationSettings.JwtConfigKey) 61 | .GetValue(ConfigurationSettings.AudienceConfigKey), 62 | 63 | ValidateLifetime = true, 64 | 65 | ValidateIssuerSigningKey = true, 66 | IssuerSigningKey = new SymmetricSecurityKey( 67 | Encoding.UTF8.GetBytes(config 68 | .GetSection(ConfigurationSettings.JwtConfigKey) 69 | .GetValue(ConfigurationSettings.SecretConfigKey)) 70 | ) 71 | }; 72 | 73 | options.Events = new JwtBearerEvents 74 | { 75 | OnChallenge = context => 76 | { 77 | context.HandleResponse(); 78 | 79 | context.Response.ContentType = "application/json"; 80 | context.Response.StatusCode = StatusCodes.Status401Unauthorized; 81 | 82 | return context.Response.WriteAsync("Unauthorized"); 83 | } 84 | }; 85 | }); 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # E-Book Repository API 2 | 3 | ## Content search example 4 | 5 | ![Content Search](./docs/content.png) 6 | 7 | ## Search 8 | 9 | Search uses Boolean *AND* Query for all given query options. 10 | If your search is based on multiple fields, it will only result in a single matching e-book result. 11 | 12 | ### Search functions: 13 | - Match search: 14 | `Search(IEBookSearchOptions options)` 15 | 16 | - Fuzzy search: 17 | `FuzzySearch(IEBookSearchOptions options)` 18 | 19 | > **_Note:_** 20 | > Additional search function on `feature/full-search` branch: 21 | > - Match search: 22 | > `SearchByTitle(string title)` 23 | > `SearchByAuthor(string author)` 24 | > `SearchByKeywords(string keywords)` 25 | > `SearchByCategory(string category)` 26 | > `SearchByLanguage(string language)` 27 | > 28 | > - Fuzzy search: 29 | > `FuzzySearchByTitle(string title)` 30 | > `FuzzySearchByAuthor(string author)` 31 | > `FuzzySearchByKeywords(string keywords)` 32 | > `FuzzySearchByCategory(string category)` 33 | > `FuzzySearchByLanguage(string language)` 34 | 35 | ### Search Options 36 | ``` 37 | public interface IEBookSearchOptions 38 | { 39 | string Title { get; set; } 40 | string Author { get; set; } 41 | string Keywords { get; set; } 42 | string Language { get; set; } 43 | string Category { get; set; } 44 | 45 | int Page { get; set; } 46 | int Size { get; set; } 47 | } 48 | ``` 49 | 50 | ### Example search requests 51 | 52 | Without fuzzy search: 53 | `GET /api/ebooks/search?title=the great gatsby&language=english` 54 | 55 | With fuzzy search: 56 | `GET /api/ebooks/search?title=cash for kings&author=gerge&fuzzy=true` 57 | 58 | ## Filter 59 | 60 | Filter uses Boolean *OR* Query for all given query options. 61 | Your filter can be based on multiple fields and it will result in all matching e-books. 62 | 63 | ### Filter functions: 64 | - Match filter: 65 | `Filter(IEBooksFilterOptions options)` 66 | 67 | - Fuzzy filter: 68 | `FuzzyFilter(IEBooksFilterOptions options)` 69 | 70 | ### Filter Options 71 | ``` 72 | public interface IEBookFilterOptions 73 | { 74 | string Title { get; set; } 75 | string Author { get; set; } 76 | string Keywords { get; set; } 77 | string Language { get; set; } 78 | string Category { get; set; } 79 | 80 | int Page { get; set; } 81 | int Size { get; set; } 82 | } 83 | ``` 84 | 85 | ### Example filter requests: 86 | 87 | Without fuzzy filter: 88 | `GET /api/ebooks/filter?author=george&title=catch 22` 89 | 90 | With fuzzy filter: 91 | `GET /api/ebooks/filter?author=gorge&title=catc 25&fuzzy=true` 92 | 93 | ## Example response: 94 | 95 | ``` 96 | { 97 | "total": 2, 98 | "page": 1, 99 | "size": 2, 100 | "items": [ 101 | { 102 | "highlights": { 103 | "file.content": [ 104 | "Ut ac dolor\nLorem ipsum dolor sit amet, consectetur adipiscing Lorem ipsum dolor sit amet, consectetur adipiscing Lorem ipsum dolor sit amet, consectetur", 105 | "Ut ac dolor\nLorem ipsum dolor sit amet, consectetur adipiscing Lorem ipsum dolor sit amet, consectetur adipiscing Lorem ipsum dolor sit amet, consectetur" 106 | ] 107 | }, 108 | "id": 2, 109 | "title": "Example book", 110 | "author": "Example author", 111 | "keywords": "example dummy content test", 112 | "publicationYear": 2020, 113 | "file": { 114 | "path": "path-to-pdf-location\\example.pdf", 115 | "mime": "application/pdf", 116 | "filename": "example.pdf" 117 | }, 118 | "category": { 119 | "id": 6, 120 | "name": "Tech" 121 | }, 122 | "language": { 123 | "id": 1, 124 | "name": "English" 125 | } 126 | }, 127 | ... other items ... 128 | } 129 | ``` -------------------------------------------------------------------------------- /EBook.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29728.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EBook.API", "EBook.API\EBook.API.csproj", "{11AE78CC-9749-430A-AC76-FC5C06ABCE5E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EBook.Domain", "EBook.Domain\EBook.Domain.csproj", "{14BD7CE5-BEFB-4D0C-A270-3D0139EF5DC0}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EBook.Services.Contracts", "EBook.Services.Contracts\EBook.Services.Contracts.csproj", "{976FB9EF-1C1F-45D8-A671-24C8CF0363A8}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EBook.Persistence.Contracts", "EBook.Persistence.Contracts\EBook.Persistence.Contracts.csproj", "{BA148A4C-CD8A-4940-9BE3-AE8B9BD07041}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EBook.Persistence", "EBook.Persistence\EBook.Persistence.csproj", "{F601A4B0-4298-4D5A-883D-8718D4761544}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EBook.Services", "EBook.Services\EBook.Services.csproj", "{C11B2090-C185-4E8D-AD8A-09DAD4EF2AF1}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3681F8FB-D4A2-490B-8A24-673766BB6847}" 19 | ProjectSection(SolutionItems) = preProject 20 | .gitignore = .gitignore 21 | ebooks-bulk.json = ebooks-bulk.json 22 | ebooks.json = ebooks.json 23 | elastic_bulk_formatter.py = elastic_bulk_formatter.py 24 | pitanja.txt = pitanja.txt 25 | README.md = README.md 26 | EndProjectSection 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {11AE78CC-9749-430A-AC76-FC5C06ABCE5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {11AE78CC-9749-430A-AC76-FC5C06ABCE5E}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {11AE78CC-9749-430A-AC76-FC5C06ABCE5E}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {11AE78CC-9749-430A-AC76-FC5C06ABCE5E}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {14BD7CE5-BEFB-4D0C-A270-3D0139EF5DC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {14BD7CE5-BEFB-4D0C-A270-3D0139EF5DC0}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {14BD7CE5-BEFB-4D0C-A270-3D0139EF5DC0}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {14BD7CE5-BEFB-4D0C-A270-3D0139EF5DC0}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {976FB9EF-1C1F-45D8-A671-24C8CF0363A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {976FB9EF-1C1F-45D8-A671-24C8CF0363A8}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {976FB9EF-1C1F-45D8-A671-24C8CF0363A8}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {976FB9EF-1C1F-45D8-A671-24C8CF0363A8}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {BA148A4C-CD8A-4940-9BE3-AE8B9BD07041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {BA148A4C-CD8A-4940-9BE3-AE8B9BD07041}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {BA148A4C-CD8A-4940-9BE3-AE8B9BD07041}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {BA148A4C-CD8A-4940-9BE3-AE8B9BD07041}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {F601A4B0-4298-4D5A-883D-8718D4761544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {F601A4B0-4298-4D5A-883D-8718D4761544}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {F601A4B0-4298-4D5A-883D-8718D4761544}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {F601A4B0-4298-4D5A-883D-8718D4761544}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {C11B2090-C185-4E8D-AD8A-09DAD4EF2AF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {C11B2090-C185-4E8D-AD8A-09DAD4EF2AF1}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {C11B2090-C185-4E8D-AD8A-09DAD4EF2AF1}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {C11B2090-C185-4E8D-AD8A-09DAD4EF2AF1}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {C166AC3F-977F-4761-9196-97094ECAB0E5} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /EBook.Services/EBooksFilterService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services 2 | { 3 | using EBook.Domain; 4 | using EBook.Persistence.Contracts; 5 | using EBook.Services.Contracts; 6 | using EBook.Services.Contracts.Filter; 7 | using EBook.Services.Contracts.Query; 8 | using EBook.Services.Models; 9 | using EBook.Services.Queries; 10 | using EBook.Services.Queries.Fuzzy; 11 | using EBook.Services.Queries.Match; 12 | using Nest; 13 | using System; 14 | using System.Collections.Generic; 15 | using System.Linq; 16 | using System.Threading.Tasks; 17 | 18 | public class EBooksFilterService : IEBooksFilterService 19 | { 20 | private IEBooksRepository _eBooksRepository; 21 | 22 | public EBooksFilterService(IEBooksRepository eBooksRepository) 23 | => _eBooksRepository = eBooksRepository; 24 | 25 | public async Task Filter(IEBookFilterOptions options) 26 | { 27 | var filterQueries = new List>(); 28 | 29 | if (!string.IsNullOrEmpty(options.Author)) 30 | filterQueries.Add(new EBookAuthorQuery(options.Author)); 31 | 32 | if (!string.IsNullOrEmpty(options.Title)) 33 | filterQueries.Add(new EBookTitleQuery(options.Title)); 34 | 35 | if (!string.IsNullOrEmpty(options.Category)) 36 | filterQueries.Add(new EBookCategoryQuery(options.Category)); 37 | 38 | if (!string.IsNullOrEmpty(options.Language)) 39 | filterQueries.Add(new EBookLanguageQuery(options.Language)); 40 | 41 | if (!string.IsNullOrEmpty(options.Keywords)) 42 | filterQueries.Add(new EBookKeywordsQuery(options.Keywords)); 43 | 44 | var orQuery = new OrSearchRequestSpecification(filterQueries); 45 | 46 | return await Search(orQuery, options.Page, options.Size); 47 | } 48 | 49 | public async Task FuzzyFilter(IEBookFilterOptions options) 50 | { 51 | var filterQueries = new List>(); 52 | 53 | if (!string.IsNullOrEmpty(options.Author)) 54 | filterQueries.Add(new EBookAuthorFuzzyQuery(options.Author)); 55 | 56 | if (!string.IsNullOrEmpty(options.Title)) 57 | filterQueries.Add(new EBookTitleFuzzyQuery(options.Title)); 58 | 59 | if (!string.IsNullOrEmpty(options.Category)) 60 | filterQueries.Add(new EBookCategoryFuzzyQuery(options.Category)); 61 | 62 | if (!string.IsNullOrEmpty(options.Language)) 63 | filterQueries.Add(new EBookLanguageFuzzyQuery(options.Language)); 64 | 65 | if (!string.IsNullOrEmpty(options.Keywords)) 66 | filterQueries.Add(new EBookKeywordsFuzzyQuery(options.Keywords)); 67 | 68 | var orQuery = new OrSearchRequestSpecification(filterQueries); 69 | 70 | return await Search(orQuery, options.Page, options.Size); 71 | } 72 | 73 | private async Task Search(SearchRequestSpecification query, int page, int size) 74 | { 75 | try 76 | { 77 | var highlightQuery = new HighlightSearchRequestSpecification(query); 78 | var paginationQuery = new PaginatedSearchRequestSpecification(highlightQuery, page, size); 79 | 80 | var response = await _eBooksRepository.Search(highlightQuery.IsSatisfiedBy()); 81 | 82 | return new EBookElasticQueryable 83 | { 84 | Items = response.Hits.Select(h => MapBook(h)), 85 | Total = (int)response.Total, 86 | Page = page + 1, 87 | Size = response.Documents.Count 88 | }; 89 | } 90 | catch (Exception) 91 | { 92 | throw; 93 | } 94 | } 95 | 96 | // @TODO: 97 | // - place this map elsewhere 98 | private HighlightableEBook MapBook(IHit hit) 99 | => new HighlightableEBook 100 | { 101 | Author = hit.Source.Author, 102 | Category = hit.Source.Category, 103 | File = hit.Source.File, 104 | Id = hit.Source.Id, 105 | Keywords = hit.Source.Keywords, 106 | Language = hit.Source.Language, 107 | PublicationYear = hit.Source.PublicationYear, 108 | Title = hit.Source.Title, 109 | Highlights = hit.Highlight 110 | }; 111 | 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /EBook.Services/EBooksSearchService.cs: -------------------------------------------------------------------------------- 1 | namespace EBook.Services 2 | { 3 | using EBook.Domain; 4 | using EBook.Persistence.Contracts; 5 | using EBook.Services.Contracts; 6 | using EBook.Services.Contracts.Query; 7 | using EBook.Services.Models; 8 | using EBook.Services.Queries; 9 | using EBook.Services.Queries.Fuzzy; 10 | using EBook.Services.Queries.Match; 11 | using Nest; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Linq; 15 | using System.Threading.Tasks; 16 | 17 | public class EBooksSearchService : IEBooksSearchService 18 | { 19 | private readonly IEBooksRepository _eBooksRepository; 20 | 21 | public EBooksSearchService(IEBooksRepository eBooksRepository) 22 | => _eBooksRepository = eBooksRepository; 23 | 24 | public async Task Search(IEBookSearchOptions options) 25 | { 26 | var filterQueries = new List>(); 27 | 28 | if (!string.IsNullOrEmpty(options.Author)) 29 | filterQueries.Add(new EBookAuthorQuery(options.Author)); 30 | 31 | if (!string.IsNullOrEmpty(options.Title)) 32 | filterQueries.Add(new EBookTitleQuery(options.Title)); 33 | 34 | if (!string.IsNullOrEmpty(options.Category)) 35 | filterQueries.Add(new EBookCategoryQuery(options.Category)); 36 | 37 | if (!string.IsNullOrEmpty(options.Language)) 38 | filterQueries.Add(new EBookLanguageQuery(options.Language)); 39 | 40 | if (!string.IsNullOrEmpty(options.Keywords)) 41 | filterQueries.Add(new EBookKeywordsQuery(options.Keywords)); 42 | 43 | if (!string.IsNullOrEmpty(options.Content)) 44 | filterQueries.Add(new EBookContentQuery(options.Content)); 45 | 46 | var andQuery = new AndSearchRequestSpecification(filterQueries); 47 | 48 | return await Search(andQuery, options.Page, options.Size); 49 | } 50 | 51 | public async Task FuzzySearch(IEBookSearchOptions options) 52 | { 53 | var filterQueries = new List>(); 54 | 55 | if (!string.IsNullOrEmpty(options.Author)) 56 | filterQueries.Add(new EBookAuthorFuzzyQuery(options.Author)); 57 | 58 | if (!string.IsNullOrEmpty(options.Title)) 59 | filterQueries.Add(new EBookTitleFuzzyQuery(options.Title)); 60 | 61 | if (!string.IsNullOrEmpty(options.Category)) 62 | filterQueries.Add(new EBookCategoryFuzzyQuery(options.Category)); 63 | 64 | if (!string.IsNullOrEmpty(options.Language)) 65 | filterQueries.Add(new EBookLanguageFuzzyQuery(options.Language)); 66 | 67 | if (!string.IsNullOrEmpty(options.Keywords)) 68 | filterQueries.Add(new EBookKeywordsFuzzyQuery(options.Keywords)); 69 | 70 | if (!string.IsNullOrEmpty(options.Content)) 71 | filterQueries.Add(new EBookContentFuzzyQuery(options.Content)); 72 | 73 | var andQuery = new AndSearchRequestSpecification(filterQueries); 74 | return await Search(andQuery, options.Page, options.Size); 75 | } 76 | 77 | private async Task Search(SearchRequestSpecification query, int page, int size) 78 | { 79 | try 80 | { 81 | var highlightQuery = new HighlightSearchRequestSpecification(query); 82 | var paginationQuery = new PaginatedSearchRequestSpecification(highlightQuery, page, size); 83 | 84 | var response = await _eBooksRepository.Search(highlightQuery.IsSatisfiedBy()); 85 | 86 | return new EBookElasticQueryable 87 | { 88 | Items = response.Hits.Select(h => MapBook(h)), 89 | Total = (int)response.Total, 90 | Page = page + 1, 91 | Size = response.Documents.Count 92 | }; 93 | } 94 | catch (Exception) 95 | { 96 | throw; 97 | } 98 | } 99 | 100 | // @TODO: 101 | // - DRY 102 | // - Place this map elsewhere 103 | private HighlightableEBook MapBook(IHit hit) 104 | => new HighlightableEBook 105 | { 106 | Author = hit.Source.Author, 107 | Category = hit.Source.Category, 108 | File = hit.Source.File, 109 | Id = hit.Source.Id, 110 | Keywords = hit.Source.Keywords, 111 | Language = hit.Source.Language, 112 | PublicationYear = hit.Source.PublicationYear, 113 | Title = hit.Source.Title, 114 | Highlights = hit.Highlight 115 | }; 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /EBook.Web/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | wwwroot/ 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # StyleCop 66 | StyleCopReport.xml 67 | 68 | # Files built by Visual Studio 69 | *_i.c 70 | *_p.c 71 | *_h.h 72 | *.ilk 73 | *.meta 74 | *.obj 75 | *.iobj 76 | *.pch 77 | *.pdb 78 | *.ipdb 79 | *.pgc 80 | *.pgd 81 | *.rsp 82 | *.sbr 83 | *.tlb 84 | *.tli 85 | *.tlh 86 | *.tmp 87 | *.tmp_proj 88 | *_wpftmp.csproj 89 | *.log 90 | *.vspscc 91 | *.vssscc 92 | .builds 93 | *.pidb 94 | *.svclog 95 | *.scc 96 | 97 | # Chutzpah Test files 98 | _Chutzpah* 99 | 100 | # Visual C++ cache files 101 | ipch/ 102 | *.aps 103 | *.ncb 104 | *.opendb 105 | *.opensdf 106 | *.sdf 107 | *.cachefile 108 | *.VC.db 109 | *.VC.VC.opendb 110 | 111 | # Visual Studio profiler 112 | *.psess 113 | *.vsp 114 | *.vspx 115 | *.sap 116 | 117 | # Visual Studio Trace Files 118 | *.e2e 119 | 120 | # TFS 2012 Local Workspace 121 | $tf/ 122 | 123 | # Guidance Automation Toolkit 124 | *.gpState 125 | 126 | # ReSharper is a .NET coding add-in 127 | _ReSharper*/ 128 | *.[Rr]e[Ss]harper 129 | *.DotSettings.user 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # NuGet Symbol Packages 189 | *.snupkg 190 | # The packages folder can be ignored because of Package Restore 191 | **/[Pp]ackages/* 192 | # except build/, which is used as an MSBuild target. 193 | !**/[Pp]ackages/build/ 194 | # Uncomment if necessary however generally it will be regenerated when needed 195 | #!**/[Pp]ackages/repositories.config 196 | # NuGet v3's project.json files produces more ignorable files 197 | *.nuget.props 198 | *.nuget.targets 199 | 200 | # Microsoft Azure Build Output 201 | csx/ 202 | *.build.csdef 203 | 204 | # Microsoft Azure Emulator 205 | ecf/ 206 | rcf/ 207 | 208 | # Windows Store app package directories and files 209 | AppPackages/ 210 | BundleArtifacts/ 211 | Package.StoreAssociation.xml 212 | _pkginfo.txt 213 | *.appx 214 | *.appxbundle 215 | *.appxupload 216 | 217 | # Visual Studio cache files 218 | # files ending in .cache can be ignored 219 | *.[Cc]ache 220 | # but keep track of directories ending in .cache 221 | !?*.[Cc]ache/ 222 | 223 | # Others 224 | ClientBin/ 225 | ~$* 226 | *~ 227 | *.dbmdl 228 | *.dbproj.schemaview 229 | *.jfm 230 | *.pfx 231 | *.publishsettings 232 | orleans.codegen.cs 233 | 234 | # Including strong name files can present a security risk 235 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 236 | #*.snk 237 | 238 | # Since there are multiple workflows, uncomment next line to ignore bower_components 239 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 240 | #bower_components/ 241 | 242 | # RIA/Silverlight projects 243 | Generated_Code/ 244 | 245 | # Backup & report files from converting an old project file 246 | # to a newer Visual Studio version. Backup files are not needed, 247 | # because we have git ;-) 248 | _UpgradeReport_Files/ 249 | Backup*/ 250 | UpgradeLog*.XML 251 | UpgradeLog*.htm 252 | ServiceFabricBackup/ 253 | *.rptproj.bak 254 | 255 | # SQL Server files 256 | *.mdf 257 | *.ldf 258 | *.ndf 259 | 260 | # Business Intelligence projects 261 | *.rdl.data 262 | *.bim.layout 263 | *.bim_*.settings 264 | *.rptproj.rsuser 265 | *- [Bb]ackup.rdl 266 | *- [Bb]ackup ([0-9]).rdl 267 | *- [Bb]ackup ([0-9][0-9]).rdl 268 | 269 | # Microsoft Fakes 270 | FakesAssemblies/ 271 | 272 | # GhostDoc plugin setting file 273 | *.GhostDoc.xml 274 | 275 | # Node.js Tools for Visual Studio 276 | .ntvs_analysis.dat 277 | node_modules/ 278 | 279 | # Visual Studio 6 build log 280 | *.plg 281 | 282 | # Visual Studio 6 workspace options file 283 | *.opt 284 | 285 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 286 | *.vbw 287 | 288 | # Visual Studio LightSwitch build output 289 | **/*.HTMLClient/GeneratedArtifacts 290 | **/*.DesktopClient/GeneratedArtifacts 291 | **/*.DesktopClient/ModelManifest.xml 292 | **/*.Server/GeneratedArtifacts 293 | **/*.Server/ModelManifest.xml 294 | _Pvt_Extensions 295 | 296 | # Paket dependency manager 297 | .paket/paket.exe 298 | paket-files/ 299 | 300 | # FAKE - F# Make 301 | .fake/ 302 | 303 | # CodeRush personal settings 304 | .cr/personal 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | 341 | # Local History for Visual Studio 342 | .localhistory/ 343 | 344 | # BeatPulse healthcheck temp database 345 | healthchecksdb 346 | 347 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 348 | MigrationBackup/ 349 | 350 | # Ionide (cross platform F# VS Code tools) working folder 351 | .ionide/ 352 | --------------------------------------------------------------------------------