From cac6d7f193a3098a266ac6481fdda958967f4d54 Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Mon, 17 Aug 2026 20:50:44 -0400 Subject: [PATCH] Replace stored-procedure repositories with Dapper-based repositories Removes the USP_* stored procedures for Auth, Location, and Brewery data access in favour of services built with Dapper. Adds a new Features.Locations slice and optimistic concurrency support for the Brewery slice. --- README.md | 1 + docs/architecture.md | 102 ++--- docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md | 16 +- docs/pipeline/README.md | 28 +- docs/website/diagrams-out/database-schema.svg | 2 +- .../website/diagrams-src/database-schema.puml | 257 +++++++---- docs/website/environment-variables.md | 71 ++- docs/website/getting-started.md | 3 +- docs/website/testing.md | 8 +- docs/website/token-validation.md | 24 +- web/backend/API/API.Core/Program.cs | 4 +- web/backend/API/API.Core/appsettings.json | 2 +- web/backend/Core.slnx | 2 + .../Database.Migrations.csproj | 5 +- .../Database/Database.Migrations/Program.cs | 43 +- .../02-functions/UDF_GetCountryIdByCode.sql | 15 - .../UDF_GetStateProvinceIdByCode.sql | 13 - .../01-UserAccount/USP_CreateUserAccount.sql | 31 -- .../01-UserAccount/USP_DeleteUserAccount.sql | 20 - .../01-UserAccount/USP_GetAllUserAccounts.sql | 15 - .../USP_GetUserAccountByEmail.sql | 20 - .../01-UserAccount/USP_GetUserAccountById.sql | 20 - .../USP_GetUserAccountByUsername.sql | 20 - .../01-UserAccount/USP_UpdateUserAccount.sql | 28 -- .../USP_GetUserCredentialByUserAccountId.sql | 18 - .../02-Auth/USP_InvalidateUserCredential.sql | 28 -- .../03-crud/02-Auth/USP_RegisterUser.sql | 50 --- .../02-Auth/USP_RotateUserCredential.sql | 31 -- .../USP_AddUserVerification.sql | 27 -- .../03-crud/04-Location/USP_CreateCity.sql | 38 -- .../03-crud/04-Location/USP_CreateCountry.sql | 24 - .../04-Location/USP_CreateStateProvince.sql | 32 -- .../05-Breweries/USP_CreateBrewery.sql | 59 --- .../05-Breweries/USP_DeleteBrewery.sql | 17 - .../05-Breweries/USP_GetAllBreweries.sql | 28 -- .../05-Breweries/USP_GetBreweryById.sql | 9 - .../05-Breweries/USP_UpdateBrewery.sql | 77 ---- .../Database.Seed/Database.Seed.csproj | 4 + .../DatabaseHelpers/ConnectionStrings.cs | 40 +- web/backend/Database/Database.Seed/Program.cs | 4 +- .../Domain.Entities/Entities/BreweryPost.cs | 1 - .../Entities/BreweryPostLocation.cs | 1 - .../Domain/Domain.Entities/Entities/City.cs | 8 + .../Domain.Entities/Entities/UserAccount.cs | 3 - .../Entities/UserCredential.cs | 1 - .../Entities/UserVerification.cs | 1 - .../Repository/AuthRepositoryTests.cs | 47 +- .../Features.Auth/Features.Auth.csproj | 1 + .../Repository/AuthRepository.cs | 292 +++++-------- .../Features.Auth/Services/TokenService.cs | 7 +- .../Commands/UpdateBreweryHandlerTests.cs | 10 +- .../Repository/BreweryRepositoryTests.cs | 234 +++++++++- .../UpdateBrewery/UpdateBreweryCommand.cs | 5 +- .../UpdateBrewery/UpdateBreweryHandler.cs | 13 +- .../UpdateBrewery/UpdateBreweryValidator.cs | 13 + .../Controllers/BreweryController.cs | 6 +- ...resBreweriesServiceCollectionExtensions.cs | 3 - .../Features.Breweries.csproj | 1 + .../GetAllBreweries/GetAllBreweriesQuery.cs | 3 - .../Repository/BreweryRepository.cs | 412 +++++++++++++----- .../Repository/IBreweryRepository.cs | 17 +- .../Features.Locations.Tests.csproj | 26 ++ .../Repository/LocationRepositoryTests.cs | 114 +++++ .../Repository/TestConnectionFactory.cs | 14 + ...resLocationsServiceCollectionExtensions.cs | 13 + .../Features.Locations/Dtos/CityLocation.cs | 13 + .../Features.Locations.csproj | 20 + .../Repository/ILocationRepository.cs | 16 + .../Repository/LocationRepository.cs | 158 +++++++ .../Repository/UserAccountRepositoryTests.cs | 51 ++- .../Repository/IUserAccountRepository.cs | 15 +- .../Repository/UserAccountRepository.cs | 157 +++---- .../Rendering/IEmailTemplateProvider.cs | 10 +- .../Infrastructure.Email/IEmailProvider.cs | 2 +- .../Infrastructure.Jwt/JwtInfrastructure.cs | 14 +- .../Argon2Infrastructure.cs | 10 +- ...structureSqlServiceCollectionExtensions.cs | 12 + .../Infrastructure.Sql.csproj | 6 +- .../Infrastructure.Sql/Repository.cs | 9 +- .../SqlConnectionStringHelper.cs | 41 +- .../Infrastructure.Sql/SqlErrorCodes.cs | 41 ++ 81 files changed, 1656 insertions(+), 1401 deletions(-) delete mode 100644 web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetCountryIdByCode.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetStateProvinceIdByCode.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_CreateUserAccount.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_DeleteUserAccount.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetAllUserAccounts.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByEmail.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountById.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByUsername.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_UpdateUserAccount.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_GetUserCredentialByUserAccountId.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_InvalidateUserCredential.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RegisterUser.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RotateUserCredential.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/03-UserVerification/USP_AddUserVerification.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCity.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCountry.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateStateProvince.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_DeleteBrewery.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetAllBreweries.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql delete mode 100644 web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_UpdateBrewery.sql create mode 100644 web/backend/Domain/Domain.Entities/Entities/City.cs create mode 100644 web/backend/Features/Features.Breweries/Commands/UpdateBrewery/UpdateBreweryValidator.cs create mode 100644 web/backend/Features/Features.Locations.Tests/Features.Locations.Tests.csproj create mode 100644 web/backend/Features/Features.Locations.Tests/Repository/LocationRepositoryTests.cs create mode 100644 web/backend/Features/Features.Locations.Tests/Repository/TestConnectionFactory.cs create mode 100644 web/backend/Features/Features.Locations/DependencyInjection/FeaturesLocationsServiceCollectionExtensions.cs create mode 100644 web/backend/Features/Features.Locations/Dtos/CityLocation.cs create mode 100644 web/backend/Features/Features.Locations/Features.Locations.csproj create mode 100644 web/backend/Features/Features.Locations/Repository/ILocationRepository.cs create mode 100644 web/backend/Features/Features.Locations/Repository/LocationRepository.cs create mode 100644 web/backend/Infrastructure/Infrastructure.Sql/DependencyInjection/InfrastructureSqlServiceCollectionExtensions.cs create mode 100644 web/backend/Infrastructure/Infrastructure.Sql/SqlErrorCodes.cs diff --git a/README.md b/README.md index 116cfe8b..9dd79325 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Website + backend (active stack): - [Getting Started](docs/website/getting-started.md) - [Architecture](docs/architecture.md) +- [Database](docs/website/database.md) - [Docker Guide](docs/website/docker.md) - [Testing](docs/website/testing.md) - [Environment Variables](docs/website/environment-variables.md) diff --git a/docs/architecture.md b/docs/architecture.md index f1847776..beaf4312 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -13,8 +13,8 @@ active website: - **Architecture Style**: Vertical-slice backend plus server-rendered React frontend -The legacy Next.js frontend has been retained in `archive/next-js-web-app/` -for reference only. +The legacy Next.js frontend has been retained in `archive/next-js-web-app/` for +reference only. ## Diagrams @@ -35,9 +35,9 @@ For visual representations, see: The backend organizes business capabilities as feature slices instead of technical layers. Each feature (`Features.Auth`, `Features.Breweries`, -`Features.UserManagement`, `Features.Emails`) is a single project that owns -its own controller, MediatR commands/queries/handlers, validators, and -repository, end to end: +`Features.UserManagement`, `Features.Emails`) is a single project that owns its +own controller, MediatR commands/queries/handlers, validators, and repository, +end to end: ``` ┌───────────────────────────────────────────────────────────────────────┐ @@ -74,18 +74,18 @@ repository, end to end: └─────────────────────────────────────┘ ``` -Slices never reference each other's project. The one cross-slice -interaction (`Features.Auth` triggering a confirmation email handled by -`Features.Emails`) goes through a MediatR command (`SendRegistrationEmailCommand`) -whose contract lives in `Shared.Application`, so neither slice takes a -project reference on the other. +Slices never reference each other's project. The one cross-slice interaction +(`Features.Auth` triggering a confirmation email handled by `Features.Emails`) +goes through a MediatR command (`SendRegistrationEmailCommand`) whose contract +lives in `Shared.Application`, so neither slice takes a project reference on the +other. ### Layer responsibilities #### API layer (`API.Core`) -**Purpose**: Thin ASP.NET Core host: no business logic, no controllers of -its own +**Purpose**: Thin ASP.NET Core host: no business logic, no controllers of its +own **Components**: @@ -114,26 +114,25 @@ its own **Components** (per slice): -- `Controllers/`: HTTP endpoints, binding directly to Command/Query types - as the request contract -- `Commands//` and `Queries//`: one folder per - operation, each containing the Command/Query record, its - `IRequestHandler`, and (for commands) a FluentValidation validator -- `Repository/`: the slice's own stored-procedure repository - implementation -- `Dtos/`: response shapes returned by query handlers (never the raw - domain entity) -- `DependencyInjection/`: an `AddFeaturesX()` extension method registering - the slice's repository/services +- `Controllers/`: HTTP endpoints, binding directly to Command/Query types as the + request contract +- `Commands//` and `Queries//`: one folder per operation, + each containing the Command/Query record, its `IRequestHandler`, and (for + commands) a FluentValidation validator +- `Repository/`: the slice's own stored-procedure repository implementation +- `Dtos/`: response shapes returned by query handlers (never the raw domain + entity) +- `DependencyInjection/`: an `AddFeaturesX()` extension method registering the + slice's repository/services -`Features.Emails` has no `Controllers/` folder. It's invoked only via -MediatR commands sent from other slices, never over HTTP. +`Features.Emails` has no `Controllers/` folder. It's invoked only via MediatR +commands sent from other slices, never over HTTP. **Dependencies**: - `Domain.Entities`, `Domain.Exceptions` -- `Infrastructure.Sql` (generic ADO.NET plumbing) plus whichever - infrastructure project the slice needs (`Infrastructure.Jwt`/ +- `Infrastructure.Sql` (generic ADO.NET plumbing) plus whichever infrastructure + project the slice needs (`Infrastructure.Jwt`/ `Infrastructure.PasswordHashing` for Auth, `Infrastructure.Email`/ `Infrastructure.Email.Templates` for Emails) - `Shared.Contracts`, `Shared.Application` @@ -142,29 +141,27 @@ MediatR commands sent from other slices, never over HTTP. **Rules**: - All business logic for that feature lives in its command/query handlers -- No direct controller-to-repository calls; everything flows through - MediatR +- No direct controller-to-repository calls; everything flows through MediatR - Read endpoints return a dedicated `Dto`, never the domain entity directly #### Shared projects (`Shared.Contracts`, `Shared.Application`) -**Purpose**: The minimum cross-slice surface area required because every -slice needs it, or because duplicating it four times would be worse than -sharing it +**Purpose**: The minimum cross-slice surface area required because every slice +needs it, or because duplicating it four times would be worse than sharing it **Components**: - `Shared.Contracts`: `ResponseBody`/`ResponseBody`, the API response envelope every controller returns -- `Shared.Application`: `ValidationBehavior` (the - MediatR pipeline behavior that runs FluentValidation before a handler - executes) and the cross-slice email commands - (`SendRegistrationEmailCommand`, `SendResendConfirmationEmailCommand`) +- `Shared.Application`: `ValidationBehavior` (the MediatR + pipeline behavior that runs FluentValidation before a handler executes) and + the cross-slice email commands (`SendRegistrationEmailCommand`, + `SendResendConfirmationEmailCommand`) **Rules**: -- Kept deliberately small: this is the exception to "no slice depends on - another slice," not a general-purpose dumping ground +- Kept deliberately small: this is the exception to "no slice depends on another + slice," not a general-purpose dumping ground #### Infrastructure layer @@ -218,16 +215,15 @@ whichever slices need them #### Vertical slice + MediatR **Purpose**: Organize code by feature instead of by technical layer, so -everything needed to understand or change one capability lives in one -project +everything needed to understand or change one capability lives in one project **Implementation**: - Each HTTP write operation is a `Command` (for example, `CreateBreweryCommand` in `Features.Breweries/Commands/CreateBrewery/`); each read operation is a `Query` (for example, `GetBreweryByIdQuery`) -- Controllers bind directly to the Command/Query as the request body; - there is no separate request DTO + mapping step for writes +- Controllers bind directly to the Command/Query as the request body; there is + no separate request DTO + mapping step for writes - A single shared `ValidationBehavior` (`Shared.Application/Behaviors/`) runs FluentValidation validators in the MediatR pipeline before any handler executes @@ -250,14 +246,14 @@ public class CreateBreweryHandler(IBreweryRepository repository) : IRequestHandl **Purpose**: Abstract database access behind interfaces -**Implementation**: each slice owns its own repository, scoped to that -feature only: +**Implementation**: each slice owns its own repository, scoped to that feature +only: - `Features.Auth/Repository/IAuthRepository.cs` - `Features.Breweries/Repository/IBreweryRepository.cs` - `Features.UserManagement/Repository/IUserAccountRepository.cs` -- `Infrastructure.Sql/DefaultSqlConnectionFactory.cs`: the generic - connection factory every slice's repository builds on +- `Infrastructure.Sql/DefaultSqlConnectionFactory.cs`: the generic connection + factory every slice's repository builds on **Benefits**: @@ -279,9 +275,9 @@ public interface IAuthRepository **Purpose**: Loose coupling and testability -**Configuration**: `Program.cs` wires up MediatR/FluentValidation across -every `Features.*` assembly; each slice exposes its own `AddFeaturesX()` -extension method that registers its repository and slice-internal services +**Configuration**: `Program.cs` wires up MediatR/FluentValidation across every +`Features.*` assembly; each slice exposes its own `AddFeaturesX()` extension +method that registers its repository and slice-internal services **Lifetimes**: @@ -351,8 +347,8 @@ All component styling should prefer semantic tokens such as `primary`, ### Legacy frontend The previous Next.js frontend has been archived at `archive/next-js-web-app/` -for reference only. Active product and engineering documentation should point -to `web/frontend`. +for reference only. Active product and engineering documentation should point to +`web/frontend`. ## Security architecture @@ -411,6 +407,10 @@ to `web/frontend`. ## Database architecture +For the table-by-table schema, the stored procedure reference (parameters, +behavior, and exactly what each one throws), and the custom SQL error code +scheme, see [Database](website/database.md). + ### SQL-first philosophy **Principles**: diff --git a/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md b/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md index 87f8a798..fc54d59b 100644 --- a/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md +++ b/docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md @@ -136,8 +136,8 @@ consumers are informed of the AI-generated origin before engagement. When forced by the system prompt to generate a "highly specialized technical brewing detail," the model frequently hallucinates fermentation science and -brewing chemistry. While the resulting text sounds confident, it is -nonsensical to a reader with brewing or scientific expertise. +brewing chemistry. While the resulting text sounds confident, it is nonsensical +to a reader with brewing or scientific expertise. Small-parameter models such as Gemma 4 E4B cannot reliably satisfy prompts calling for specialist brewing detail. This is consistent with the **CHOKE** @@ -262,8 +262,8 @@ idiomatic phrasing. ] ``` -When fed into the pipeline, this dataset often causes the model to reason that -a local variant of French is needed, but it often defaults to a standardized +When fed into the pipeline, this dataset often causes the model to reason that a +local variant of French is needed, but it often defaults to a standardized dialect of French, without cultural or linguistic nuance. For languages such as Welsh (Wales), Māori (Aotearoa/New Zealand), or Sicilian @@ -312,10 +312,10 @@ not a source of truth. Simhi, Itay Itzhak, Fazl Barez, Gabriel Stanovsky, Yonatan Belinkov. [^llm-bias]: - For example, Blasi et al. (2022), "Systematic Inequalities in Language Technology - Performance across the World's Languages," _ACL Anthology_. The pattern is - consistent with models trained predominantly on English-language web - corpora. + For example, Blasi et al. (2022), "Systematic Inequalities in Language + Technology Performance across the World's Languages," _ACL Anthology_. The + pattern is consistent with models trained predominantly on English-language + web corpora. [^wp-license]: Source: diff --git a/docs/pipeline/README.md b/docs/pipeline/README.md index 33a47ce0..7916007e 100644 --- a/docs/pipeline/README.md +++ b/docs/pipeline/README.md @@ -101,21 +101,21 @@ Each run writes a fresh dated SQLite file such as #### CLI flags -| Flag | Purpose | -| ------------------ | ---------------------------------------------------------------------------------------------------- | -| `--mocked` | Deterministic mock generator, no model required. | -| `--model, -m` | Path to a GGUF file. Required unless `--mocked` is set. | +| Flag | Purpose | +| ------------------ | ------------------------------------------------------------------------------------------------------------ | +| `--mocked` | Deterministic mock generator, no model required. | +| `--model, -m` | Path to a GGUF file. Required unless `--mocked` is set. | | `--prompt-dir` | Directory containing prompt files (for example, `BREWERY_GENERATION.md`). Required unless `--mocked` is set. | -| `--output, -o` | Directory for generated SQLite artifacts. Default: `output`. | -| `--log-path` | Path for application logs. Default: `pipeline.log`. | -| `--location-count` | Number of cities to sample from `locations.json` per run. Default: `10`. | -| `--temperature` | Sampling temperature. Default: `1.0`. | -| `--top-p` | Nucleus sampling. Default: `0.95`. | -| `--top-k` | Top-k sampling. Default: `64`. | -| `--n-ctx` | Context window size. Default: `8192`. | -| `--seed` | Random seed. Default: `-1` (random at runtime). | -| `--n-gpu-layers` | Number of model layers to offload to GPU. Default: `0`. | -| `--help, -h` | Print usage and exit. | +| `--output, -o` | Directory for generated SQLite artifacts. Default: `output`. | +| `--log-path` | Path for application logs. Default: `pipeline.log`. | +| `--location-count` | Number of cities to sample from `locations.json` per run. Default: `10`. | +| `--temperature` | Sampling temperature. Default: `1.0`. | +| `--top-p` | Nucleus sampling. Default: `0.95`. | +| `--top-k` | Top-k sampling. Default: `64`. | +| `--n-ctx` | Context window size. Default: `8192`. | +| `--seed` | Random seed. Default: `-1` (random at runtime). | +| `--n-gpu-layers` | Number of model layers to offload to GPU. Default: `0`. | +| `--help, -h` | Print usage and exit. | `--mocked` and `--model` are mutually exclusive. Omitting both exits with an error before the pipeline starts. Sampling flags are ignored when `--mocked` is diff --git a/docs/website/diagrams-out/database-schema.svg b/docs/website/diagrams-out/database-schema.svg index 47169b0d..1e8c49a0 100644 --- a/docs/website/diagrams-out/database-schema.svg +++ b/docs/website/diagrams-out/database-schema.svg @@ -1 +1 @@ -Key Database Schema - User & AuthenticationKey Database Schema - User & AuthenticationUserAccountUserAccountId: INT «PK»Username: NVARCHAR(30) «UNIQUE»Email: NVARCHAR(255) «UNIQUE»FirstName: NVARCHAR(50)LastName: NVARCHAR(50)Bio: NVARCHAR(500)CreatedAt: DATETIME2UpdatedAt: DATETIME2LastLoginAt: DATETIME2UserCredentialUserCredentialId: INT «PK»UserAccountId: INT «FK»PasswordHash: VARBINARY(32)PasswordSalt: VARBINARY(16)CredentialRotatedAt: DATETIME2CredentialExpiresAt: DATETIME2CredentialRevokedAt: DATETIME2IsActive: BITCreatedAt: DATETIME2UserVerificationUserVerificationId: INT «PK»UserAccountId: INT «FK»IsVerified: BITVerifiedAt: DATETIME2VerificationToken: NVARCHAR(255)TokenExpiresAt: DATETIME2UserAvatarUserAvatarId: INT «PK»UserAccountId: INT «FK»PhotoId: INT «FK»IsActive: BITCreatedAt: DATETIME2UserFollowUserFollowId: INT «PK»FollowerUserId: INT «FK»FollowedUserId: INT «FK»CreatedAt: DATETIME2PhotoPhotoId: INT «PK»Url: NVARCHAR(500)CloudinaryPublicId: NVARCHAR(255)Width: INTHeight: INTFormat: NVARCHAR(10)CreatedAt: DATETIME2Password hashing:- Algorithm: Argon2id- Memory: 64MB- Iterations: 4- Salt: 128-bit- Hash: 256-bitAccount verificationvia email tokenwith expiryCore stored procedures:- USP_RegisterUser- USP_GetUserAccountByUsername- USP_RotateUserCredential- USP_UpdateUserAccounthashashasfollowsfollowed byrefers to \ No newline at end of file +Database SchemaUser & AuthLocationBreweryBeer (*)UserAccountUserAccountID: UNIQUEIDENTIFIER «PK»Username: VARCHAR(64) «UNIQUE»FirstName: NVARCHAR(128)LastName: NVARCHAR(128)Email: VARCHAR(128) «UNIQUE»CreatedAt: DATETIMEUpdatedAt: DATETIMEDateOfBirth: DATEUserCredentialUserCredentialID: UNIQUEIDENTIFIER «PK»UserAccountID: UNIQUEIDENTIFIER «FK»CreatedAt: DATETIMEExpiry: DATETIMEHash: NVARCHAR(256)IsRevoked: BITRevokedAt: DATETIMEUserVerificationUserVerificationID: UNIQUEIDENTIFIER «PK»UserAccountID: UNIQUEIDENTIFIER «FK, UNIQUE»VerificationDateTime: DATETIMEUserAvatar (*)UserAvatarID: UNIQUEIDENTIFIER «PK»UserAccountID: UNIQUEIDENTIFIER «FK, UNIQUE»PhotoID: UNIQUEIDENTIFIER «FK»UserFollow (*)UserFollowID: UNIQUEIDENTIFIER «PK»UserAccountID: UNIQUEIDENTIFIER «FK»FollowingID: UNIQUEIDENTIFIER «FK»CreatedAt: DATETIMEPhoto (*)PhotoID: UNIQUEIDENTIFIER «PK»Hyperlink: NVARCHAR(256)UploadedByID: UNIQUEIDENTIFIER «FK»UploadedAt: DATETIMECountryCountryID: UNIQUEIDENTIFIER «PK»CountryName: NVARCHAR(100)ISO3166_1: CHAR(2) «UNIQUE»StateProvinceStateProvinceID: UNIQUEIDENTIFIER «PK»StateProvinceName: NVARCHAR(100)ISO3166_2: CHAR(6) «UNIQUE»CountryID: UNIQUEIDENTIFIER «FK»CityCityID: UNIQUEIDENTIFIER «PK»CityName: NVARCHAR(100)StateProvinceID: UNIQUEIDENTIFIER «FK»BreweryPostBreweryPostID: UNIQUEIDENTIFIER «PK»BreweryName: NVARCHAR(256)PostedByID: UNIQUEIDENTIFIER «FK»Description: NVARCHAR(512)CreatedAt: DATETIMEUpdatedAt: DATETIMEBreweryPostLocationBreweryPostLocationID: UNIQUEIDENTIFIER «PK»BreweryPostID: UNIQUEIDENTIFIER «FK, UNIQUE»AddressLine1: NVARCHAR(256)AddressLine2: NVARCHAR(256)PostalCode: NVARCHAR(20)CityID: UNIQUEIDENTIFIER «FK»Coordinates: GEOGRAPHYBreweryPostPhoto (*)BreweryPostPhotoID: UNIQUEIDENTIFIER «PK»BreweryPostID: UNIQUEIDENTIFIER «FK»PhotoID: UNIQUEIDENTIFIER «FK»LinkedAt: DATETIMEBeerStyle (*)BeerStyleID: UNIQUEIDENTIFIER «PK»StyleName: NVARCHAR(100) «UNIQUE»Description: NVARCHAR(MAX)BeerPost (*)BeerPostID: UNIQUEIDENTIFIER «PK»Name: NVARCHAR(100)Description: NVARCHAR(MAX)ABV: DECIMAL(4,2)IBU: INTPostedByID: UNIQUEIDENTIFIER «FK»BeerStyleID: UNIQUEIDENTIFIER «FK»BrewedByID: UNIQUEIDENTIFIER «FK»CreatedAt: DATETIMEUpdatedAt: DATETIMEBeerPostPhoto (*)BeerPostPhotoID: UNIQUEIDENTIFIER «PK»BeerPostID: UNIQUEIDENTIFIER «FK»PhotoID: UNIQUEIDENTIFIER «FK»LinkedAt: DATETIMEBeerPostComment (*)BeerPostCommentID: UNIQUEIDENTIFIER «PK»Comment: NVARCHAR(250)BeerPostID: UNIQUEIDENTIFIER «FK»CommentedByID: UNIQUEIDENTIFIER «FK»Rating: INTCreatedAt: DATETIMEUpdatedAt: DATETIMECore stored procedures:- USP_RegisterUser- usp_UpdateUserAccount- usp_DeleteUserAccount- USP_RotateUserCredential- USP_CreateUserVerificationCore stored procedures:- USP_CreateBrewery- USP_UpdateBrewery- USP_DeleteBrewery(optimistic concurrency via Timer)hashashasrefers tofollows (UserAccountID)followed by (FollowingID)uploadedhashaspostedhaslocated inhaslinked topostedcategorizesbrewedhaslinked tohascommentedEvery table also has a Timer ROWVERSION column (optimistic concurrency), omitted below for brevity.Tables marked (*) have no CRUD stored procedures yet. \ No newline at end of file diff --git a/docs/website/diagrams-src/database-schema.puml b/docs/website/diagrams-src/database-schema.puml index 9c752575..095bb307 100644 --- a/docs/website/diagrams-src/database-schema.puml +++ b/docs/website/diagrams-src/database-schema.puml @@ -3,102 +3,207 @@ skinparam backgroundColor #FFFFFF skinparam defaultFontName Arial skinparam linetype ortho +skinparam packageStyle rectangle -title Key Database Schema - User & Authentication +title Database Schema +caption Every table also has a Timer ROWVERSION column (optimistic concurrency), omitted below for brevity.\nTables marked (*) have no CRUD stored procedures yet. -entity "UserAccount" as User { - * UserAccountId: INT <> - -- - * Username: NVARCHAR(30) <> - * Email: NVARCHAR(255) <> - * FirstName: NVARCHAR(50) - * LastName: NVARCHAR(50) - Bio: NVARCHAR(500) - CreatedAt: DATETIME2 - UpdatedAt: DATETIME2 - LastLoginAt: DATETIME2 +package "User & Auth" { + entity "UserAccount" as User { + * UserAccountID: UNIQUEIDENTIFIER <> + -- + * Username: VARCHAR(64) <> + * FirstName: NVARCHAR(128) + * LastName: NVARCHAR(128) + * Email: VARCHAR(128) <> + * CreatedAt: DATETIME + UpdatedAt: DATETIME + * DateOfBirth: DATE + } + + entity "UserCredential" as Cred { + * UserCredentialID: UNIQUEIDENTIFIER <> + -- + * UserAccountID: UNIQUEIDENTIFIER <> + * CreatedAt: DATETIME + * Expiry: DATETIME + * Hash: NVARCHAR(256) + * IsRevoked: BIT + RevokedAt: DATETIME + } + + entity "UserVerification" as Verify { + * UserVerificationID: UNIQUEIDENTIFIER <> + -- + * UserAccountID: UNIQUEIDENTIFIER <> + * VerificationDateTime: DATETIME + } + + entity "UserAvatar (*)" as Avatar { + * UserAvatarID: UNIQUEIDENTIFIER <> + -- + * UserAccountID: UNIQUEIDENTIFIER <> + * PhotoID: UNIQUEIDENTIFIER <> + } + + entity "UserFollow (*)" as Follow { + * UserFollowID: UNIQUEIDENTIFIER <> + -- + * UserAccountID: UNIQUEIDENTIFIER <> + * FollowingID: UNIQUEIDENTIFIER <> + * CreatedAt: DATETIME + } + + entity "Photo (*)" as Photo { + * PhotoID: UNIQUEIDENTIFIER <> + -- + Hyperlink: NVARCHAR(256) + * UploadedByID: UNIQUEIDENTIFIER <> + * UploadedAt: DATETIME + } } -entity "UserCredential" as Cred { - * UserCredentialId: INT <> - -- - * UserAccountId: INT <> - * PasswordHash: VARBINARY(32) - * PasswordSalt: VARBINARY(16) - CredentialRotatedAt: DATETIME2 - CredentialExpiresAt: DATETIME2 - CredentialRevokedAt: DATETIME2 - * IsActive: BIT - CreatedAt: DATETIME2 +package "Location" { + entity "Country" as Country { + * CountryID: UNIQUEIDENTIFIER <> + -- + * CountryName: NVARCHAR(100) + * ISO3166_1: CHAR(2) <> + } + + entity "StateProvince" as StateProvince { + * StateProvinceID: UNIQUEIDENTIFIER <> + -- + * StateProvinceName: NVARCHAR(100) + * ISO3166_2: CHAR(6) <> + * CountryID: UNIQUEIDENTIFIER <> + } + + entity "City" as City { + * CityID: UNIQUEIDENTIFIER <> + -- + * CityName: NVARCHAR(100) + * StateProvinceID: UNIQUEIDENTIFIER <> + } } -entity "UserVerification" as Verify { - * UserVerificationId: INT <> - -- - * UserAccountId: INT <> - * IsVerified: BIT - VerifiedAt: DATETIME2 - VerificationToken: NVARCHAR(255) - TokenExpiresAt: DATETIME2 +package "Brewery" { + entity "BreweryPost" as Brewery { + * BreweryPostID: UNIQUEIDENTIFIER <> + -- + * BreweryName: NVARCHAR(256) + * PostedByID: UNIQUEIDENTIFIER <> + * Description: NVARCHAR(512) + * CreatedAt: DATETIME + UpdatedAt: DATETIME + } + + entity "BreweryPostLocation" as BreweryLoc { + * BreweryPostLocationID: UNIQUEIDENTIFIER <> + -- + * BreweryPostID: UNIQUEIDENTIFIER <> + * AddressLine1: NVARCHAR(256) + AddressLine2: NVARCHAR(256) + * PostalCode: NVARCHAR(20) + * CityID: UNIQUEIDENTIFIER <> + Coordinates: GEOGRAPHY + } + + entity "BreweryPostPhoto (*)" as BreweryPhoto { + * BreweryPostPhotoID: UNIQUEIDENTIFIER <> + -- + * BreweryPostID: UNIQUEIDENTIFIER <> + * PhotoID: UNIQUEIDENTIFIER <> + * LinkedAt: DATETIME + } } -entity "UserAvatar" as Avatar { - * UserAvatarId: INT <> - -- - * UserAccountId: INT <> - PhotoId: INT <> - * IsActive: BIT - CreatedAt: DATETIME2 +package "Beer (*)" { + entity "BeerStyle (*)" as BeerStyle { + * BeerStyleID: UNIQUEIDENTIFIER <> + -- + * StyleName: NVARCHAR(100) <> + Description: NVARCHAR(MAX) + } + + entity "BeerPost (*)" as Beer { + * BeerPostID: UNIQUEIDENTIFIER <> + -- + * Name: NVARCHAR(100) + * Description: NVARCHAR(MAX) + * ABV: DECIMAL(4,2) + * IBU: INT + * PostedByID: UNIQUEIDENTIFIER <> + * BeerStyleID: UNIQUEIDENTIFIER <> + * BrewedByID: UNIQUEIDENTIFIER <> + * CreatedAt: DATETIME + UpdatedAt: DATETIME + } + + entity "BeerPostPhoto (*)" as BeerPhoto { + * BeerPostPhotoID: UNIQUEIDENTIFIER <> + -- + * BeerPostID: UNIQUEIDENTIFIER <> + * PhotoID: UNIQUEIDENTIFIER <> + * LinkedAt: DATETIME + } + + entity "BeerPostComment (*)" as BeerComment { + * BeerPostCommentID: UNIQUEIDENTIFIER <> + -- + * Comment: NVARCHAR(250) + * BeerPostID: UNIQUEIDENTIFIER <> + * CommentedByID: UNIQUEIDENTIFIER <> + * Rating: INT + * CreatedAt: DATETIME + UpdatedAt: DATETIME + } } -entity "UserFollow" as Follow { - * UserFollowId: INT <> - -- - * FollowerUserId: INT <> - * FollowedUserId: INT <> - CreatedAt: DATETIME2 -} - -entity "Photo" as Photo { - * PhotoId: INT <> - -- - * Url: NVARCHAR(500) - * CloudinaryPublicId: NVARCHAR(255) - Width: INT - Height: INT - Format: NVARCHAR(10) - CreatedAt: DATETIME2 -} - -' Relationships +' Relationships: User & Auth User ||--o{ Cred : "has" User ||--o| Verify : "has" -User ||--o{ Avatar : "has" -User ||--o{ Follow : "follows" -User ||--o{ Follow : "followed by" +User ||--o| Avatar : "has" Avatar }o--|| Photo : "refers to" +User ||--o{ Follow : "follows (UserAccountID)" +User ||--o{ Follow : "followed by (FollowingID)" +User ||--o{ Photo : "uploaded" -note right of Cred - Password hashing: - - Algorithm: Argon2id - - Memory: 64MB - - Iterations: 4 - - Salt: 128-bit - - Hash: 256-bit -end note +' Relationships: Location +Country ||--o{ StateProvince : "has" +StateProvince ||--o{ City : "has" -note right of Verify - Account verification - via email token - with expiry -end note +' Relationships: Brewery +User ||--o{ Brewery : "posted" +Brewery ||--o| BreweryLoc : "has" +City ||--o{ BreweryLoc : "located in" +Brewery ||--o{ BreweryPhoto : "has" +Photo ||--o{ BreweryPhoto : "linked to" + +' Relationships: Beer +User ||--o{ Beer : "posted" +BeerStyle ||--o{ Beer : "categorizes" +Brewery ||--o{ Beer : "brewed" +Beer ||--o{ BeerPhoto : "has" +Photo ||--o{ BeerPhoto : "linked to" +Beer ||--o{ BeerComment : "has" +User ||--o{ BeerComment : "commented" note bottom of User Core stored procedures: - USP_RegisterUser - - USP_GetUserAccountByUsername + - usp_UpdateUserAccount + - usp_DeleteUserAccount - USP_RotateUserCredential - - USP_UpdateUserAccount + - USP_CreateUserVerification +end note + +note bottom of Brewery + Core stored procedures: + - USP_CreateBrewery + - USP_UpdateBrewery + - USP_DeleteBrewery + (optimistic concurrency via Timer) end note @enduml diff --git a/docs/website/environment-variables.md b/docs/website/environment-variables.md index d4360183..d0b4ca93 100644 --- a/docs/website/environment-variables.md +++ b/docs/website/environment-variables.md @@ -166,8 +166,8 @@ NODE_ENV=development # Standard Node runtime mode ### SMTP configuration (backend) -Read by `Infrastructure.Email/SmtpEmailProvider.cs` for sending confirmation -and account emails. +Read by `Infrastructure.Email/SmtpEmailProvider.cs` for sending confirmation and +account emails. ```bash SMTP_HOST=mailpit # Required, no default @@ -183,8 +183,8 @@ SMTP_FROM_NAME=The Biergarten App # Optional, defaults to "The Biergarten startup if `SMTP_HOST` or `SMTP_FROM_EMAIL` is missing - **Local dev**: point at the `mailpit` Docker service (SMTP on port 1025, web UI on http://localhost:8025) -- **Production**: point at a real provider (SendGrid, Mailgun, Amazon SES, - and so on) +- **Production**: point at a real provider (SendGrid, Mailgun, Amazon SES, and + so on) ## Docker-specific variables @@ -234,35 +234,35 @@ reference only and is not run as part of the active stack. ## Variable reference table -| Variable | Backend | Frontend | Docker | Required | Notes | -| ----------------------------- | :-----: | :------: | :----: | :------: | -------------------------- | -| `DB_SERVER` | ✓ | | ✓ | Yes\* | SQL Server address | -| `DB_NAME` | ✓ | | ✓ | Yes\* | Database name | -| `DB_USER` | ✓ | | ✓ | Yes\* | SQL username | -| `DB_PASSWORD` | ✓ | | ✓ | Yes\* | SQL password | -| `DB_CONNECTION_STRING` | ✓ | | | Yes\* | Alternative to components | -| `DB_TRUST_SERVER_CERTIFICATE` | ✓ | | ✓ | No | Defaults to `True` | -| `ACCESS_TOKEN_SECRET` | ✓ | | ✓ | Yes | Access token signing | -| `REFRESH_TOKEN_SECRET` | ✓ | | ✓ | Yes | Refresh token signing | -| `CONFIRMATION_TOKEN_SECRET` | ✓ | | ✓ | Yes | Confirmation token signing | -| `WEBSITE_BASE_URL` | ✓ | | | Yes | Website URL for emails | -| `SMTP_HOST` | ✓ | | ✓ | Yes | SMTP server host | -| `SMTP_PORT` | ✓ | | ✓ | No | Defaults to `587` | -| `SMTP_USERNAME` | ✓ | | ✓ | No | SMTP auth username | -| `SMTP_PASSWORD` | ✓ | | ✓ | No | SMTP auth password | -| `SMTP_USE_SSL` | ✓ | | ✓ | No | Defaults to `true` | -| `SMTP_FROM_EMAIL` | ✓ | | ✓ | Yes | Email sender address | -| `SMTP_FROM_NAME` | ✓ | | ✓ | No | Defaults to `The Biergarten` | -| `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL | -| `SESSION_SECRET` | | ✓ | | Yes | Website session signing | -| `NODE_ENV` | | ✓ | | No | Runtime mode | -| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag | -| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment | -| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address | -| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container | -| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA | -| `MSSQL_PID` | | | ✓ | No | SQL Server edition | -| `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag | +| Variable | Backend | Frontend | Docker | Required | Notes | +| ----------------------------- | :-----: | :------: | :----: | :------: | ---------------------------- | +| `DB_SERVER` | ✓ | | ✓ | Yes\* | SQL Server address | +| `DB_NAME` | ✓ | | ✓ | Yes\* | Database name | +| `DB_USER` | ✓ | | ✓ | Yes\* | SQL username | +| `DB_PASSWORD` | ✓ | | ✓ | Yes\* | SQL password | +| `DB_CONNECTION_STRING` | ✓ | | | Yes\* | Alternative to components | +| `DB_TRUST_SERVER_CERTIFICATE` | ✓ | | ✓ | No | Defaults to `True` | +| `ACCESS_TOKEN_SECRET` | ✓ | | ✓ | Yes | Access token signing | +| `REFRESH_TOKEN_SECRET` | ✓ | | ✓ | Yes | Refresh token signing | +| `CONFIRMATION_TOKEN_SECRET` | ✓ | | ✓ | Yes | Confirmation token signing | +| `WEBSITE_BASE_URL` | ✓ | | | Yes | Website URL for emails | +| `SMTP_HOST` | ✓ | | ✓ | Yes | SMTP server host | +| `SMTP_PORT` | ✓ | | ✓ | No | Defaults to `587` | +| `SMTP_USERNAME` | ✓ | | ✓ | No | SMTP auth username | +| `SMTP_PASSWORD` | ✓ | | ✓ | No | SMTP auth password | +| `SMTP_USE_SSL` | ✓ | | ✓ | No | Defaults to `true` | +| `SMTP_FROM_EMAIL` | ✓ | | ✓ | Yes | Email sender address | +| `SMTP_FROM_NAME` | ✓ | | ✓ | No | Defaults to `The Biergarten` | +| `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL | +| `SESSION_SECRET` | | ✓ | | Yes | Website session signing | +| `NODE_ENV` | | ✓ | | No | Runtime mode | +| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag | +| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment | +| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address | +| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container | +| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA | +| `MSSQL_PID` | | | ✓ | No | SQL Server edition | +| `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag | \* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided. @@ -276,9 +276,8 @@ Variables are validated at startup: - Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`, DB connection values) cause the application to fail with an `InvalidOperationException` -- No minimum length is enforced on the token secrets in code; the - "minimum 32 characters" guidance above is a recommendation, not an - enforced check +- No minimum length is enforced on the token secrets in code; the "minimum 32 + characters" guidance above is a recommendation, not an enforced check ### Frontend validation diff --git a/docs/website/getting-started.md b/docs/website/getting-started.md index ecbf4998..440ea9aa 100644 --- a/docs/website/getting-started.md +++ b/docs/website/getting-started.md @@ -46,7 +46,8 @@ See [Environment Variables](environment-variables.md) for the full list. docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d ``` -This starts SQL Server, migrations, seeding, the API, and Mailpit (local dev SMTP). +This starts SQL Server, migrations, seeding, the API, and Mailpit (local dev +SMTP). Available endpoints: diff --git a/docs/website/testing.md b/docs/website/testing.md index 850caaea..3baddcda 100644 --- a/docs/website/testing.md +++ b/docs/website/testing.md @@ -7,8 +7,8 @@ Biergarten App. The project uses a multi-layered testing approach across backend and frontend: -- **API.Specs** - BDD integration tests using Reqnroll (Gherkin), run against - a live, seeded database +- **API.Specs** - BDD integration tests using Reqnroll (Gherkin), run against a + live, seeded database - **Features.\*.Tests** - One unit test project per backend feature slice (`Features.Auth.Tests`, `Features.Breweries.Tests`, `Features.UserManagement.Tests`, `Features.Emails.Tests`), covering that @@ -77,8 +77,8 @@ dotnet test API/API.Specs/API.Specs.csproj ### Feature slice unit tests -Each feature slice has its own test project, covering its command/query -handlers and repository: +Each feature slice has its own test project, covering its command/query handlers +and repository: ```bash cd web/backend diff --git a/docs/website/token-validation.md b/docs/website/token-validation.md index d4efaa73..6eb5b90e 100644 --- a/docs/website/token-validation.md +++ b/docs/website/token-validation.md @@ -34,8 +34,8 @@ Low-level JWT operations. #### [ITokenService](../../web/backend/Features/Features.Auth/Services/ITokenService.cs) -Both token generation and validation live on the same slice-internal -service (there is no separate validation service/class). +Both token generation and validation live on the same slice-internal service +(there is no separate validation service/class). **Generation methods:** @@ -56,18 +56,18 @@ service (there is no separate validation service/class). - `Username` (string) - `Principal` (ClaimsPrincipal) - Full JWT claims -**Implementation:** [TokenService.cs](../../web/backend/Features/Features.Auth/Services/TokenService.cs) +**Implementation:** +[TokenService.cs](../../web/backend/Features/Features.Auth/Services/TokenService.cs) - Reads token secrets from environment variables - Extracts and validates claims (Sub, UniqueName) - Throws `UnauthorizedException` on validation failure -`TokenService` is registered by `Features.Auth`'s own -`AddFeaturesAuth()` extension method, except for the lower-level -`ITokenInfrastructure` (JWT signing/verification) it depends on, which is -registered by the host (`API.Core/Program.cs`) since -`JwtAuthenticationHandler` (host-level auth middleware) also depends on -it directly. +`TokenService` is registered by `Features.Auth`'s own `AddFeaturesAuth()` +extension method, except for the lower-level `ITokenInfrastructure` (JWT +signing/verification) it depends on, which is registered by the host +(`API.Core/Program.cs`) since `JwtAuthenticationHandler` (host-level auth +middleware) also depends on it directly. ### Integration points @@ -97,9 +97,9 @@ it directly. **Flow:** 1. Receives a refresh token via `RefreshTokenCommand` -2. Delegates to `ITokenService.RefreshTokenAsync()`, which validates the - token, retrieves the user account via `IAuthRepository.GetUserByIdAsync()`, - and issues a new access/refresh token pair +2. Delegates to `ITokenService.RefreshTokenAsync()`, which validates the token, + retrieves the user account via `IAuthRepository.GetUserByIdAsync()`, and + issues a new access/refresh token pair 3. Maps the result onto `LoginPayload` #### [AuthController](../../web/backend/Features/Features.Auth/Controllers/AuthController.cs) diff --git a/web/backend/API/API.Core/Program.cs b/web/backend/API/API.Core/Program.cs index 326a1e15..474e7176 100644 --- a/web/backend/API/API.Core/Program.cs +++ b/web/backend/API/API.Core/Program.cs @@ -11,7 +11,7 @@ using Features.UserManagement.DependencyInjection; using FluentValidation; using FluentValidation.AspNetCore; using Infrastructure.Jwt; -using Infrastructure.Sql; +using Infrastructure.Sql.DependencyInjection; using Shared.Application.Behaviors; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -60,7 +60,7 @@ if (!builder.Environment.IsProduction()) // Configure Dependency Injection ------------------------------------------------------------------------------------- -builder.Services.AddSingleton(); +builder.Services.AddInfrastructureSql(); builder.Services.AddFeaturesBreweries(); builder.Services.AddFeaturesUserManagement(); diff --git a/web/backend/API/API.Core/appsettings.json b/web/backend/API/API.Core/appsettings.json index 04953465..d4810ac2 100644 --- a/web/backend/API/API.Core/appsettings.json +++ b/web/backend/API/API.Core/appsettings.json @@ -12,7 +12,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "" + "Default": "" }, "Jwt": { "SecretKey": "", diff --git a/web/backend/Core.slnx b/web/backend/Core.slnx index 44d81d53..7d869556 100644 --- a/web/backend/Core.slnx +++ b/web/backend/Core.slnx @@ -27,6 +27,8 @@ + + diff --git a/web/backend/Database/Database.Migrations/Database.Migrations.csproj b/web/backend/Database/Database.Migrations/Database.Migrations.csproj index 0ec05c6c..35e0da20 100644 --- a/web/backend/Database/Database.Migrations/Database.Migrations.csproj +++ b/web/backend/Database/Database.Migrations/Database.Migrations.csproj @@ -10,7 +10,10 @@ - + + + + diff --git a/web/backend/Database/Database.Migrations/Program.cs b/web/backend/Database/Database.Migrations/Program.cs index b3d973ec..202d5e06 100644 --- a/web/backend/Database/Database.Migrations/Program.cs +++ b/web/backend/Database/Database.Migrations/Program.cs @@ -2,6 +2,7 @@ using System.Reflection; using DbUp; using DbUp.Engine; +using Infrastructure.Sql; using Microsoft.Data.SqlClient; namespace Database.Migrations; @@ -96,12 +97,6 @@ public static class Program Console.WriteLine($"Database '{settings.DatabaseName}' is present."); } - /// - /// Opens a connection, executes a non-query batch parameterised by database name, and - /// disposes both the command and the connection. - /// - /// The connection string to open. - /// The Transact-SQL batch to execute. /// The value bound to the @databaseName parameter. private static async Task ExecuteNonQueryAsync( string connectionString, @@ -171,16 +166,11 @@ public static class Program /// public static DatabaseSettings FromEnvironment() { - string server = Required("DB_SERVER"); - string databaseName = Required("DB_NAME"); - string user = Required("DB_USER"); - string password = Required("DB_PASSWORD"); - - bool trustServerCertificate = - !bool.TryParse( - Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE"), - out bool parsedTrust - ) || parsedTrust; + string databaseName = + Environment.GetEnvironmentVariable("DB_NAME") + ?? throw new InvalidOperationException( + "The DB_NAME environment variable is not set." + ); bool clearDatabase = string.Equals( Environment.GetEnvironmentVariable("CLEAR_DATABASE"), @@ -189,28 +179,11 @@ public static class Program ); return new DatabaseSettings( - Build(databaseName), - Build("master"), + SqlConnectionStringHelper.BuildConnectionString(databaseName), + SqlConnectionStringHelper.BuildMasterConnectionString(), databaseName, clearDatabase ); - - string Build(string initialCatalog) => - new SqlConnectionStringBuilder - { - DataSource = server, - InitialCatalog = initialCatalog, - UserID = user, - Password = password, - Encrypt = true, - TrustServerCertificate = trustServerCertificate, - }.ConnectionString; } - - private static string Required(string variableName) => - Environment.GetEnvironmentVariable(variableName) - ?? throw new InvalidOperationException( - $"The {variableName} environment variable is not set." - ); } } diff --git a/web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetCountryIdByCode.sql b/web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetCountryIdByCode.sql deleted file mode 100644 index c0ce88f1..00000000 --- a/web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetCountryIdByCode.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE OR ALTER FUNCTION dbo.UDF_GetCountryIdByCode( - @CountryCode NVARCHAR(2) -) - RETURNS UNIQUEIDENTIFIER -AS -BEGIN - DECLARE - @CountryId UNIQUEIDENTIFIER; - - SELECT @CountryId = CountryID - FROM dbo.Country - WHERE ISO3166_1 = @CountryCode; - - RETURN @CountryId; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetStateProvinceIdByCode.sql b/web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetStateProvinceIdByCode.sql deleted file mode 100644 index baea6063..00000000 --- a/web/backend/Database/Database.Migrations/scripts/02-functions/UDF_GetStateProvinceIdByCode.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE OR ALTER FUNCTION dbo.UDF_GetStateProvinceIdByCode( - @StateProvinceCode NVARCHAR(6) -) - RETURNS UNIQUEIDENTIFIER -AS -BEGIN - DECLARE - @StateProvinceId UNIQUEIDENTIFIER; - SELECT @StateProvinceId = StateProvinceID - FROM dbo.StateProvince - WHERE ISO3166_2 = @StateProvinceCode; - RETURN @StateProvinceId; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_CreateUserAccount.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_CreateUserAccount.sql deleted file mode 100644 index e8cc9351..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_CreateUserAccount.sql +++ /dev/null @@ -1,31 +0,0 @@ -CREATE OR ALTER PROCEDURE usp_CreateUserAccount( - @UserAccountId UNIQUEIDENTIFIER OUTPUT, - @Username VARCHAR(64), - @FirstName NVARCHAR(128), - @LastName NVARCHAR(128), - @DateOfBirth DATETIME, - @Email VARCHAR(128) -) -AS -BEGIN - SET - NOCOUNT ON; - - DECLARE - @Inserted TABLE - ( - UserAccountID UNIQUEIDENTIFIER - ); - - INSERT INTO UserAccount - (Username, - FirstName, - LastName, - DateOfBirth, - Email) - OUTPUT INSERTED.UserAccountID INTO @Inserted - VALUES (@Username, @FirstName, @LastName, @DateOfBirth, @Email); - - SELECT @UserAccountId = UserAccountID - FROM @Inserted; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_DeleteUserAccount.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_DeleteUserAccount.sql deleted file mode 100644 index 58f7b238..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_DeleteUserAccount.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE OR ALTER PROCEDURE usp_DeleteUserAccount( - @UserAccountId UNIQUEIDENTIFIER -) -AS -BEGIN - SET NOCOUNT ON - - IF NOT EXISTS (SELECT 1 FROM UserAccount WHERE UserAccountId = @UserAccountId) - BEGIN - RAISERROR - ('UserAccount with the specified ID does not exist.', 16, - 1); - ROLLBACK TRANSACTION - RETURN - END - - DELETE - FROM UserAccount - WHERE UserAccountId = @UserAccountId; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetAllUserAccounts.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetAllUserAccounts.sql deleted file mode 100644 index e23d90a0..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetAllUserAccounts.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts -AS -BEGIN - SET NOCOUNT ON; - SELECT UserAccountID, - Username, - FirstName, - LastName, - Email, - CreatedAt, - UpdatedAt, - DateOfBirth, - Timer - FROM dbo.UserAccount; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByEmail.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByEmail.sql deleted file mode 100644 index 3ac92b53..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByEmail.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail( - @Email VARCHAR(128) -) -AS -BEGIN - SET - NOCOUNT ON; - - SELECT UserAccountID, - Username, - FirstName, - LastName, - Email, - CreatedAt, - UpdatedAt, - DateOfBirth, - Timer - FROM dbo.UserAccount - WHERE Email = @Email; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountById.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountById.sql deleted file mode 100644 index 0a83ebf5..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountById.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE OR ALTER PROCEDURE USP_GetUserAccountById( - @UserAccountId UNIQUEIDENTIFIER -) -AS -BEGIN - SET - NOCOUNT ON; - - SELECT UserAccountID, - Username, - FirstName, - LastName, - Email, - CreatedAt, - UpdatedAt, - DateOfBirth, - Timer - FROM dbo.UserAccount - WHERE UserAccountID = @UserAccountId; -END diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByUsername.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByUsername.sql deleted file mode 100644 index 2b8919d5..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_GetUserAccountByUsername.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername( - @Username VARCHAR(64) -) -AS -BEGIN - SET - NOCOUNT ON; - - SELECT UserAccountID, - Username, - FirstName, - LastName, - Email, - CreatedAt, - UpdatedAt, - DateOfBirth, - Timer - FROM dbo.UserAccount - WHERE Username = @Username; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_UpdateUserAccount.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_UpdateUserAccount.sql deleted file mode 100644 index be30d16e..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/01-UserAccount/USP_UpdateUserAccount.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE OR ALTER PROCEDURE usp_UpdateUserAccount( - @Username VARCHAR(64), - @FirstName NVARCHAR(128), - @LastName NVARCHAR(128), - @DateOfBirth DATETIME, - @Email VARCHAR(128), - @UserAccountId UNIQUEIDENTIFIER -) -AS -BEGIN - SET - NOCOUNT ON; - - UPDATE UserAccount - SET Username = @Username, - FirstName = @FirstName, - LastName = @LastName, - DateOfBirth = @DateOfBirth, - Email = @Email - WHERE UserAccountId = @UserAccountId; - - IF - @@ROWCOUNT = 0 - BEGIN - THROW - 50001, 'UserAccount with the specified ID does not exist.', 1; - END -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_GetUserCredentialByUserAccountId.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_GetUserCredentialByUserAccountId.sql deleted file mode 100644 index e962cb26..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_GetUserCredentialByUserAccountId.sql +++ /dev/null @@ -1,18 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_GetActiveUserCredentialByUserAccountId( - @UserAccountId UNIQUEIDENTIFIER -) -AS -BEGIN - SET - NOCOUNT ON; - - SELECT UserCredentialId, - UserAccountId, - Hash, - IsRevoked, - CreatedAt, - RevokedAt - FROM dbo.UserCredential - WHERE UserAccountId = @UserAccountId - AND IsRevoked = 0; -END; \ No newline at end of file diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_InvalidateUserCredential.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_InvalidateUserCredential.sql deleted file mode 100644 index b655f2f2..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_InvalidateUserCredential.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_InvalidateUserCredential( - @UserAccountId_ UNIQUEIDENTIFIER -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - - BEGIN - TRANSACTION; - - EXEC dbo.USP_GetUserAccountByID @UserAccountId = @UserAccountId_; - IF - @@ROWCOUNT = 0 - THROW 50001, 'User account not found', 1; - - -- invalidate all other credentials by setting them to revoked - UPDATE dbo.UserCredential - SET IsRevoked = 1, - RevokedAt = GETDATE() - WHERE UserAccountId = @UserAccountId_ - AND IsRevoked != 1; - - - COMMIT TRANSACTION; -END; \ No newline at end of file diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RegisterUser.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RegisterUser.sql deleted file mode 100644 index 4c2a4597..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RegisterUser.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_RegisterUser( - @Username VARCHAR(64), - @FirstName NVARCHAR(128), - @LastName NVARCHAR(128), - @DateOfBirth DATETIME, - @Email VARCHAR(128), - @Hash NVARCHAR(MAX) -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - - DECLARE - @UserAccountId_ UNIQUEIDENTIFIER; - - BEGIN - TRANSACTION; - - EXEC usp_CreateUserAccount - @UserAccountId = @UserAccountId_ OUTPUT, - @Username = @Username, - @FirstName = @FirstName, - @LastName = @LastName, - @DateOfBirth = @DateOfBirth, - @Email = @Email; - - IF - @UserAccountId_ IS NULL - BEGIN - THROW - 50000, 'Failed to create user account.', 1; - END - - INSERT INTO dbo.UserCredential - (UserAccountId, Hash) - VALUES (@UserAccountId_, @Hash); - - IF - @@ROWCOUNT = 0 - BEGIN - THROW - 50002, 'Failed to create user credential.', 1; - END - COMMIT TRANSACTION; - - SELECT @UserAccountId_ AS UserAccountId; -END diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RotateUserCredential.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RotateUserCredential.sql deleted file mode 100644 index 3408b182..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/02-Auth/USP_RotateUserCredential.sql +++ /dev/null @@ -1,31 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_RotateUserCredential( - @UserAccountId_ UNIQUEIDENTIFIER, - @Hash NVARCHAR(MAX) -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - BEGIN - TRANSACTION; - - EXEC USP_GetUserAccountByID @UserAccountId = @UserAccountId_ - - IF @@ROWCOUNT = 0 - THROW 50001, 'User account not found', 1; - - - -- invalidate all other credentials -- set them to revoked - UPDATE dbo.UserCredential - SET IsRevoked = 1, - RevokedAt = GETDATE() - WHERE UserAccountId = @UserAccountId_; - - INSERT INTO dbo.UserCredential - (UserAccountId, Hash) - VALUES (@UserAccountId_, @Hash); - - -END; \ No newline at end of file diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/03-UserVerification/USP_AddUserVerification.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/03-UserVerification/USP_AddUserVerification.sql deleted file mode 100644 index bdfe538d..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/03-UserVerification/USP_AddUserVerification.sql +++ /dev/null @@ -1,27 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_CreateUserVerification @UserAccountID_ UNIQUEIDENTIFIER, - @VerificationDateTime DATETIME = NULL -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - - IF - @VerificationDateTime IS NULL - SET @VerificationDateTime = GETDATE(); - - BEGIN - TRANSACTION; - - EXEC USP_GetUserAccountByID @UserAccountId = @UserAccountID_; - IF - @@ROWCOUNT = 0 - THROW 50001, 'Could not find a user with that id', 1; - - INSERT INTO dbo.UserVerification - (UserAccountId, VerificationDateTime) - VALUES (@UserAccountID_, @VerificationDateTime); - - COMMIT TRANSACTION; -END diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCity.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCity.sql deleted file mode 100644 index e95a4960..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCity.sql +++ /dev/null @@ -1,38 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_CreateCity( - @CityName NVARCHAR(100), - @StateProvinceCode NVARCHAR(6) -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - - BEGIN - TRANSACTION - DECLARE - @StateProvinceId UNIQUEIDENTIFIER = dbo.UDF_GetStateProvinceIdByCode(@StateProvinceCode); - IF - @StateProvinceId IS NULL - BEGIN - THROW - 50001, 'State/province does not exist', 1; - END - - IF - EXISTS (SELECT 1 - FROM dbo.City - WHERE CityName = @CityName - AND StateProvinceID = @StateProvinceId) - BEGIN - - THROW - 50002, 'City already exists.', 1; - END - - INSERT INTO dbo.City - (StateProvinceID, CityName) - VALUES (@StateProvinceId, @CityName); - COMMIT TRANSACTION -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCountry.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCountry.sql deleted file mode 100644 index 0f60e353..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateCountry.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_CreateCountry( - @CountryName NVARCHAR(100), - @ISO3166_1 NVARCHAR(2) -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - BEGIN - TRANSACTION; - - IF - EXISTS (SELECT 1 - FROM dbo.Country - WHERE ISO3166_1 = @ISO3166_1) - THROW 50001, 'Country already exists', 1; - - INSERT INTO dbo.Country - (CountryName, ISO3166_1) - VALUES (@CountryName, @ISO3166_1); - COMMIT TRANSACTION; -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateStateProvince.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateStateProvince.sql deleted file mode 100644 index 893e52e2..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/04-Location/USP_CreateStateProvince.sql +++ /dev/null @@ -1,32 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_CreateStateProvince( - @StateProvinceName NVARCHAR(100), - @ISO3166_2 NVARCHAR(6), - @CountryCode NVARCHAR(2) -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - - IF - EXISTS (SELECT 1 - FROM dbo.StateProvince - WHERE ISO3166_2 = @ISO3166_2) - RETURN; - - DECLARE - @CountryId UNIQUEIDENTIFIER = dbo.UDF_GetCountryIdByCode(@CountryCode); - IF - @CountryId IS NULL - BEGIN - THROW - 50001, 'Country does not exist', 1; - - END - - INSERT INTO dbo.StateProvince - (StateProvinceName, ISO3166_2, CountryID) - VALUES (@StateProvinceName, @ISO3166_2, @CountryId); -END; diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql deleted file mode 100644 index 999c1588..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_CreateBrewery.sql +++ /dev/null @@ -1,59 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_CreateBrewery( - @BreweryName NVARCHAR(256), - @Description NVARCHAR(512), - @PostedByID UNIQUEIDENTIFIER, - @CityID UNIQUEIDENTIFIER, - @AddressLine1 NVARCHAR(256), - @AddressLine2 NVARCHAR(256) = NULL, - @PostalCode NVARCHAR(20), - @Coordinates GEOGRAPHY = NULL -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - - IF - @BreweryName IS NULL - THROW 50001, 'Brewery name cannot be null.', 1; - - IF - @Description IS NULL - THROW 50002, 'Brewery description cannot be null.', 1; - - IF - NOT EXISTS (SELECT 1 - FROM dbo.UserAccount - WHERE UserAccountID = @PostedByID) - THROW 50404, 'User not found.', 1; - - IF - NOT EXISTS (SELECT 1 - FROM dbo.City - WHERE CityID = @CityID) - THROW 50404, 'City not found.', 1; - - DECLARE - @NewBreweryID UNIQUEIDENTIFIER = NEWID(); - DECLARE - @NewBrewerLocationID UNIQUEIDENTIFIER = NEWID(); - - BEGIN - TRANSACTION; - - INSERT INTO dbo.BreweryPost - (BreweryPostID, BreweryName, Description, PostedByID) - VALUES (@NewBreweryID, @BreweryName, @Description, @PostedByID); - - INSERT INTO dbo.BreweryPostLocation - (BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates) - VALUES (@NewBrewerLocationID, @NewBreweryID, @CityID, @AddressLine1, @AddressLine2, @PostalCode, @Coordinates); - - COMMIT TRANSACTION; - - SELECT @NewBreweryID AS BreweryPostID, - @NewBrewerLocationID AS BreweryPostLocationID; - -END diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_DeleteBrewery.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_DeleteBrewery.sql deleted file mode 100644 index b4bf3905..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_DeleteBrewery.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_DeleteBrewery @BreweryPostID UNIQUEIDENTIFIER -AS -BEGIN - SET - NOCOUNT ON; - - IF - NOT EXISTS (SELECT 1 - FROM dbo.BreweryPost - WHERE BreweryPostID = @BreweryPostID) - THROW 50404, 'Brewery not found.', 1; - - -- BreweryPostLocation and BreweryPostPhoto cascade-delete with their parent BreweryPost. - DELETE - FROM dbo.BreweryPost - WHERE BreweryPostID = @BreweryPostID; -END diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetAllBreweries.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetAllBreweries.sql deleted file mode 100644 index 208be24f..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetAllBreweries.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_GetAllBreweries( - @Limit INT = NULL, - @Offset INT = NULL -) -AS -BEGIN - SET - NOCOUNT ON; - - SELECT bp.BreweryPostID, - bp.PostedByID, - bp.BreweryName, - bp.Description, - bp.CreatedAt, - bp.UpdatedAt, - bp.Timer, - bpl.BreweryPostLocationID, - bpl.CityID, - bpl.AddressLine1, - bpl.AddressLine2, - bpl.PostalCode, - bpl.Coordinates - FROM dbo.BreweryPost bp - LEFT JOIN dbo.BreweryPostLocation bpl - ON bp.BreweryPostID = bpl.BreweryPostID - ORDER BY bp.CreatedAt DESC - OFFSET ISNULL(@Offset, 0) ROWS FETCH NEXT ISNULL(@Limit, 2147483647) ROWS ONLY; -END diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql deleted file mode 100644 index 25425efc..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_GetBreweryById.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_GetBreweryById @BreweryPostID UNIQUEIDENTIFIER -AS -BEGIN - SELECT * - FROM BreweryPost bp - INNER JOIN BreweryPostLocation bpl - ON bp.BreweryPostID = bpl.BreweryPostID - WHERE bp.BreweryPostID = @BreweryPostID; -END \ No newline at end of file diff --git a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_UpdateBrewery.sql b/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_UpdateBrewery.sql deleted file mode 100644 index 3f3ba76c..00000000 --- a/web/backend/Database/Database.Migrations/scripts/03-crud/05-Breweries/USP_UpdateBrewery.sql +++ /dev/null @@ -1,77 +0,0 @@ -CREATE OR ALTER PROCEDURE dbo.USP_UpdateBrewery( - @BreweryPostID UNIQUEIDENTIFIER, - @BreweryName NVARCHAR(256), - @Description NVARCHAR(512), - @BreweryPostLocationID UNIQUEIDENTIFIER = NULL, - @CityID UNIQUEIDENTIFIER = NULL, - @AddressLine1 NVARCHAR(256) = NULL, - @AddressLine2 NVARCHAR(256) = NULL, - @PostalCode NVARCHAR(20) = NULL, - @Coordinates GEOGRAPHY = NULL -) -AS -BEGIN - SET - NOCOUNT ON; - SET - XACT_ABORT ON; - - IF - @BreweryName IS NULL - THROW 50001, 'Brewery name cannot be null.', 1; - - IF - @Description IS NULL - THROW 50002, 'Brewery description cannot be null.', 1; - - IF - NOT EXISTS (SELECT 1 - FROM dbo.BreweryPost - WHERE BreweryPostID = @BreweryPostID) - THROW 50404, 'Brewery not found.', 1; - - IF - @CityID IS NOT NULL AND NOT EXISTS (SELECT 1 - FROM dbo.City - WHERE CityID = @CityID) - THROW 50404, 'City not found.', 1; - - BEGIN - TRANSACTION; - - UPDATE dbo.BreweryPost - SET BreweryName = @BreweryName, - Description = @Description, - UpdatedAt = GETDATE() - WHERE BreweryPostID = @BreweryPostID; - - IF - @CityID IS NULL - BEGIN - -- No location supplied: clear any existing location for this brewery. - DELETE - FROM dbo.BreweryPostLocation - WHERE BreweryPostID = @BreweryPostID; - END - ELSE - IF EXISTS (SELECT 1 - FROM dbo.BreweryPostLocation - WHERE BreweryPostID = @BreweryPostID) - BEGIN - UPDATE dbo.BreweryPostLocation - SET CityID = @CityID, - AddressLine1 = @AddressLine1, - AddressLine2 = @AddressLine2, - PostalCode = @PostalCode, - Coordinates = @Coordinates - WHERE BreweryPostID = @BreweryPostID; - END - ELSE - BEGIN - INSERT INTO dbo.BreweryPostLocation - (BreweryPostLocationID, BreweryPostID, CityID, AddressLine1, AddressLine2, PostalCode, Coordinates) - VALUES (ISNULL(@BreweryPostLocationID, NEWID()), @BreweryPostID, @CityID, @AddressLine1, @AddressLine2, - @PostalCode, @Coordinates); - END - COMMIT TRANSACTION; -END diff --git a/web/backend/Database/Database.Seed/Database.Seed.csproj b/web/backend/Database/Database.Seed/Database.Seed.csproj index 224faadc..93e8f065 100644 --- a/web/backend/Database/Database.Seed/Database.Seed.csproj +++ b/web/backend/Database/Database.Seed/Database.Seed.csproj @@ -28,6 +28,10 @@ + + + true +