Created Woodpecker CI/CD deployment
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Created Dockerfile for packing up API and Web projects as Docker image
This commit is contained in:
25
API/.dockerignore
Normal file
25
API/.dockerignore
Normal file
@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
63
API/Readme.md
Normal file
63
API/Readme.md
Normal file
@ -0,0 +1,63 @@
|
||||
# YABA: Yet Another Bookmark App
|
||||
|
||||
## YABA.API - Developer Guide
|
||||
|
||||
### Running Migrations
|
||||
When running migrations, .NET seems to be ignoring dependency injection settings. In order to get around this, be sure to add the connection string to the command like. For example, when adding the migration:
|
||||
|
||||
```
|
||||
dotnet ef migrations add InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE}
|
||||
```
|
||||
|
||||
When removing last migration:
|
||||
```
|
||||
dotnet ef migrations remove InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE}
|
||||
```
|
||||
|
||||
When applying migrations:
|
||||
```
|
||||
dotnet ef database update -p YABA.Data -s YABA.API -c YABABaseContext -- { CONNECTION_STRING_HERE }
|
||||
```
|
||||
|
||||
As per the documentation [on MSDN](https://learn.microsoft.com/en-ca/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-application-services):
|
||||
> The -- token directs dotnet ef to treat everything that follows as an argument and not try to parse them as options. Any extra arguments not used by dotnet ef are forwarded to the app.
|
||||
|
||||
### Managing secrets
|
||||
|
||||
Best practice dictates that sensitive values should never be committed to source control. To manage secrets locally, this project utilize the [secrets manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=windows).
|
||||
|
||||
To initialize a secrets manager:
|
||||
```
|
||||
dotnet user-secrets init --project YABA.API
|
||||
```
|
||||
|
||||
To set (or override) the value of a secret using a provided key:
|
||||
```
|
||||
dotnet user-secrets set "Key" "Value" --project YABA.API
|
||||
```
|
||||
```
|
||||
dotnet user-secrets set "Object:Property" "Value" --project YABA.API
|
||||
```
|
||||
|
||||
To list all secets:
|
||||
```
|
||||
dotnet user-secrets list --project YABA.API
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
To build a dockerized version of the API project:
|
||||
|
||||
```
|
||||
docker build { API_PROJECT_ROOT_SOURCE } -f { API_PROJECT_DOCKERFILE_PATH } -t { imagename:tag }
|
||||
```
|
||||
|
||||
In order to run a container using the image built above, keep the following things in mind:
|
||||
- It might be necessary to map container port 80 to another
|
||||
- In the absence of a linked user secrets, secrets will have to be passed in to the container as environment variables, prefixed with `ASPNETCORE_`
|
||||
|
||||
```
|
||||
docker run -d -p { HOST_PC_PORT_HERE }:80 --env ASPNETCORE_Object__Property=Value --name { CONTAINER_NAME } {imagename:tag}
|
||||
```
|
||||
|
||||
Environment variables that are explicitly listed in `YABA.API\Dockerfile` will have to be properly set for proper operation of the application
|
||||
146
API/YABA.API/Controllers/BookmarksController.cs
Normal file
146
API/YABA.API/Controllers/BookmarksController.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.JsonPatch;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using YABA.API.ViewModels.Bookmarks;
|
||||
using YABA.API.ViewModels.Tags;
|
||||
using YABA.Common.DTOs.Bookmarks;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
[Authorize]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public class BookmarksController : ControllerBase
|
||||
{
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
|
||||
public BookmarksController(IMapper mapper, IBookmarkService bookmarkService)
|
||||
{
|
||||
_mapper = mapper;
|
||||
_bookmarkService = bookmarkService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.Created)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> Create([FromBody] CreateBookmarkRequestDTO request)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var result = await _bookmarkService.CreateBookmark(request);
|
||||
|
||||
if(result == null) return BadRequest();
|
||||
|
||||
return CreatedAtAction(nameof(Create), _mapper.Map<BookmarkResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> UpdateBookmark(int id, [FromBody] UpdateBookmarkRequestDTO request)
|
||||
{
|
||||
var result = await _bookmarkService.UpdateBookmark(id, request);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<BookmarkResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
[ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> PatchBookmark(int id, [FromBody] JsonPatchDocument<PatchBookmarkRequest> request)
|
||||
{
|
||||
if (request == null || !ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var entryToEdit = await _bookmarkService.Get(id);
|
||||
if(entryToEdit == null) return NotFound();
|
||||
|
||||
var entryToEditAsPatchRequest = _mapper.Map<PatchBookmarkRequest>(entryToEdit);
|
||||
request.ApplyTo(entryToEditAsPatchRequest, ModelState);
|
||||
|
||||
var updateRequest = _mapper.Map<UpdateBookmarkRequestDTO>(entryToEditAsPatchRequest);
|
||||
var result = await _bookmarkService.UpdateBookmark(id, updateRequest);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<BookmarkResponse>), (int)HttpStatusCode.OK)]
|
||||
public IActionResult GetAll(bool showHidden = false)
|
||||
{
|
||||
var result = _bookmarkService.GetAll(showHidden);
|
||||
return Ok(_mapper.Map<IEnumerable<BookmarkResponse>>(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(BookmarkResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(int id)
|
||||
{
|
||||
var result = await _bookmarkService.Get(id);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<BookmarkResponse>(result));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var result = await _bookmarkService.DeleteBookmark(id);
|
||||
|
||||
if (!result.HasValue) return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> DeleteBookmarks([FromBody] DeleteBookmarksRequest request)
|
||||
{
|
||||
if (request.Ids == null || !request.Ids.Any()) return BadRequest();
|
||||
|
||||
var result = await _bookmarkService.DeleteBookmarks(request.Ids);
|
||||
|
||||
if(result == null) return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("Hide")]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> HideBookmarks([FromBody] HideBookmarksRequest request)
|
||||
{
|
||||
if (request.Ids == null || !request.Ids.Any()) return BadRequest();
|
||||
|
||||
var result = await _bookmarkService.HideBookmarks(request.Ids);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("Tags")]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
public IActionResult GetBookmarkTags(bool showHidden = false)
|
||||
{
|
||||
var result = _bookmarkService.GetAllBookmarkTags(showHidden);
|
||||
return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
40
API/YABA.API/Controllers/MiscController.cs
Normal file
40
API/YABA.API/Controllers/MiscController.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using YABA.API.Settings;
|
||||
using YABA.API.ViewModels;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
[Authorize, Route("api/v{version:apiVersion}/[controller]")]
|
||||
public class MiscController : ControllerBase
|
||||
{
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IMiscService _miscService;
|
||||
|
||||
public MiscController(
|
||||
IMapper mapper,
|
||||
IMiscService miscService)
|
||||
{
|
||||
_mapper = mapper;
|
||||
_miscService = miscService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[DevOnly]
|
||||
[Route("GetWebsiteMetaData")]
|
||||
[ProducesResponseType(typeof(GetWebsiteMetaDataResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public IActionResult GetWebsiteMetaData(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) return BadRequest();
|
||||
|
||||
var response = _miscService.GetWebsiteMetaData(url);
|
||||
return Ok(_mapper.Map<GetWebsiteMetaDataResponse>(response));
|
||||
}
|
||||
}
|
||||
}
|
||||
110
API/YABA.API/Controllers/TagsController.cs
Normal file
110
API/YABA.API/Controllers/TagsController.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using YABA.API.ViewModels.Tags;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
[Authorize]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public class TagsController : ControllerBase
|
||||
{
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ITagsService _tagsService;
|
||||
|
||||
public TagsController(IMapper mapper, ITagsService tagsService)
|
||||
{
|
||||
_mapper = mapper;
|
||||
_tagsService = tagsService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> GetAll()
|
||||
{
|
||||
var result = await _tagsService.GetAll();
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<IEnumerable<TagResponse>>(result));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType(typeof(TagResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(int id)
|
||||
{
|
||||
var result = await _tagsService.Get(id);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<TagResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> CreateTag([FromBody]CreateTagDTO request)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
var result = await _tagsService.CreateTag(request);
|
||||
|
||||
if (result == null) return BadRequest();
|
||||
|
||||
return Ok(_mapper.Map<TagResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> UpdateTag(int id, [FromBody]UpdateTagDTO request)
|
||||
{
|
||||
var result = await _tagsService.UpdateTag(id, request);
|
||||
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return Ok(_mapper.Map<TagResponse>(result));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<TagResponse>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> PatchTag(int id, [FromBody] UpdateTagDTO request) => await UpdateTag(id, request);
|
||||
|
||||
|
||||
[HttpDelete]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType ((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> DeleteTags([FromBody] DeleteTagsRequest request)
|
||||
{
|
||||
if(request.Ids == null || !request.Ids.Any()) return BadRequest();
|
||||
|
||||
var result = await _tagsService.DeleteTags(request.Ids);
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("Hide")]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> HideTags([FromBody] HideTagsRequest request)
|
||||
{
|
||||
if (request.Ids == null || !request.Ids.Any()) return BadRequest();
|
||||
|
||||
var result = await _tagsService.HideTags(request.Ids);
|
||||
if (result == null) return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
API/YABA.API/Controllers/TestController.cs
Normal file
42
API/YABA.API/Controllers/TestController.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using YABA.API.Settings;
|
||||
|
||||
namespace YABA.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
[DevOnly]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public class TestController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TestController> _logger;
|
||||
|
||||
public TestController(ILogger<TestController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("TestLog")]
|
||||
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public IActionResult TestLog()
|
||||
{
|
||||
var testObject = new { id = 1, name = "Test Message" };
|
||||
_logger.LogDebug("Testing debug: {@TestObject}", testObject);
|
||||
return Ok(testObject);
|
||||
}
|
||||
|
||||
[HttpGet("TestLogError")]
|
||||
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
|
||||
public IActionResult TestLogError()
|
||||
{
|
||||
var testObject = new { id = 1, name = "Test Message" };
|
||||
throw new Exception();
|
||||
return Ok(testObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
API/YABA.API/Controllers/UsersController.cs
Normal file
44
API/YABA.API/Controllers/UsersController.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using YABA.API.Extensions;
|
||||
using YABA.API.ViewModels;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
[Authorize]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public UsersController(IMapper mapper, IUserService userService)
|
||||
{
|
||||
_mapper = mapper;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(UserResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NoContent)]
|
||||
public async Task<IActionResult> Register()
|
||||
{
|
||||
var authProviderId = this.GetAuthProviderId();
|
||||
|
||||
if (string.IsNullOrEmpty(authProviderId)) return NotFound();
|
||||
|
||||
var isRegistered = _userService.IsUserRegistered(authProviderId);
|
||||
|
||||
if (isRegistered) return NoContent();
|
||||
|
||||
var registedUser = await _userService.RegisterUser(authProviderId);
|
||||
return Ok(_mapper.Map<UserResponse>(registedUser));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
API/YABA.API/Dockerfile
Normal file
36
API/YABA.API/Dockerfile
Normal file
@ -0,0 +1,36 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
# Set environment variables
|
||||
ENV ASPNETCORE_Environment=Development
|
||||
ENV ASPNETCORE_Authentication__Auth0__ClientId=
|
||||
ENV ASPNETCORE_Authentication__Auth0__ClientSecret=
|
||||
ENV ASPNETCORE_Authentication__Auth0__Domain=
|
||||
ENV ASPNETCORE_Authentication__Auth0__Identifier=
|
||||
ENV ASPNETCORE_ConnectionStrings__YABAReadOnlyDbConnectionString=
|
||||
ENV ASPNETCORE_ConnectionStrings__YABAReadWriteDbConnectionString=
|
||||
ENV ASPNETCORE_WebClient__Url=
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY ["YABA.API/YABA.API.csproj", "YABA.API/"]
|
||||
COPY ["YABA.Common/YABA.Common.csproj", "YABA.Common/"]
|
||||
COPY ["YABA.Data/YABA.Data.csproj", "YABA.Data/"]
|
||||
COPY ["YABA.Models/YABA.Models.csproj", "YABA.Models/"]
|
||||
COPY ["YABA.Service/YABA.Service.csproj", "YABA.Service/"]
|
||||
RUN dotnet restore "YABA.API/YABA.API.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/YABA.API"
|
||||
RUN dotnet build "YABA.API.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "YABA.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "YABA.API.dll"]
|
||||
28
API/YABA.API/Extensions/ControllerExtensions.cs
Normal file
28
API/YABA.API/Extensions/ControllerExtensions.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using YABA.Common.Extensions;
|
||||
using YABA.Common.Lookups;
|
||||
|
||||
namespace YABA.API.Extensions
|
||||
{
|
||||
public static class ControllerExtensions
|
||||
{
|
||||
public static string GetAuthProviderId(this ControllerBase controller)
|
||||
{
|
||||
return controller.User.Identity.GetCustomClaim(ClaimsLookup.AuthProviderId);
|
||||
}
|
||||
|
||||
public static int GetUserId(this ControllerBase controller)
|
||||
{
|
||||
var isValidUserId = int.TryParse(controller.User.Identity.GetCustomClaim(ClaimsLookup.UserId), out int userId);
|
||||
return isValidUserId ? userId : 0;
|
||||
}
|
||||
|
||||
public static string GetIpAddress(this ControllerBase controller)
|
||||
{
|
||||
if (controller.Request.Headers.ContainsKey("X-Forwarded-For"))
|
||||
return controller.Request.Headers["X-Forwarded-For"];
|
||||
|
||||
return controller.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
41
API/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs
Normal file
41
API/YABA.API/Middlewares/AddCustomClaimsMiddleware.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.Security.Claims;
|
||||
using YABA.API.Extensions;
|
||||
using YABA.Common.Extensions;
|
||||
using YABA.Common.Lookups;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.API.Middlewares
|
||||
{
|
||||
public class AddCustomClaimsMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public AddCustomClaimsMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext httpContext, IUserService userService)
|
||||
{
|
||||
if (httpContext.User != null && httpContext.User.Identity.IsAuthenticated)
|
||||
{
|
||||
var userAuthProviderId = httpContext.User.Identity.GetAuthProviderId();
|
||||
|
||||
if (!string.IsNullOrEmpty(userAuthProviderId))
|
||||
{
|
||||
var userId = userService.GetUserId(userAuthProviderId);
|
||||
|
||||
if(userId <= 0)
|
||||
{
|
||||
var registedUser = await userService.RegisterUser(userAuthProviderId);
|
||||
userId = registedUser.Id;
|
||||
}
|
||||
|
||||
httpContext.User.Identities.FirstOrDefault().AddClaim(new Claim(ClaimsLookup.UserId.GetClaimName(), userId.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
await _next(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
using Serilog.Context;
|
||||
using YABA.Common.Extensions;
|
||||
|
||||
namespace YABA.API.Middlewares
|
||||
{
|
||||
public class AddCustomLoggingPropertiesMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public AddCustomLoggingPropertiesMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext httpContext)
|
||||
{
|
||||
if(httpContext.Request.Path.HasValue && httpContext.Request.Path.Value.Contains("/api"))
|
||||
{
|
||||
LogContext.PushProperty("UserId", httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.GetUserId() : "Anonymous");
|
||||
LogContext.PushProperty("RemoteIpAddress", httpContext.Connection.RemoteIpAddress.MapToIPv4());
|
||||
}
|
||||
|
||||
await _next(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
127
API/YABA.API/Program.cs
Normal file
127
API/YABA.API/Program.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Versioning;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Security.Claims;
|
||||
using YABA.API.Middlewares;
|
||||
using YABA.API.Settings;
|
||||
using YABA.API.Settings.Swashbuckle;
|
||||
using YABA.Data.Configuration;
|
||||
using YABA.Data.Context;
|
||||
using YABA.Service.Configuration;
|
||||
using Serilog;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration.AddEnvironmentVariables();
|
||||
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
// Add services to the container.
|
||||
var auth0Section = configuration.GetSection("Authentication").GetSection("Auth0");
|
||||
var auth0Settings = auth0Section.Get<Auth0Settings>();
|
||||
var domain = $"https://{auth0Settings.Domain}/";
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = domain;
|
||||
options.Audience = auth0Settings.Identifier;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
NameClaimType = ClaimTypes.NameIdentifier
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddApiVersioning(setup =>
|
||||
{
|
||||
setup.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
setup.AssumeDefaultVersionWhenUnspecified = true;
|
||||
setup.ReportApiVersions = true;
|
||||
setup.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
});
|
||||
|
||||
// Add services to the container
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddServiceProjectDependencyInjectionConfiguration(configuration);
|
||||
builder.Services.AddDataProjectDependencyInjectionConfiguration(configuration);
|
||||
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
// Add AutoMapper profiles
|
||||
var mapperConfiguration = new MapperConfiguration(mapperConfiguration =>
|
||||
{
|
||||
mapperConfiguration.AddProfile(new YABA.API.Settings.AutoMapperProfile());
|
||||
mapperConfiguration.AddProfile(new YABA.Service.Configuration.AutoMapperProfile());
|
||||
});
|
||||
|
||||
IMapper mapper = mapperConfiguration.CreateMapper();
|
||||
builder.Services.AddSingleton(mapper);
|
||||
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(
|
||||
c =>
|
||||
{
|
||||
c.SwaggerDoc(
|
||||
"v1",
|
||||
new OpenApiInfo
|
||||
{
|
||||
Title = "YABA.API",
|
||||
Version = "v1"
|
||||
});
|
||||
c.OperationFilter<RemoveVersionParameterFilter>();
|
||||
c.DocumentFilter<ReplaceVersionWithExactValueInPathFilter>();
|
||||
c.ResolveConflictingActions(apiDescription => apiDescription.First());
|
||||
}
|
||||
);
|
||||
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
});
|
||||
|
||||
// Add Serilog
|
||||
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).Enrich.FromLogContext().CreateLogger();
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Run database migrations
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var yabaDbContext = scope.ServiceProvider.GetRequiredService<YABAReadWriteContext>();
|
||||
yabaDbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseForwardedHeaders();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
// Add custom middlewares
|
||||
app.UseMiddleware<AddCustomClaimsMiddleware>();
|
||||
app.UseMiddleware<AddCustomLoggingPropertiesMiddleware>();
|
||||
|
||||
var webClientUrl = configuration.GetSection("WebClient").GetValue<string>("Url");
|
||||
app.UseCors(x => x.WithOrigins(webClientUrl).AllowAnyMethod().AllowAnyHeader());
|
||||
app.MapHealthChecks("/Pulse");
|
||||
|
||||
app.Run();
|
||||
38
API/YABA.API/Properties/launchSettings.json
Normal file
38
API/YABA.API/Properties/launchSettings.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:7922",
|
||||
"sslPort": 44310
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"YABA.API": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:7032;http://localhost:5032",
|
||||
"dotnetRunMessages": true
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
}
|
||||
}
|
||||
10
API/YABA.API/Settings/Auth0Settings.cs
Normal file
10
API/YABA.API/Settings/Auth0Settings.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace YABA.API.Settings
|
||||
{
|
||||
public class Auth0Settings
|
||||
{
|
||||
public string Domain { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
}
|
||||
}
|
||||
26
API/YABA.API/Settings/AutoMapperProfile.cs
Normal file
26
API/YABA.API/Settings/AutoMapperProfile.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using AutoMapper;
|
||||
using System.Net;
|
||||
using YABA.API.ViewModels;
|
||||
using YABA.API.ViewModels.Bookmarks;
|
||||
using YABA.API.ViewModels.Tags;
|
||||
using YABA.Common.DTOs;
|
||||
using YABA.Common.DTOs.Bookmarks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
|
||||
namespace YABA.API.Settings
|
||||
{
|
||||
public class AutoMapperProfile : Profile
|
||||
{
|
||||
public AutoMapperProfile()
|
||||
{
|
||||
CreateMap<UserDTO, UserResponse>();
|
||||
CreateMap<BookmarkDTO, BookmarkResponse>()
|
||||
.ForMember(dest => dest.Title, opt => opt.MapFrom(src => WebUtility.HtmlDecode(src.Title)))
|
||||
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => WebUtility.HtmlDecode(src.Description)));
|
||||
CreateMap<WebsiteMetaDataDTO, GetWebsiteMetaDataResponse>();
|
||||
CreateMap<BookmarkDTO, PatchBookmarkRequest>();
|
||||
CreateMap<PatchBookmarkRequest, UpdateBookmarkRequestDTO>();
|
||||
CreateMap<TagDTO, TagResponse>().ReverseMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
API/YABA.API/Settings/DevOnlyAttribute.cs
Normal file
34
API/YABA.API/Settings/DevOnlyAttribute.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace YABA.API.Settings
|
||||
{
|
||||
// Source: https://stackoverflow.com/questions/56495475/asp-net-core-its-possible-to-configure-an-action-in-controller-only-in-developm
|
||||
public class DevOnlyAttribute : Attribute, IFilterFactory
|
||||
{
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new DevOnlyAttributeImpl(serviceProvider.GetRequiredService<IWebHostEnvironment>());
|
||||
}
|
||||
|
||||
public bool IsReusable => true;
|
||||
|
||||
private class DevOnlyAttributeImpl : Attribute, IAuthorizationFilter
|
||||
{
|
||||
public DevOnlyAttributeImpl(IWebHostEnvironment hostingEnv)
|
||||
{
|
||||
HostingEnv = hostingEnv;
|
||||
}
|
||||
|
||||
private IWebHostEnvironment HostingEnv { get; }
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
if (!HostingEnv.IsDevelopment())
|
||||
{
|
||||
context.Result = new NotFoundResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace YABA.API.Settings.Swashbuckle
|
||||
{
|
||||
public class RemoveVersionParameterFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var versionParameter = operation.Parameters.Single(p => p.Name == "version");
|
||||
operation.Parameters.Remove(versionParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace YABA.API.Settings.Swashbuckle
|
||||
{
|
||||
public class ReplaceVersionWithExactValueInPathFilter : IDocumentFilter
|
||||
{
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
var paths = new OpenApiPaths();
|
||||
foreach (var path in swaggerDoc.Paths)
|
||||
{
|
||||
paths.Add(path.Key.Replace("v{version}", swaggerDoc.Info.Version), path.Value);
|
||||
}
|
||||
swaggerDoc.Paths = paths;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
API/YABA.API/ViewModels/Bookmarks/BookmarkResponse.cs
Normal file
17
API/YABA.API/ViewModels/Bookmarks/BookmarkResponse.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using YABA.API.ViewModels.Tags;
|
||||
|
||||
namespace YABA.API.ViewModels.Bookmarks
|
||||
{
|
||||
public class BookmarkResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string Url { get; set; }
|
||||
public IEnumerable<TagResponse> Tags { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace YABA.API.ViewModels.Bookmarks
|
||||
{
|
||||
public class DeleteBookmarksRequest
|
||||
{
|
||||
public IEnumerable<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace YABA.API.ViewModels.Bookmarks
|
||||
{
|
||||
public class HideBookmarksRequest
|
||||
{
|
||||
public IEnumerable<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
11
API/YABA.API/ViewModels/Bookmarks/PatchBookmarkRequest.cs
Normal file
11
API/YABA.API/ViewModels/Bookmarks/PatchBookmarkRequest.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace YABA.API.ViewModels.Bookmarks
|
||||
{
|
||||
public class PatchBookmarkRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace YABA.API.ViewModels.Bookmarks
|
||||
{
|
||||
public class UpdateBookmarkTagRequest
|
||||
{
|
||||
public List<string> Tags { get; set; }
|
||||
}
|
||||
}
|
||||
8
API/YABA.API/ViewModels/GetWebsiteMetaDataResponse.cs
Normal file
8
API/YABA.API/ViewModels/GetWebsiteMetaDataResponse.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace YABA.API.ViewModels
|
||||
{
|
||||
public class GetWebsiteMetaDataResponse
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
7
API/YABA.API/ViewModels/Tags/DeleteTagsRequest.cs
Normal file
7
API/YABA.API/ViewModels/Tags/DeleteTagsRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace YABA.API.ViewModels.Tags
|
||||
{
|
||||
public class DeleteTagsRequest
|
||||
{
|
||||
public IEnumerable<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
7
API/YABA.API/ViewModels/Tags/HideTagsRequest.cs
Normal file
7
API/YABA.API/ViewModels/Tags/HideTagsRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace YABA.API.ViewModels.Tags
|
||||
{
|
||||
public class HideTagsRequest
|
||||
{
|
||||
public IEnumerable<int> Ids { get; set; }
|
||||
}
|
||||
}
|
||||
9
API/YABA.API/ViewModels/Tags/TagResponse.cs
Normal file
9
API/YABA.API/ViewModels/Tags/TagResponse.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace YABA.API.ViewModels.Tags
|
||||
{
|
||||
public class TagResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
}
|
||||
}
|
||||
11
API/YABA.API/ViewModels/UserResponse.cs
Normal file
11
API/YABA.API/ViewModels/UserResponse.cs
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
namespace YABA.API.ViewModels
|
||||
{
|
||||
public class UserResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
}
|
||||
45
API/YABA.API/YABA.API.csproj
Normal file
45
API/YABA.API/YABA.API.csproj
Normal file
@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>726eb626-1514-45b8-8521-cd7353303edb</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.14">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
||||
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.2.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YABA.Common\YABA.Common.csproj" />
|
||||
<ProjectReference Include="..\YABA.Data\YABA.Data.csproj" />
|
||||
<ProjectReference Include="..\YABA.Service\YABA.Service.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Logs\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
API/YABA.API/appsettings.Development.json
Normal file
8
API/YABA.API/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
46
API/YABA.API/appsettings.json
Normal file
46
API/YABA.API/appsettings.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": "Information",
|
||||
"Using": [
|
||||
"Serilog.Sinks.Grafana.Loki"
|
||||
],
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "Logs/log.json",
|
||||
"rollingInterval": "Day",
|
||||
"formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "GrafanaLoki",
|
||||
"Args": {
|
||||
"uri": "https://loki.iwanaga.moe",
|
||||
"labels": [
|
||||
{
|
||||
"key": "job",
|
||||
"value": "YABA.API"
|
||||
},
|
||||
{
|
||||
"key": "environment",
|
||||
"value": "localhost"
|
||||
}
|
||||
],
|
||||
"propertiesAsLabels": [ "job", "environment" ]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": ["FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId"]
|
||||
},
|
||||
"WebClient": {
|
||||
"Url": "http://localhost:3000"
|
||||
}
|
||||
}
|
||||
14
API/YABA.Common/Attributes/ClaimNameAttribute.cs
Normal file
14
API/YABA.Common/Attributes/ClaimNameAttribute.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace YABA.Common.Attributes
|
||||
{
|
||||
public class ClaimNameAttribute : Attribute
|
||||
{
|
||||
public string Name { get; private set; }
|
||||
|
||||
public ClaimNameAttribute(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
API/YABA.Common/DTOs/Bookmarks/BookmarkDTO.cs
Normal file
20
API/YABA.Common/DTOs/Bookmarks/BookmarkDTO.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
using YABA.Common.Interfaces;
|
||||
|
||||
namespace YABA.Common.DTOs.Bookmarks
|
||||
{
|
||||
public class BookmarkDTO : IBookmark
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string Url { get; set; }
|
||||
public List<TagDTO>? Tags { get; set; } = new List<TagDTO>();
|
||||
}
|
||||
}
|
||||
18
API/YABA.Common/DTOs/Bookmarks/CreateBookmarkRequestDTO.cs
Normal file
18
API/YABA.Common/DTOs/Bookmarks/CreateBookmarkRequestDTO.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using YABA.Common.Interfaces;
|
||||
|
||||
namespace YABA.Common.DTOs.Bookmarks
|
||||
{
|
||||
public class CreateBookmarkRequestDTO : IBookmark
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public IEnumerable<string>? Tags { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
16
API/YABA.Common/DTOs/Bookmarks/UpdateBookmarkRequestDTO.cs
Normal file
16
API/YABA.Common/DTOs/Bookmarks/UpdateBookmarkRequestDTO.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using YABA.Common.Interfaces;
|
||||
|
||||
namespace YABA.Common.DTOs.Bookmarks
|
||||
{
|
||||
public class UpdateBookmarkRequestDTO : IBookmark
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string? Url { get; set; }
|
||||
public IEnumerable<string>? Tags { get; set; }
|
||||
}
|
||||
}
|
||||
11
API/YABA.Common/DTOs/Tags/CreateTagDTO.cs
Normal file
11
API/YABA.Common/DTOs/Tags/CreateTagDTO.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YABA.Common.DTOs.Tags
|
||||
{
|
||||
public class CreateTagDTO
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
}
|
||||
}
|
||||
9
API/YABA.Common/DTOs/Tags/TagDTO.cs
Normal file
9
API/YABA.Common/DTOs/Tags/TagDTO.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace YABA.Common.DTOs.Tags
|
||||
{
|
||||
public class TagDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
}
|
||||
}
|
||||
10
API/YABA.Common/DTOs/Tags/UpdateTagDTO.cs
Normal file
10
API/YABA.Common/DTOs/Tags/UpdateTagDTO.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YABA.Common.DTOs.Tags
|
||||
{
|
||||
public class UpdateTagDTO
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public bool? IsHidden { get; set; }
|
||||
}
|
||||
}
|
||||
12
API/YABA.Common/DTOs/UserDTO.cs
Normal file
12
API/YABA.Common/DTOs/UserDTO.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace YABA.Common.DTOs
|
||||
{
|
||||
public class UserDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
}
|
||||
9
API/YABA.Common/DTOs/WebsiteMetaDataDTO.cs
Normal file
9
API/YABA.Common/DTOs/WebsiteMetaDataDTO.cs
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
namespace YABA.Common.DTOs
|
||||
{
|
||||
public class WebsiteMetaDataDTO
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
24
API/YABA.Common/Extensions/DictionaryExtensions.cs
Normal file
24
API/YABA.Common/Extensions/DictionaryExtensions.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YABA.Common.Extensions
|
||||
{
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static void AddRange<K, V>(this Dictionary<K, V> source, Dictionary<K, V> collection)
|
||||
{
|
||||
if (collection == null)
|
||||
{
|
||||
throw new ArgumentNullException("Collection is null");
|
||||
}
|
||||
|
||||
foreach (var item in collection)
|
||||
{
|
||||
if (!source.ContainsKey(item.Key))
|
||||
{
|
||||
source.Add(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
API/YABA.Common/Extensions/EnumExtensions.cs
Normal file
40
API/YABA.Common/Extensions/EnumExtensions.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using YABA.Common.Attributes;
|
||||
using YABA.Common.Lookups;
|
||||
|
||||
namespace YABA.Common.Extensions
|
||||
{
|
||||
public static class EnumExtensions
|
||||
{
|
||||
private static readonly IEnumerable<CrudResultLookup> SuccessfulCrudStatuses = new List<CrudResultLookup>() {
|
||||
CrudResultLookup.CreateSucceeded,
|
||||
CrudResultLookup.UpdateSucceeded,
|
||||
CrudResultLookup.DeleteSucceeded,
|
||||
CrudResultLookup.RetrieveSuccessful
|
||||
};
|
||||
|
||||
public static TAttribute GetAttribute<TAttribute>(this Enum value) where TAttribute : Attribute
|
||||
{
|
||||
var enumType = value.GetType();
|
||||
var name = Enum.GetName(enumType, value);
|
||||
return enumType.GetField(name).GetCustomAttributes(false).OfType<TAttribute>().SingleOrDefault();
|
||||
}
|
||||
|
||||
public static string GetDisplayName(this Enum enumValue)
|
||||
{
|
||||
return enumValue.GetAttribute<DisplayAttribute>().Name;
|
||||
}
|
||||
|
||||
public static string GetClaimName(this ClaimsLookup claimLookup)
|
||||
{
|
||||
return claimLookup.GetAttribute<ClaimNameAttribute>().Name;
|
||||
}
|
||||
|
||||
public static bool IsCrudResultSuccessful(this CrudResultLookup importStatusLookup) => SuccessfulCrudStatuses.Contains(importStatusLookup);
|
||||
|
||||
public static bool IsCrudResultFailure(this CrudResultLookup importStatusLookup) => !SuccessfulCrudStatuses.Contains(importStatusLookup);
|
||||
}
|
||||
}
|
||||
18
API/YABA.Common/Extensions/UserIdentityExtensions.cs
Normal file
18
API/YABA.Common/Extensions/UserIdentityExtensions.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using YABA.Common.Lookups;
|
||||
|
||||
namespace YABA.Common.Extensions
|
||||
{
|
||||
public static class UserIdentityExtensions
|
||||
{
|
||||
public static string GetUserId(this IIdentity identity) => GetCustomClaim(identity, ClaimsLookup.UserId);
|
||||
public static string GetAuthProviderId(this IIdentity identity) => GetCustomClaim(identity, ClaimsLookup.AuthProviderId);
|
||||
|
||||
public static string GetCustomClaim(this IIdentity identity, ClaimsLookup claim)
|
||||
{
|
||||
var claimsIdentity = identity as ClaimsIdentity;
|
||||
return claimsIdentity.FindFirst(claim.GetClaimName())?.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
API/YABA.Common/Interfaces/IBookmark.cs
Normal file
12
API/YABA.Common/Interfaces/IBookmark.cs
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
namespace YABA.Common.Interfaces
|
||||
{
|
||||
public interface IBookmark
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
22
API/YABA.Common/Lookups/ClaimsLookup.cs
Normal file
22
API/YABA.Common/Lookups/ClaimsLookup.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using YABA.Common.Attributes;
|
||||
|
||||
namespace YABA.Common.Lookups
|
||||
{
|
||||
public enum ClaimsLookup
|
||||
{
|
||||
[ClaimNameAttribute("https://dev.iwanaga.moe/api/auth_provider_id")]
|
||||
AuthProviderId = 1,
|
||||
|
||||
[ClaimNameAttribute("https://dev.iwanaga.moe/api/email_address")]
|
||||
UserEmail = 2,
|
||||
|
||||
[ClaimNameAttribute("https://dev.iwanaga.moe/api/email_verified")]
|
||||
IsEmailConfirmed = 3,
|
||||
|
||||
[ClaimNameAttribute("https://dev.iwanaga.moe/api/username")]
|
||||
Username = 4,
|
||||
|
||||
[ClaimNameAttribute("https://dev.iwanaga.moe/api/id")]
|
||||
UserId = 5
|
||||
}
|
||||
}
|
||||
34
API/YABA.Common/Lookups/CrudResultLookup.cs
Normal file
34
API/YABA.Common/Lookups/CrudResultLookup.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace YABA.Common.Lookups
|
||||
{
|
||||
public enum CrudResultLookup
|
||||
{
|
||||
[Display(Name = "Insert failed")]
|
||||
CreateFailed = 1,
|
||||
|
||||
[Display(Name = "Insert succeeded")]
|
||||
CreateSucceeded = 2,
|
||||
|
||||
[Display(Name = "Insert failed. Entry already exists")]
|
||||
CreateFailedEntryExists = 3,
|
||||
|
||||
[Display(Name = "Update failed")]
|
||||
UpdateFailed = 4,
|
||||
|
||||
[Display(Name = "Update succeeded")]
|
||||
UpdateSucceeded = 5,
|
||||
|
||||
[Display(Name = "Delete failed")]
|
||||
DeleteFailed = 6,
|
||||
|
||||
[Display(Name = "Delete succeeded")]
|
||||
DeleteSucceeded = 7,
|
||||
|
||||
[Display(Name = "Retrieve failed")]
|
||||
RetrieveFailed = 8,
|
||||
|
||||
[Display(Name = "Retrieve successful")]
|
||||
RetrieveSuccessful = 9
|
||||
}
|
||||
}
|
||||
12
API/YABA.Common/YABA.Common.csproj
Normal file
12
API/YABA.Common/YABA.Common.csproj
Normal file
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YABA.Data.Context;
|
||||
|
||||
namespace YABA.Data.Configuration
|
||||
{
|
||||
public static class DependencyInjectionConfiguration
|
||||
{
|
||||
public static void AddDataProjectDependencyInjectionConfiguration(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped(x =>
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<YABABaseContext>();
|
||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString("YABAReadOnlyDbConnectionString")).UseSnakeCaseNamingConvention();
|
||||
return new YABAReadOnlyContext(optionsBuilder.Options);
|
||||
});
|
||||
|
||||
services.AddScoped(x => {
|
||||
var optionsBuilder = new DbContextOptionsBuilder<YABABaseContext>();
|
||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString("YABAReadWriteDbConnectionString")).UseSnakeCaseNamingConvention();
|
||||
return new YABAReadWriteContext(optionsBuilder.Options);
|
||||
});
|
||||
|
||||
services.AddDbContext<YABABaseContext>(options => options
|
||||
.UseNpgsql(configuration.GetConnectionString("YABAReadWriteDbConnectionString"))
|
||||
.UseSnakeCaseNamingConvention()
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll));
|
||||
|
||||
services.AddDbContext<YABAReadWriteContext>(options => options
|
||||
.UseNpgsql(configuration.GetConnectionString("YABAReadWriteDbConnectionString"))
|
||||
.UseSnakeCaseNamingConvention()
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll));
|
||||
|
||||
services.AddDbContext<YABAReadOnlyContext>(options => options
|
||||
.UseNpgsql(configuration.GetConnectionString("YABAReadOnlyDbConnectionString"))
|
||||
.UseSnakeCaseNamingConvention()
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
|
||||
}
|
||||
}
|
||||
}
|
||||
67
API/YABA.Data/Context/YABABaseContext.cs
Normal file
67
API/YABA.Data/Context/YABABaseContext.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using YABA.Models;
|
||||
using YABA.Models.Interfaces;
|
||||
|
||||
namespace YABA.Data.Context
|
||||
{
|
||||
public class YABABaseContext : DbContext
|
||||
{
|
||||
public YABABaseContext() : base() { }
|
||||
public YABABaseContext(DbContextOptions<YABABaseContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// Add lookup backed data here
|
||||
// SAMPLE
|
||||
// var lookupBackedData = Enum.GetValues(typeof(LookupEnum)).Cast<LookupEnum>();
|
||||
// modelBuilder.Entity<LookupModel>().HasData(lookupBackedData.Select(x => new LookupModel(x)));
|
||||
|
||||
|
||||
modelBuilder.Model.GetEntityTypes()
|
||||
.Where(entityType => typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType))
|
||||
.ToList()
|
||||
.ForEach(entityType =>
|
||||
{
|
||||
modelBuilder.Entity(entityType.ClrType)
|
||||
.HasQueryFilter(ConvertFilterExpression<ISoftDeletable>(e => !e.IsDeleted, entityType.ClrType));
|
||||
});
|
||||
|
||||
modelBuilder.Entity<BookmarkTag>()
|
||||
.HasKey(x => new { x.BookmarkId, x.TagId });
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.HasIndex(x => x.Auth0Id)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<Tag>()
|
||||
.HasIndex(x => new { x.Name, x.UserId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<Bookmark>()
|
||||
.HasIndex(x => new { x.Url, x.UserId })
|
||||
.IsUnique();
|
||||
}
|
||||
|
||||
public virtual DbSet<Bookmark> Bookmarks { get; set; }
|
||||
public virtual DbSet<Tag> Tags { get; set; }
|
||||
public virtual DbSet<BookmarkTag> BookmarkTags { get; set; }
|
||||
public virtual DbSet<User> Users { get; set; }
|
||||
|
||||
private static LambdaExpression ConvertFilterExpression<TInterface>(
|
||||
Expression<Func<TInterface, bool>> filterExpression,
|
||||
Type entityType)
|
||||
{
|
||||
// SOURCE: https://stackoverflow.com/questions/47673524/ef-core-soft-delete-with-shadow-properties-and-query-filters/48744644#48744644
|
||||
var newParam = Expression.Parameter(entityType);
|
||||
var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);
|
||||
|
||||
return Expression.Lambda(newBody, newParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
API/YABA.Data/Context/YABABaseContextFactory.cs
Normal file
18
API/YABA.Data/Context/YABABaseContextFactory.cs
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace YABA.Data.Context
|
||||
{
|
||||
public class YABABaseContextFactory : IDesignTimeDbContextFactory<YABABaseContext>
|
||||
{
|
||||
public YABABaseContext CreateDbContext(string[] args)
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<YABABaseContext>();
|
||||
optionsBuilder.UseNpgsql(args[0])
|
||||
.UseSnakeCaseNamingConvention()
|
||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll);
|
||||
return new YABABaseContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
API/YABA.Data/Context/YABAReadOnlyContext.cs
Normal file
13
API/YABA.Data/Context/YABAReadOnlyContext.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace YABA.Data.Context
|
||||
{
|
||||
public class YABAReadOnlyContext : YABABaseContext
|
||||
{
|
||||
public YABAReadOnlyContext() : base() { }
|
||||
public YABAReadOnlyContext(DbContextOptions<YABABaseContext> options) : base(options)
|
||||
{
|
||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
}
|
||||
}
|
||||
}
|
||||
57
API/YABA.Data/Context/YABAReadWriteContext.cs
Normal file
57
API/YABA.Data/Context/YABAReadWriteContext.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Models.Interfaces;
|
||||
|
||||
namespace YABA.Data.Context
|
||||
{
|
||||
public class YABAReadWriteContext : YABABaseContext
|
||||
{
|
||||
public YABAReadWriteContext() : base() { }
|
||||
|
||||
public YABAReadWriteContext(DbContextOptions<YABABaseContext> options) : base(options)
|
||||
{
|
||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
|
||||
}
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
var dateCreatedTrackableEntries = ChangeTracker
|
||||
.Entries()
|
||||
.Where(e => e.Entity is IDateCreatedTrackable && e.State == EntityState.Added);
|
||||
|
||||
foreach (var entry in dateCreatedTrackableEntries)
|
||||
((IDateCreatedTrackable)entry.Entity).CreatedOn = DateTimeOffset.UtcNow;
|
||||
|
||||
var dateModifiedTrackableItems = ChangeTracker
|
||||
.Entries()
|
||||
.Where(e => e.Entity is IDateModifiedTrackable && (e.State == EntityState.Modified || e.State == EntityState.Added));
|
||||
|
||||
foreach (var entry in dateModifiedTrackableItems)
|
||||
((IDateModifiedTrackable)entry.Entity).LastModified = DateTimeOffset.UtcNow;
|
||||
|
||||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dateCreatedTrackableEntries = ChangeTracker
|
||||
.Entries()
|
||||
.Where(e => e.Entity is IDateCreatedTrackable && e.State == EntityState.Added);
|
||||
|
||||
foreach (var entry in dateCreatedTrackableEntries)
|
||||
((IDateCreatedTrackable)entry.Entity).CreatedOn = DateTimeOffset.UtcNow;
|
||||
|
||||
var dateModifiedTrackableItems = ChangeTracker
|
||||
.Entries()
|
||||
.Where(e => e.Entity is IDateModifiedTrackable && (e.State == EntityState.Modified || e.State == EntityState.Added));
|
||||
|
||||
foreach (var entry in dateModifiedTrackableItems)
|
||||
((IDateModifiedTrackable)entry.Entity).LastModified = DateTimeOffset.UtcNow;
|
||||
|
||||
return base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
API/YABA.Data/Extensions/GenericDbSetExtensions.cs
Normal file
35
API/YABA.Data/Extensions/GenericDbSetExtensions.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YABA.Models.Interfaces;
|
||||
|
||||
namespace YABA.Data.Extensions
|
||||
{
|
||||
public static class GenericDbSetExtensions
|
||||
{
|
||||
public static void Upsert<T>(this DbSet<T> dbSet, T entity) where T : class, IIdentifiable
|
||||
{
|
||||
var entityInList = new List<T>() { entity };
|
||||
dbSet.UpsertRange(entityInList);
|
||||
}
|
||||
|
||||
public static void UpsertRange<T>(this DbSet<T> dbSet, IEnumerable<T> entities) where T : class, IIdentifiable
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var entityExists = dbSet.Any(x => x.Id == entity.Id);
|
||||
|
||||
if (entityExists)
|
||||
{
|
||||
EntityEntry<T> attachedEntity = dbSet.Attach(entity);
|
||||
attachedEntity.State = EntityState.Modified;
|
||||
}
|
||||
else
|
||||
{
|
||||
dbSet.Add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
API/YABA.Data/Extensions/UsersDbSetExtensions.cs
Normal file
23
API/YABA.Data/Extensions/UsersDbSetExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Models;
|
||||
|
||||
namespace YABA.Data.Extensions
|
||||
{
|
||||
public static class UsersDbSetExtensions
|
||||
{
|
||||
public static async Task<bool> UserExistsAsync(this DbSet<User> userDbSet, int userId)
|
||||
{
|
||||
return await userDbSet.AnyAsync(x => x.Id == userId);
|
||||
}
|
||||
|
||||
public static bool UserExists(this DbSet<User> userDbSet, int userId)
|
||||
{
|
||||
return userDbSet.Any(x => x.Id == userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
221
API/YABA.Data/Migrations/20230125055348_InitialMigration.Designer.cs
generated
Normal file
221
API/YABA.Data/Migrations/20230125055348_InitialMigration.Designer.cs
generated
Normal file
@ -0,0 +1,221 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YABA.Data.Context;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(YABABaseContext))]
|
||||
[Migration("20230125055348_InitialMigration")]
|
||||
partial class InitialMigration
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.17")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("note");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmarks");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_bookmarks_user_id");
|
||||
|
||||
b.ToTable("bookmarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<int>("BookmarkId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bookmark_id");
|
||||
|
||||
b.Property<int>("TagId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("tag_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmark_tags");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("ix_bookmark_tags_tag_id");
|
||||
|
||||
b.HasIndex("BookmarkId", "TagId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bookmark_tags_bookmark_id_tag_id");
|
||||
|
||||
b.ToTable("bookmark_tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tags");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tags_user_id");
|
||||
|
||||
b.ToTable("tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("Auth0Id")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("auth0id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Auth0Id")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_auth0id");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_bookmarks_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.Bookmark", "Bookmark")
|
||||
.WithMany()
|
||||
.HasForeignKey("BookmarkId")
|
||||
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YABA.Models.Tag", "Tag")
|
||||
.WithMany()
|
||||
.HasForeignKey("TagId")
|
||||
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bookmark");
|
||||
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_tags_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
144
API/YABA.Data/Migrations/20230125055348_InitialMigration.cs
Normal file
144
API/YABA.Data/Migrations/20230125055348_InitialMigration.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
public partial class InitialMigration : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "users",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
is_deleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
created_on = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
last_modified = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
auth0id = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_users", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "bookmarks",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
is_deleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
created_on = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
last_modified = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
title = table.Column<string>(type: "text", nullable: false),
|
||||
description = table.Column<string>(type: "text", nullable: false),
|
||||
note = table.Column<string>(type: "text", nullable: false),
|
||||
is_hidden = table.Column<bool>(type: "boolean", nullable: false),
|
||||
user_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_bookmarks", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_bookmarks_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tags",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
is_deleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: false),
|
||||
is_hidden = table.Column<bool>(type: "boolean", nullable: false),
|
||||
user_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tags", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_tags_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "bookmark_tags",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
bookmark_id = table.Column<int>(type: "integer", nullable: false),
|
||||
tag_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_bookmark_tags", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_bookmark_tags_bookmarks_bookmark_id",
|
||||
column: x => x.bookmark_id,
|
||||
principalTable: "bookmarks",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_bookmark_tags_tags_tag_id",
|
||||
column: x => x.tag_id,
|
||||
principalTable: "tags",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_bookmark_tags_bookmark_id_tag_id",
|
||||
table: "bookmark_tags",
|
||||
columns: new[] { "bookmark_id", "tag_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_bookmark_tags_tag_id",
|
||||
table: "bookmark_tags",
|
||||
column: "tag_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_bookmarks_user_id",
|
||||
table: "bookmarks",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tags_user_id",
|
||||
table: "tags",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_auth0id",
|
||||
table: "users",
|
||||
column: "auth0id",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "bookmark_tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "bookmarks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
226
API/YABA.Data/Migrations/20230126050046_AddedUrlToBookmark.Designer.cs
generated
Normal file
226
API/YABA.Data/Migrations/20230126050046_AddedUrlToBookmark.Designer.cs
generated
Normal file
@ -0,0 +1,226 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YABA.Data.Context;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(YABABaseContext))]
|
||||
[Migration("20230126050046_AddedUrlToBookmark")]
|
||||
partial class AddedUrlToBookmark
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.17")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("note");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmarks");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_bookmarks_user_id");
|
||||
|
||||
b.ToTable("bookmarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<int>("BookmarkId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bookmark_id");
|
||||
|
||||
b.Property<int>("TagId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("tag_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmark_tags");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("ix_bookmark_tags_tag_id");
|
||||
|
||||
b.HasIndex("BookmarkId", "TagId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bookmark_tags_bookmark_id_tag_id");
|
||||
|
||||
b.ToTable("bookmark_tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tags");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tags_user_id");
|
||||
|
||||
b.ToTable("tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("Auth0Id")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("auth0id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Auth0Id")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_auth0id");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_bookmarks_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.Bookmark", "Bookmark")
|
||||
.WithMany()
|
||||
.HasForeignKey("BookmarkId")
|
||||
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YABA.Models.Tag", "Tag")
|
||||
.WithMany()
|
||||
.HasForeignKey("TagId")
|
||||
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bookmark");
|
||||
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_tags_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
public partial class AddedUrlToBookmark : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "url",
|
||||
table: "bookmarks",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "url",
|
||||
table: "bookmarks");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YABA.Data.Context;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(YABABaseContext))]
|
||||
[Migration("20230128064541_ModifiedBookmarkTagsPK_RemovedSoftDeleteFromTagsAndBookmarks")]
|
||||
partial class ModifiedBookmarkTagsPK_RemovedSoftDeleteFromTagsAndBookmarks
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.17")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("note");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmarks");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_bookmarks_user_id");
|
||||
|
||||
b.ToTable("bookmarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.Property<int>("BookmarkId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bookmark_id");
|
||||
|
||||
b.Property<int>("TagId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("tag_id");
|
||||
|
||||
b.HasKey("BookmarkId", "TagId")
|
||||
.HasName("pk_bookmark_tags");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("ix_bookmark_tags_tag_id");
|
||||
|
||||
b.ToTable("bookmark_tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tags");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tags_name");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tags_user_id");
|
||||
|
||||
b.ToTable("tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("Auth0Id")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("auth0id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Auth0Id")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_auth0id");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_bookmarks_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.Bookmark", "Bookmark")
|
||||
.WithMany()
|
||||
.HasForeignKey("BookmarkId")
|
||||
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YABA.Models.Tag", "Tag")
|
||||
.WithMany()
|
||||
.HasForeignKey("TagId")
|
||||
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bookmark");
|
||||
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_tags_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
public partial class ModifiedBookmarkTagsPK_RemovedSoftDeleteFromTagsAndBookmarks : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "pk_bookmark_tags",
|
||||
table: "bookmark_tags");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_bookmark_tags_bookmark_id_tag_id",
|
||||
table: "bookmark_tags");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_deleted",
|
||||
table: "tags");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_deleted",
|
||||
table: "bookmarks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "id",
|
||||
table: "bookmark_tags");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "pk_bookmark_tags",
|
||||
table: "bookmark_tags",
|
||||
columns: new[] { "bookmark_id", "tag_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tags_name",
|
||||
table: "tags",
|
||||
column: "name",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_tags_name",
|
||||
table: "tags");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "pk_bookmark_tags",
|
||||
table: "bookmark_tags");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_deleted",
|
||||
table: "tags",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_deleted",
|
||||
table: "bookmarks",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "id",
|
||||
table: "bookmark_tags",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "pk_bookmark_tags",
|
||||
table: "bookmark_tags",
|
||||
column: "id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_bookmark_tags_bookmark_id_tag_id",
|
||||
table: "bookmark_tags",
|
||||
columns: new[] { "bookmark_id", "tag_id" },
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
210
API/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.Designer.cs
generated
Normal file
210
API/YABA.Data/Migrations/20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional.Designer.cs
generated
Normal file
@ -0,0 +1,210 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YABA.Data.Context;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(YABABaseContext))]
|
||||
[Migration("20230128193757_ModifiedBookmarkTable_MakeNoteAndDescriptionOptional")]
|
||||
partial class ModifiedBookmarkTable_MakeNoteAndDescriptionOptional
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.17")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("note");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmarks");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_bookmarks_user_id");
|
||||
|
||||
b.ToTable("bookmarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.Property<int>("BookmarkId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bookmark_id");
|
||||
|
||||
b.Property<int>("TagId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("tag_id");
|
||||
|
||||
b.HasKey("BookmarkId", "TagId")
|
||||
.HasName("pk_bookmark_tags");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("ix_bookmark_tags_tag_id");
|
||||
|
||||
b.ToTable("bookmark_tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tags");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tags_name");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tags_user_id");
|
||||
|
||||
b.ToTable("tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("Auth0Id")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("auth0id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Auth0Id")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_auth0id");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_bookmarks_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.Bookmark", "Bookmark")
|
||||
.WithMany()
|
||||
.HasForeignKey("BookmarkId")
|
||||
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YABA.Models.Tag", "Tag")
|
||||
.WithMany()
|
||||
.HasForeignKey("TagId")
|
||||
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bookmark");
|
||||
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_tags_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
public partial class ModifiedBookmarkTable_MakeNoteAndDescriptionOptional : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "note",
|
||||
table: "bookmarks",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "description",
|
||||
table: "bookmarks",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "note",
|
||||
table: "bookmarks",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "description",
|
||||
table: "bookmarks",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,214 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YABA.Data.Context;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(YABABaseContext))]
|
||||
[Migration("20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark")]
|
||||
partial class AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.17")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("note");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmarks");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_bookmarks_user_id");
|
||||
|
||||
b.HasIndex("Url", "UserId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bookmarks_url_user_id");
|
||||
|
||||
b.ToTable("bookmarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.Property<int>("BookmarkId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bookmark_id");
|
||||
|
||||
b.Property<int>("TagId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("tag_id");
|
||||
|
||||
b.HasKey("BookmarkId", "TagId")
|
||||
.HasName("pk_bookmark_tags");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("ix_bookmark_tags_tag_id");
|
||||
|
||||
b.ToTable("bookmark_tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tags");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tags_user_id");
|
||||
|
||||
b.HasIndex("Name", "UserId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tags_name_user_id");
|
||||
|
||||
b.ToTable("tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("Auth0Id")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("auth0id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Auth0Id")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_auth0id");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_bookmarks_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.Bookmark", "Bookmark")
|
||||
.WithMany()
|
||||
.HasForeignKey("BookmarkId")
|
||||
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YABA.Models.Tag", "Tag")
|
||||
.WithMany()
|
||||
.HasForeignKey("TagId")
|
||||
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bookmark");
|
||||
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_tags_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
public partial class AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_tags_name",
|
||||
table: "tags");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tags_name_user_id",
|
||||
table: "tags",
|
||||
columns: new[] { "name", "user_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_bookmarks_url_user_id",
|
||||
table: "bookmarks",
|
||||
columns: new[] { "url", "user_id" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_tags_name_user_id",
|
||||
table: "tags");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_bookmarks_url_user_id",
|
||||
table: "bookmarks");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tags_name",
|
||||
table: "tags",
|
||||
column: "name",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
212
API/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs
Normal file
212
API/YABA.Data/Migrations/YABABaseContextModelSnapshot.cs
Normal file
@ -0,0 +1,212 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using YABA.Data.Context;
|
||||
|
||||
namespace YABA.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(YABABaseContext))]
|
||||
partial class YABABaseContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||
.HasAnnotation("ProductVersion", "5.0.17")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("note");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bookmarks");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_bookmarks_user_id");
|
||||
|
||||
b.HasIndex("Url", "UserId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bookmarks_url_user_id");
|
||||
|
||||
b.ToTable("bookmarks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.Property<int>("BookmarkId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("bookmark_id");
|
||||
|
||||
b.Property<int>("TagId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("tag_id");
|
||||
|
||||
b.HasKey("BookmarkId", "TagId")
|
||||
.HasName("pk_bookmark_tags");
|
||||
|
||||
b.HasIndex("TagId")
|
||||
.HasDatabaseName("ix_bookmark_tags_tag_id");
|
||||
|
||||
b.ToTable("bookmark_tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tags");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tags_user_id");
|
||||
|
||||
b.HasIndex("Name", "UserId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tags_name_user_id");
|
||||
|
||||
b.ToTable("tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id")
|
||||
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
b.Property<string>("Auth0Id")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("auth0id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedOn")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_on");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_modified");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Auth0Id")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_auth0id");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_bookmarks_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.Bookmark", "Bookmark")
|
||||
.WithMany()
|
||||
.HasForeignKey("BookmarkId")
|
||||
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YABA.Models.Tag", "Tag")
|
||||
.WithMany()
|
||||
.HasForeignKey("TagId")
|
||||
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bookmark");
|
||||
|
||||
b.Navigation("Tag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("YABA.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_tags_users_user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
25
API/YABA.Data/YABA.Data.csproj
Normal file
25
API/YABA.Data/YABA.Data.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.14">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YABA.Common\YABA.Common.csproj" />
|
||||
<ProjectReference Include="..\YABA.Models\YABA.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
28
API/YABA.Models/Bookmark.cs
Normal file
28
API/YABA.Models/Bookmark.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using YABA.Common.Interfaces;
|
||||
using YABA.Models.Interfaces;
|
||||
|
||||
namespace YABA.Models
|
||||
{
|
||||
public class Bookmark : IIdentifiable, IDateCreatedTrackable, IDateModifiedTrackable, IBookmark
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Note { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
public string Url { get; set; }
|
||||
|
||||
[Required]
|
||||
[ForeignKey(nameof(User))]
|
||||
public int UserId { get; set; }
|
||||
public virtual User User { get; set; }
|
||||
|
||||
public ICollection<BookmarkTag> BookmarkTags { get; set; }
|
||||
}
|
||||
}
|
||||
18
API/YABA.Models/BookmarkTag.cs
Normal file
18
API/YABA.Models/BookmarkTag.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace YABA.Models
|
||||
{
|
||||
public class BookmarkTag
|
||||
{
|
||||
[Required]
|
||||
[ForeignKey(nameof(Bookmark))]
|
||||
public int BookmarkId { get; set; }
|
||||
public virtual Bookmark Bookmark { get; set; }
|
||||
|
||||
[Required]
|
||||
[ForeignKey(nameof(Tag))]
|
||||
public int TagId { get; set; }
|
||||
public virtual Tag Tag { get; set; }
|
||||
}
|
||||
}
|
||||
9
API/YABA.Models/Interfaces/IDateCreatedTrackable.cs
Normal file
9
API/YABA.Models/Interfaces/IDateCreatedTrackable.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace YABA.Models.Interfaces
|
||||
{
|
||||
public interface IDateCreatedTrackable
|
||||
{
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
}
|
||||
}
|
||||
9
API/YABA.Models/Interfaces/IDateModifiedTrackable.cs
Normal file
9
API/YABA.Models/Interfaces/IDateModifiedTrackable.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace YABA.Models.Interfaces
|
||||
{
|
||||
public interface IDateModifiedTrackable
|
||||
{
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
}
|
||||
8
API/YABA.Models/Interfaces/IIdentifiable.cs
Normal file
8
API/YABA.Models/Interfaces/IIdentifiable.cs
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
namespace YABA.Models.Interfaces
|
||||
{
|
||||
public interface IIdentifiable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
}
|
||||
8
API/YABA.Models/Interfaces/ISoftDeletable.cs
Normal file
8
API/YABA.Models/Interfaces/ISoftDeletable.cs
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
namespace YABA.Models.Interfaces
|
||||
{
|
||||
public interface ISoftDeletable
|
||||
{
|
||||
public bool IsDeleted { get; set; }
|
||||
}
|
||||
}
|
||||
21
API/YABA.Models/Tag.cs
Normal file
21
API/YABA.Models/Tag.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using YABA.Models.Interfaces;
|
||||
|
||||
namespace YABA.Models
|
||||
{
|
||||
public class Tag : IIdentifiable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
|
||||
[Required]
|
||||
[ForeignKey(nameof(User))]
|
||||
public int UserId { get; set; }
|
||||
public virtual User User { get; set; }
|
||||
|
||||
public virtual ICollection<BookmarkTag> TagBookmarks { get; set; }
|
||||
}
|
||||
}
|
||||
17
API/YABA.Models/User.cs
Normal file
17
API/YABA.Models/User.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using YABA.Models.Interfaces;
|
||||
|
||||
namespace YABA.Models
|
||||
{
|
||||
public class User : IIdentifiable, ISoftDeletable, IDateCreatedTrackable, IDateModifiedTrackable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Auth0Id { get; set; }
|
||||
}
|
||||
}
|
||||
16
API/YABA.Models/YABA.Models.csproj
Normal file
16
API/YABA.Models/YABA.Models.csproj
Normal file
@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YABA.Common\YABA.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,67 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using YABA.Data.Context;
|
||||
using YABA.Models;
|
||||
|
||||
namespace YABA.Service.Tests
|
||||
{
|
||||
public partial class UserServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[ClassData(typeof(IsUserRegistedTestData))]
|
||||
public void IsUserRegistedTests(IsUserRegisteredScenario scenario)
|
||||
{
|
||||
var usersDbSet = new Mock<DbSet<User>>();
|
||||
usersDbSet.As<IQueryable<User>>().Setup(m => m.Provider).Returns(scenario.Users.Provider);
|
||||
usersDbSet.As<IQueryable<User>>().Setup(m => m.Expression).Returns(scenario.Users.Expression);
|
||||
usersDbSet.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(scenario.Users.ElementType);
|
||||
usersDbSet.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(() => scenario.Users.GetEnumerator());
|
||||
|
||||
var mockContext = new Mock<YABAReadOnlyContext>();
|
||||
mockContext.Setup(x => x.Users).Returns(usersDbSet.Object);
|
||||
|
||||
var userService = new UserService(mockContext.Object, null, null, null);
|
||||
var actualIsUserRegistered = userService.IsUserRegistered(scenario.AuthProviderId);
|
||||
Assert.Equal(scenario.ExpectedResult, actualIsUserRegistered);
|
||||
}
|
||||
}
|
||||
|
||||
public class IsUserRegistedTestData : TheoryData<IsUserRegisteredScenario>
|
||||
{
|
||||
public IsUserRegistedTestData()
|
||||
{
|
||||
Add(new IsUserRegisteredScenario
|
||||
{
|
||||
Users = new List<User>
|
||||
{
|
||||
new User { Id = 1, Auth0Id = "auth0|TestId1" },
|
||||
}.AsQueryable(),
|
||||
AuthProviderId = "auth0|TestId1",
|
||||
ExpectedResult = true
|
||||
});
|
||||
|
||||
// Not Found
|
||||
Add(new IsUserRegisteredScenario
|
||||
{
|
||||
Users = new List<User>
|
||||
{
|
||||
new User { Id = 1, Auth0Id = "auth0|TestId1" },
|
||||
}.AsQueryable(),
|
||||
AuthProviderId = "auth0|TestId2",
|
||||
ExpectedResult = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class IsUserRegisteredScenario
|
||||
{
|
||||
public IQueryable<User> Users { get; set; }
|
||||
public string AuthProviderId { get; set; }
|
||||
public bool ExpectedResult { get; set; }
|
||||
public bool ActualResult { get; set; }
|
||||
}
|
||||
}
|
||||
27
API/YABA.Service.Tests/YABA.Service.Tests.csproj
Normal file
27
API/YABA.Service.Tests/YABA.Service.Tests.csproj
Normal file
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YABA.Service\YABA.Service.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
280
API/YABA.Service/BookmarkService.cs
Normal file
280
API/YABA.Service/BookmarkService.cs
Normal file
@ -0,0 +1,280 @@
|
||||
using AutoMapper;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs.Bookmarks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
using YABA.Common.Extensions;
|
||||
using YABA.Common.Interfaces;
|
||||
using YABA.Data.Context;
|
||||
using YABA.Data.Extensions;
|
||||
using YABA.Models;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.Service
|
||||
{
|
||||
public class BookmarkService : IBookmarkService
|
||||
{
|
||||
private readonly YABAReadOnlyContext _roContext;
|
||||
private readonly YABAReadWriteContext _context;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public BookmarkService(
|
||||
YABAReadOnlyContext roContext,
|
||||
YABAReadWriteContext context,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMapper mapper)
|
||||
{
|
||||
_roContext = roContext;
|
||||
_context = context;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public IEnumerable<BookmarkDTO> GetAll(bool showHidden = false)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
var userBookmarkDTOs = new List<BookmarkDTO>();
|
||||
|
||||
|
||||
var showHiddenCondition = new Func<Bookmark, bool>(b => b.IsHidden || b.BookmarkTags.Where(x => x.Tag.UserId == currentUserId).Any(x => x.Tag.IsHidden));
|
||||
var showNotHiddenCondition = new Func<Bookmark, bool>(b => !b.IsHidden && b.BookmarkTags.Where(x => x.Tag.UserId == currentUserId).All(bt => !bt.Tag.IsHidden));
|
||||
|
||||
var filteredBookmarks = _roContext.Bookmarks
|
||||
.Include(b => b.BookmarkTags)
|
||||
.ThenInclude(bt => bt.Tag)
|
||||
.Where(b => b.UserId == currentUserId)
|
||||
.Where(showHidden ? showHiddenCondition : showNotHiddenCondition)
|
||||
.ToList();
|
||||
|
||||
foreach(var bookmark in filteredBookmarks)
|
||||
{
|
||||
var isBookmarkHidden = bookmark.IsHidden;
|
||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
||||
|
||||
var bookmarkTags = bookmark.BookmarkTags.Select(x => x.Tag);
|
||||
bookmarkDTO.Tags = _mapper.Map<List<TagDTO>>(bookmarkTags);
|
||||
|
||||
userBookmarkDTOs.Add(bookmarkDTO);
|
||||
}
|
||||
|
||||
return userBookmarkDTOs;
|
||||
}
|
||||
|
||||
public async Task<BookmarkDTO?> CreateBookmark(CreateBookmarkRequestDTO request)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
if (!_roContext.Users.UserExists(currentUserId)
|
||||
|| await _roContext.Bookmarks.AnyAsync(x => x.UserId == currentUserId && x.Url == request.Url)) return null;
|
||||
|
||||
var bookmark = _mapper.Map<Bookmark>(request);
|
||||
UpdateBookmarkWithMetaData(bookmark);
|
||||
bookmark.UserId = currentUserId;
|
||||
|
||||
var newEntity = await _context.Bookmarks.AddAsync(bookmark);
|
||||
|
||||
if (await _context.SaveChangesAsync() > 0)
|
||||
{
|
||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(newEntity.Entity);
|
||||
|
||||
if(request.Tags != null && request.Tags.Any())
|
||||
bookmarkDTO.Tags.AddRange(await UpdateBookmarkTags(bookmarkDTO.Id, request.Tags));
|
||||
return bookmarkDTO;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<BookmarkDTO?> UpdateBookmark(int id, UpdateBookmarkRequestDTO request)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
var bookmark = _context.Bookmarks.FirstOrDefault(x => x.UserId == currentUserId && x.Id == id);
|
||||
var tags = new List<TagDTO>();
|
||||
|
||||
if (request.Tags != null && request.Tags.Any())
|
||||
tags = (await UpdateBookmarkTags(id, request.Tags)).ToList();
|
||||
|
||||
|
||||
if (bookmark == null) return null;
|
||||
|
||||
bookmark.Title = !string.IsNullOrEmpty(request.Title) ? request.Title : bookmark.Title;
|
||||
bookmark.Description = !string.IsNullOrEmpty(request.Description) ? request.Description : bookmark.Description;
|
||||
bookmark.Note = !string.IsNullOrEmpty(request.Note) ? request.Note : bookmark.Note;
|
||||
bookmark.IsHidden = request.IsHidden;
|
||||
bookmark.Url = !string.IsNullOrEmpty(request.Url) ? request.Url : bookmark.Url;
|
||||
UpdateBookmarkWithMetaData(bookmark);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
||||
bookmarkDTO.Tags = tags;
|
||||
return bookmarkDTO;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TagDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == currentUserId)
|
||||
|| tags == null || !tags.Any()) return null;
|
||||
|
||||
// Add tags that are not yet in the database
|
||||
var savedUserTags = _context.Tags.Where(x => x.UserId == currentUserId).ToList();
|
||||
var tagsToSave = tags.Except(savedUserTags.Select(x => x.Name).ToHashSet()).Select(x => new Tag { Name = x, UserId = currentUserId }).ToList();
|
||||
await _context.Tags.AddRangeAsync(tagsToSave);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Add newly added tags to the lookup
|
||||
savedUserTags.AddRange(tagsToSave);
|
||||
|
||||
var existingBookmarkTags = _roContext.BookmarkTags.Include(x => x.Tag).Where(x => x.BookmarkId == id).ToList();
|
||||
var existingBookmarkTagsLookup = existingBookmarkTags.ToDictionary(k => k.TagId, v => v.Tag.Name);
|
||||
|
||||
var bookmarkTagsToRemove = existingBookmarkTagsLookup
|
||||
.Where(x => !tags.Contains(x.Value))
|
||||
.Select(x => new BookmarkTag { BookmarkId = id, TagId = x.Key });
|
||||
|
||||
var savedUserTagsByName = savedUserTags.ToDictionary(k => k.Name, v => v.Id);
|
||||
var bookmarkTagsToAdd = tags.Except(existingBookmarkTagsLookup.Values)
|
||||
.Select(x => new BookmarkTag { BookmarkId = id, TagId = savedUserTagsByName[x] });
|
||||
|
||||
_context.BookmarkTags.RemoveRange(bookmarkTagsToRemove);
|
||||
await _context.BookmarkTags.AddRangeAsync(bookmarkTagsToAdd);
|
||||
|
||||
if (await _context.SaveChangesAsync() >= 0)
|
||||
{
|
||||
var updatedBookmarkTags = _roContext.BookmarkTags
|
||||
.Include(x => x.Tag)
|
||||
.Where(x => x.BookmarkId == id)
|
||||
.Select(x => x.Tag);
|
||||
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(updatedBookmarkTags);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<BookmarkDTO?> Get(int id)
|
||||
{
|
||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||
var bookmark = await _roContext.Bookmarks.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId);
|
||||
|
||||
if (bookmark == null) return null;
|
||||
|
||||
var bookmarkTags = _roContext.BookmarkTags
|
||||
.Include(x => x.Tag)
|
||||
.Where(x => x.BookmarkId == id)
|
||||
.Select(x => x.Tag)
|
||||
.ToList();
|
||||
|
||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
||||
bookmarkDTO.Tags = _mapper.Map<List<TagDTO>>(bookmarkTags);
|
||||
|
||||
return bookmarkDTO;
|
||||
}
|
||||
|
||||
public IEnumerable<TagDTO>? GetBookmarkTags(int id)
|
||||
{
|
||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||
if (!_roContext.Bookmarks.Any(x => x.Id == id && x.UserId == userId)) return null;
|
||||
|
||||
var bookmarkTags = _roContext.BookmarkTags
|
||||
.Include(x => x.Tag)
|
||||
.Where(x => x.BookmarkId == id)
|
||||
.Select(x => x.Tag)
|
||||
.ToList();
|
||||
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(bookmarkTags);
|
||||
}
|
||||
|
||||
public async Task<int?> DeleteBookmark(int id)
|
||||
{
|
||||
var result = await DeleteBookmarks(new List<int> { id });
|
||||
return result.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>?> DeleteBookmarks(IEnumerable<int> ids)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var entriesToDelete = _context.Bookmarks.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList();
|
||||
_context.Bookmarks.RemoveRange(entriesToDelete);
|
||||
|
||||
if (await _context.SaveChangesAsync() <= 0) return null;
|
||||
|
||||
return entriesToDelete.Select(x => x.Id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>?> HideBookmarks(IEnumerable<int> ids)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var entriesToHide = _context.Bookmarks.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList();
|
||||
entriesToHide.ForEach((x) => { x.IsHidden = !x.IsHidden; });
|
||||
|
||||
if(await _context.SaveChangesAsync() <= 0) return null;
|
||||
|
||||
return entriesToHide.Select(x => x.Id);
|
||||
}
|
||||
|
||||
public IEnumerable<TagDTO> GetAllBookmarkTags(bool showHidden = false)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
var activeUserTags = _roContext.BookmarkTags
|
||||
.Include(x => x.Tag)
|
||||
.Where(x => x.Tag.UserId == currentUserId && x.Tag.IsHidden == showHidden)
|
||||
.ToList()
|
||||
.GroupBy(x => x.Tag.Id)
|
||||
.Select(g => g.First()?.Tag);
|
||||
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(activeUserTags);
|
||||
}
|
||||
|
||||
private int GetCurrentUserId()
|
||||
{
|
||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||
return userId;
|
||||
}
|
||||
|
||||
private void UpdateBookmarkWithMetaData(IBookmark bookmark)
|
||||
{
|
||||
var webClient = new WebClient();
|
||||
webClient.Headers.Add("User-Agent", "APIClient");
|
||||
var sourceData = webClient.DownloadString(bookmark.Url);
|
||||
var title = Regex.Match(sourceData, @"\<title\b[^>]*\>\s*(?<Title>[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value;
|
||||
var description = string.Empty;
|
||||
|
||||
var getHtmlDoc = new HtmlWeb();
|
||||
var document = getHtmlDoc.Load(bookmark.Url);
|
||||
var metaTags = document.DocumentNode.SelectNodes("//meta");
|
||||
if (metaTags != null)
|
||||
{
|
||||
foreach (var sitetag in metaTags)
|
||||
{
|
||||
if (sitetag.Attributes["name"] != null && sitetag.Attributes["content"] != null && sitetag.Attributes["name"].Value == "description")
|
||||
|
||||
{
|
||||
|
||||
description = sitetag.Attributes["content"].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bookmark.Title = !string.IsNullOrEmpty(bookmark.Title) ? bookmark.Title : string.IsNullOrEmpty(title) ? bookmark.Url : title;
|
||||
bookmark.Description = !string.IsNullOrEmpty(bookmark.Description) ? bookmark.Description : description;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
API/YABA.Service/Configuration/AutoMapperProfile.cs
Normal file
22
API/YABA.Service/Configuration/AutoMapperProfile.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using AutoMapper;
|
||||
using YABA.Common.DTOs;
|
||||
using YABA.Common.DTOs.Bookmarks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
using YABA.Models;
|
||||
|
||||
namespace YABA.Service.Configuration
|
||||
{
|
||||
public class AutoMapperProfile : Profile
|
||||
{
|
||||
public AutoMapperProfile()
|
||||
{
|
||||
CreateMap<User, UserDTO>();
|
||||
CreateMap<BookmarkDTO, Bookmark>();
|
||||
CreateMap<Bookmark, BookmarkDTO>();
|
||||
CreateMap<CreateBookmarkRequestDTO, Bookmark>();
|
||||
CreateMap<Tag, TagDTO>();
|
||||
CreateMap<CreateTagDTO, Tag>()
|
||||
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name.ToLowerInvariant()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.Service.Configuration
|
||||
{
|
||||
public static class DependencyInjectionConfiguration
|
||||
{
|
||||
public static void AddServiceProjectDependencyInjectionConfiguration(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
services.AddScoped<IBookmarkService, BookmarkService>();
|
||||
services.AddScoped<IMiscService, MiscService>();
|
||||
services.AddScoped<ITagsService, TagsService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
API/YABA.Service/Interfaces/IBookmarkService.cs
Normal file
22
API/YABA.Service/Interfaces/IBookmarkService.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs.Bookmarks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
|
||||
namespace YABA.Service.Interfaces
|
||||
{
|
||||
public interface IBookmarkService
|
||||
{
|
||||
Task<BookmarkDTO?> CreateBookmark(CreateBookmarkRequestDTO request);
|
||||
Task<BookmarkDTO?> UpdateBookmark(int id, UpdateBookmarkRequestDTO request);
|
||||
Task<IEnumerable<TagDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags);
|
||||
IEnumerable<BookmarkDTO> GetAll(bool isHidden = false);
|
||||
Task<BookmarkDTO?> Get(int id);
|
||||
IEnumerable<TagDTO>? GetBookmarkTags(int id);
|
||||
Task<int?> DeleteBookmark(int id);
|
||||
Task<IEnumerable<int>?> DeleteBookmarks(IEnumerable<int> ids);
|
||||
Task<IEnumerable<int>?> HideBookmarks(IEnumerable<int> ids);
|
||||
IEnumerable<TagDTO> GetAllBookmarkTags(bool showHidden = false);
|
||||
|
||||
}
|
||||
}
|
||||
9
API/YABA.Service/Interfaces/IMiscService.cs
Normal file
9
API/YABA.Service/Interfaces/IMiscService.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using YABA.Common.DTOs;
|
||||
|
||||
namespace YABA.Service.Interfaces
|
||||
{
|
||||
public interface IMiscService
|
||||
{
|
||||
public WebsiteMetaDataDTO GetWebsiteMetaData(string url);
|
||||
}
|
||||
}
|
||||
18
API/YABA.Service/Interfaces/ITagsService.cs
Normal file
18
API/YABA.Service/Interfaces/ITagsService.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
|
||||
namespace YABA.Service.Interfaces
|
||||
{
|
||||
public interface ITagsService
|
||||
{
|
||||
Task<IEnumerable<TagDTO>?> GetAll();
|
||||
Task<TagDTO?> Get(int id);
|
||||
Task<IEnumerable<TagDTO>?> UpsertTags(IEnumerable<TagDTO> tags);
|
||||
Task<TagDTO?> UpsertTag(TagDTO tag);
|
||||
Task<IEnumerable<int>?> DeleteTags(IEnumerable<int> ids);
|
||||
Task<IEnumerable<int>?> HideTags(IEnumerable<int> ids);
|
||||
Task<TagDTO?> CreateTag(CreateTagDTO request);
|
||||
Task<TagDTO?> UpdateTag(int id, UpdateTagDTO request);
|
||||
}
|
||||
}
|
||||
12
API/YABA.Service/Interfaces/IUserService.cs
Normal file
12
API/YABA.Service/Interfaces/IUserService.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs;
|
||||
|
||||
namespace YABA.Service.Interfaces
|
||||
{
|
||||
public interface IUserService
|
||||
{
|
||||
bool IsUserRegistered(string authProviderId);
|
||||
Task<UserDTO> RegisterUser(string authProviderId);
|
||||
int GetUserId(string authProviderId);
|
||||
}
|
||||
}
|
||||
53
API/YABA.Service/MiscService.cs
Normal file
53
API/YABA.Service/MiscService.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using HtmlAgilityPack;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using YABA.Common.DTOs;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.Service
|
||||
{
|
||||
public class MiscService : IMiscService
|
||||
{
|
||||
public MiscService() { }
|
||||
|
||||
public WebsiteMetaDataDTO GetWebsiteMetaData(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var webClient = new WebClient();
|
||||
webClient.Headers.Add("user-agent", "API-Application");
|
||||
var sourceData = webClient.DownloadString(url);
|
||||
var title = Regex.Match(sourceData, @"\<title\b[^>]*\>\s*(?<Title>[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value;
|
||||
var description = string.Empty;
|
||||
|
||||
var getHtmlDoc = new HtmlWeb();
|
||||
var document = getHtmlDoc.Load(url);
|
||||
var metaTags = document.DocumentNode.SelectNodes("//meta");
|
||||
if (metaTags != null)
|
||||
{
|
||||
foreach (var sitetag in metaTags)
|
||||
{
|
||||
if (sitetag.Attributes["name"] != null && sitetag.Attributes["content"] != null && sitetag.Attributes["name"].Value == "description")
|
||||
{
|
||||
description = sitetag.Attributes["content"].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new WebsiteMetaDataDTO
|
||||
{
|
||||
Title = title,
|
||||
Description = description
|
||||
};
|
||||
} catch(Exception)
|
||||
{
|
||||
return new WebsiteMetaDataDTO
|
||||
{
|
||||
Title = url,
|
||||
Description = string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
API/YABA.Service/TagsService.cs
Normal file
133
API/YABA.Service/TagsService.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs.Tags;
|
||||
using YABA.Common.Extensions;
|
||||
using YABA.Data.Context;
|
||||
using YABA.Data.Extensions;
|
||||
using YABA.Models;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.Service
|
||||
{
|
||||
public class TagsService : ITagsService
|
||||
{
|
||||
private readonly YABAReadOnlyContext _roContext;
|
||||
private readonly YABAReadWriteContext _rwContext;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public TagsService(
|
||||
YABAReadOnlyContext roContext,
|
||||
YABAReadWriteContext rwContext,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMapper mapper)
|
||||
{
|
||||
_roContext = roContext;
|
||||
_rwContext = rwContext;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TagDTO>?> GetAll()
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var activeUserTags = _roContext.Tags
|
||||
.Where(x => x.UserId == currentUserId)
|
||||
.ToList();
|
||||
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(activeUserTags);
|
||||
}
|
||||
|
||||
public async Task<TagDTO?> Get(int id)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var bookmark = await _roContext.Tags.FirstOrDefaultAsync(x => x.Id == id && x.UserId == currentUserId);
|
||||
return _mapper.Map<TagDTO>(bookmark);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TagDTO>?> UpsertTags(IEnumerable<TagDTO> tags)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var tagsToUpsert = tags.Select(x => new Tag { Id = x.Id, Name = x.Name.ToLower(), IsHidden = x.IsHidden, UserId = currentUserId});
|
||||
_rwContext.Tags.UpsertRange(tagsToUpsert);
|
||||
await _rwContext.SaveChangesAsync();
|
||||
|
||||
var newlyUpsertedTags = _roContext.Tags.Where(x => tagsToUpsert.Select(x => x.Name).Contains(x.Name)).ToList();
|
||||
return _mapper.Map<IEnumerable<TagDTO>>(newlyUpsertedTags);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>?> DeleteTags(IEnumerable<int> ids)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var entriesToDelete = _rwContext.Tags.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList();
|
||||
_rwContext.Tags.RemoveRange(entriesToDelete);
|
||||
|
||||
if (await _rwContext.SaveChangesAsync() <= 0) return null;
|
||||
|
||||
return entriesToDelete.Select(x => x.Id);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>?> HideTags(IEnumerable<int> ids)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
if (!await _roContext.Users.UserExistsAsync(currentUserId)) return null;
|
||||
|
||||
var entriesToHide = _rwContext.Tags.Where(x => x.UserId == currentUserId && ids.Contains(x.Id)).ToList();
|
||||
entriesToHide.ForEach((x) => { x.IsHidden = !x.IsHidden; });
|
||||
|
||||
if (await _rwContext.SaveChangesAsync() <= 0) return null;
|
||||
|
||||
return entriesToHide.Select(x => x.Id);
|
||||
}
|
||||
|
||||
public async Task<TagDTO?> UpsertTag(TagDTO tag) => (await UpsertTags(new List<TagDTO>() { tag }))?.FirstOrDefault();
|
||||
|
||||
public async Task<TagDTO?> CreateTag(CreateTagDTO request)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
|
||||
if(!(await _roContext.Users.UserExistsAsync(currentUserId))
|
||||
|| await _roContext.Tags.AnyAsync(x => x.UserId == currentUserId && x.Name.ToLower() == request.Name.ToLower()))
|
||||
return null;
|
||||
|
||||
var tag = _mapper.Map<Tag>(request);
|
||||
tag.UserId = currentUserId;
|
||||
|
||||
var newEntity = await _rwContext.Tags.AddAsync(tag);
|
||||
|
||||
return await _rwContext.SaveChangesAsync() > 0 ? _mapper.Map<TagDTO>(newEntity.Entity) : null;
|
||||
}
|
||||
|
||||
public async Task<TagDTO?> UpdateTag(int id, UpdateTagDTO request)
|
||||
{
|
||||
var currentUserId = GetCurrentUserId();
|
||||
var tag = await _rwContext.Tags.FirstOrDefaultAsync(x => x.UserId == currentUserId && x.Id == id);
|
||||
|
||||
if (tag == null) return null;
|
||||
|
||||
tag.Name = !string.IsNullOrEmpty(request.Name) ? request.Name.ToLower() : tag.Name;
|
||||
tag.IsHidden = request.IsHidden.HasValue ? request.IsHidden.Value : tag.IsHidden;
|
||||
|
||||
return await _rwContext.SaveChangesAsync() > 0 ? _mapper.Map<TagDTO>(tag) : null;
|
||||
}
|
||||
|
||||
private int GetCurrentUserId()
|
||||
{
|
||||
int.TryParse(_httpContextAccessor.HttpContext.User.Identity.GetUserId(), out int userId);
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
API/YABA.Service/UserService.cs
Normal file
79
API/YABA.Service/UserService.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using YABA.Common.DTOs;
|
||||
using YABA.Data.Context;
|
||||
using YABA.Models;
|
||||
using YABA.Service.Interfaces;
|
||||
|
||||
namespace YABA.Service
|
||||
{
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private readonly YABAReadOnlyContext _roContext;
|
||||
private readonly YABAReadWriteContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<UserService> _logger;
|
||||
|
||||
public UserService (YABAReadOnlyContext roContext, YABAReadWriteContext context, IMapper mapper, ILogger<UserService> logger)
|
||||
{
|
||||
_roContext = roContext;
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsUserRegistered(string authProviderId)
|
||||
{
|
||||
return _roContext.Users.Any(x => x.Auth0Id == authProviderId);
|
||||
}
|
||||
|
||||
public async Task<UserDTO> RegisterUser(string authProviderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsUserRegistered(authProviderId))
|
||||
{
|
||||
var userToRegister = new User
|
||||
{
|
||||
Auth0Id = authProviderId
|
||||
};
|
||||
|
||||
var registedUser = _context.Users.Add(userToRegister);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if(ex.InnerException is PostgresException &&
|
||||
((PostgresException)ex.InnerException).Code == "23505")
|
||||
{
|
||||
var postgresException = (PostgresException)ex.InnerException;
|
||||
_logger.LogWarning("Swallowing constraint violation: {@ConstraintName} for {@AuthProviderId}", postgresException.ConstraintName, authProviderId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
return await Get(authProviderId);
|
||||
}
|
||||
|
||||
public async Task<UserDTO> Get(string authProviderId)
|
||||
{
|
||||
var user = await _roContext.Users.FirstOrDefaultAsync(x => x.Auth0Id == authProviderId);
|
||||
return _mapper.Map<UserDTO>(user);
|
||||
}
|
||||
|
||||
public int GetUserId(string authProviderId)
|
||||
{
|
||||
var user = _roContext.Users.FirstOrDefault(x => x.Auth0Id == authProviderId);
|
||||
return user != null ? user.Id : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
API/YABA.Service/YABA.Service.csproj
Normal file
22
API/YABA.Service/YABA.Service.csproj
Normal file
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YABA.Common\YABA.Common.csproj" />
|
||||
<ProjectReference Include="..\YABA.Data\YABA.Data.csproj" />
|
||||
<ProjectReference Include="..\YABA.Models\YABA.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
67
API/YABA.sln
Normal file
67
API/YABA.sln
Normal file
@ -0,0 +1,67 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32505.173
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.API", "YABA.API\YABA.API.csproj", "{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Models", "YABA.Models\YABA.Models.csproj", "{DDA30925-F844-426B-8B90-3E6E258BD407}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Data", "YABA.Data\YABA.Data.csproj", "{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Service", "YABA.Service\YABA.Service.csproj", "{0098D0A8-0273-46F1-9FE7-B0409442251A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Common", "YABA.Common\YABA.Common.csproj", "{CA107B5D-4B8E-4515-8380-CB474C57F79C}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{17A95D6E-D7F8-40F0-A140-1556492495DB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CA49C56-9635-4BA7-A88A-DA16264E2ED2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Service.Tests", "YABA.Service.Tests\YABA.Service.Tests.csproj", "{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DDA30925-F844-426B-8B90-3E6E258BD407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DDA30925-F844-426B-8B90-3E6E258BD407}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DDA30925-F844-426B-8B90-3E6E258BD407}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DDA30925-F844-426B-8B90-3E6E258BD407}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0098D0A8-0273-46F1-9FE7-B0409442251A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0098D0A8-0273-46F1-9FE7-B0409442251A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0098D0A8-0273-46F1-9FE7-B0409442251A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0098D0A8-0273-46F1-9FE7-B0409442251A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||
{DDA30925-F844-426B-8B90-3E6E258BD407} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||
{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||
{0098D0A8-0273-46F1-9FE7-B0409442251A} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071} = {0CA49C56-9635-4BA7-A88A-DA16264E2ED2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8B677B7D-5CF6-460E-B4F6-7AF3BD655B12}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Reference in New Issue
Block a user