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 |
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 | 
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 |
--------------------------------------------------------------------------------