Skip to content

Commit

Permalink
[RND-698] add postgresql insert and get support
Browse files Browse the repository at this point in the history
  • Loading branch information
bradbanister committed Jan 16, 2024
1 parent a2a1df2 commit fc074bf
Show file tree
Hide file tree
Showing 16 changed files with 389 additions and 27 deletions.
3 changes: 3 additions & 0 deletions Meadowlark.net/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,6 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml

# DotNetEnv
.env
4 changes: 4 additions & 0 deletions Meadowlark.net/Meadowlark.Net.Core/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#### PostgreSQL backend options

POSTGRES_USER=
POSTGRES_PASSWORD=
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using Meadowlark.Net.Core.Model;
namespace Meadowlark.Net.Core.Backend.Model;

public record GetRequest(DocumentUuid DocumentUuid, ResourceInfo ResourceInfo);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using Meadowlark.Net.Core.Model;
namespace Meadowlark.Net.Core.Backend.Model;

public record GetResponse(string EdfiDoc);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Meadowlark.Net.Core.Model;
using Newtonsoft.Json.Linq;
namespace Meadowlark.Net.Core.Backend.Model;

public record InsertRequest(DocumentUuid DocumentUuid, ResourceInfo ResourceInfo, JObject EdfiDoc);
101 changes: 101 additions & 0 deletions Meadowlark.net/Meadowlark.Net.Core/Backend/Postgresql/Db.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Meadowlark.Net.Core.Backend.Model;
using Meadowlark.Net.Core.Model;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Npgsql;
using NpgsqlTypes;

namespace Meadowlark.Net.Core.Backend.Postgresql;

public static class Db
{
// TODO: Get .env loading working
private static readonly string connectionString = "Server=localhost;Port=5432;User Id=postgres;Password=abcdefgh1!;Database=MeadowlarkNet;";

// static Db()
// {
// Load .env file with Postgresql username/password
// DotNetEnv.Env.TraversePath().Load();
// connectionString: $"Server=localhost;Port=5432;User Id={Environment.GetEnvironmentVariable("POSTGRES_USER")};Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")};Database=MeadowlarkNet;");
// }

private static async Task TryCreateTable(ResourceInfo resourceInfo)
{
// Note: ProjectName and ResourceName have been validated against the ApiSchema, so SQL injection is not a concern here
string schemaName = resourceInfo.ProjectName.Value.Replace("-", "");
string tableName = resourceInfo.ResourceName.Value.Replace("-", "");

using var con = new NpgsqlConnection(connectionString);
con.Open();
using var cmd = new NpgsqlCommand();
cmd.Connection = con;

cmd.CommandText = $"CREATE SCHEMA IF NOT EXISTS {schemaName}";
await cmd.ExecuteNonQueryAsync();
cmd.CommandText = $@"CREATE TABLE IF NOT EXISTS {schemaName}.{tableName}(
id bigserial PRIMARY KEY,
document_uuid UUID NOT NULL,
project_name VARCHAR NOT NULL,
resource_name VARCHAR NOT NULL,
resource_version VARCHAR NOT NULL,
is_descriptor BOOLEAN NOT NULL,
edfi_doc JSONB NOT NULL);";
await cmd.ExecuteNonQueryAsync();
}

public static async Task InsertDocument(InsertRequest insertRequest)
{
await TryCreateTable(insertRequest.ResourceInfo);

// Note: ProjectName and ResourceName have been validated against the ApiSchema, so SQL injection is not a concern here
string schemaName = insertRequest.ResourceInfo.ProjectName.Value.Replace("-", "");
string tableName = insertRequest.ResourceInfo.ResourceName.Value.Replace("-", "");
var (documentUuid, resourceInfo, edfiDoc) = insertRequest;

// Add the new documentUuid to the document
edfiDoc.Add(new JProperty("id", documentUuid.Value));

using var con = new NpgsqlConnection(connectionString);
con.Open();

var commandText = $@" INSERT INTO {schemaName}.{tableName}
(document_uuid, project_name, resource_name, resource_version, is_descriptor, edfi_doc)
VALUES ($1, $2, $3, $4, $5, $6)";
await using var cmd = new NpgsqlCommand(commandText, con)
{
Parameters = {
new() {Value = new Guid(documentUuid.Value), NpgsqlDbType = NpgsqlDbType.Uuid },
new() {Value = resourceInfo.ProjectName.Value },
new() {Value = resourceInfo.ResourceName.Value },
new() {Value = resourceInfo.ResourceVersion.Value },
new() {Value = resourceInfo.IsDescriptor },
new() {Value = JsonConvert.SerializeObject(edfiDoc), NpgsqlDbType = NpgsqlDbType.Jsonb }
}
};
await cmd.ExecuteNonQueryAsync();
}

public static async Task<GetResponse> FindDocumentByDocumentUuid(GetRequest getRequest)
{
await TryCreateTable(getRequest.ResourceInfo);

// Note: ProjectName and ResourceName have been validated against the ApiSchema, so SQL injection is not a concern here
string schemaName = getRequest.ResourceInfo.ProjectName.Value.Replace("-", "");
string tableName = getRequest.ResourceInfo.ResourceName.Value.Replace("-", "");

using var con = new NpgsqlConnection(connectionString);
con.Open();

var commandText = $@"SELECT edfi_doc FROM {schemaName}.{tableName} WHERE document_uuid = $1;";

await using var cmd = new NpgsqlCommand(commandText, con)
{
Parameters = {
new() { Value = new Guid(getRequest.DocumentUuid.Value), NpgsqlDbType = NpgsqlDbType.Uuid }
}
};

var result = await cmd.ExecuteScalarAsync();
return new(result == null ? "" : result.ToString() ?? "");
}
}
13 changes: 13 additions & 0 deletions Meadowlark.net/Meadowlark.Net.Core/Backend/Postgresql/GetById.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Meadowlark.Net.Core.Backend.Model;
using static Meadowlark.Net.Core.Backend.Postgresql.Db;

namespace Meadowlark.Net.Core.Backend.Postgresql;

public static class GetById
{
public static async Task<FrontendResponse> GetByIdDb(GetRequest getRequest)
{
GetResponse result = await FindDocumentByDocumentUuid(getRequest);
return new(StatusCode: 200, Body: $"Success: {result.EdfiDoc}");
}
}
13 changes: 13 additions & 0 deletions Meadowlark.net/Meadowlark.Net.Core/Backend/Postgresql/Upsert.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Meadowlark.Net.Core.Backend.Model;
using static Meadowlark.Net.Core.Backend.Postgresql.Db;

namespace Meadowlark.Net.Core.Backend.Postgresql;

public static class Upsert
{
public static async Task<FrontendResponse> UpsertDb(InsertRequest insertRequest)
{
await InsertDocument(insertRequest);
return new(StatusCode: 200, Body: $"Success: {insertRequest.ToString()}");
}
}
23 changes: 23 additions & 0 deletions Meadowlark.net/Meadowlark.Net.Core/Backend/Utility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Meadowlark.Net.Backend.Postgresql;

public static class Utility
{

/**
* Simple only-once guard for a function with no parameters
*/
public static Action Once(Action fn){
var called = false;

void guardedFunction()
{
if (!called)
{
fn();
called = true;
}
}

return guardedFunction;
}
}
42 changes: 39 additions & 3 deletions Meadowlark.net/Meadowlark.Net.Core/Handler/FrontendFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
using static Meadowlark.Net.Core.Middleware.ApiSchemaLoadingMiddleware;
using static Meadowlark.Net.Core.Middleware.ParsePathMiddleware;
using static Meadowlark.Net.Core.Middleware.ValidateEndpointMiddleware;
using static Meadowlark.Net.Core.Middleware.ValidateDocumentMiddleware;
using static Meadowlark.Net.Core.Backend.Postgresql.Upsert;
using static Meadowlark.Net.Core.Backend.Postgresql.GetById;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Meadowlark.Net.Core;

Expand All @@ -12,7 +17,36 @@ public static class FrontendFacade
/**
* Entry point for API upsert actions
*/
public static FrontendResponse UpsertCore(FrontendRequest frontendRequest)
public static async Task<FrontendResponse> UpsertCore(FrontendRequest frontendRequest)
{
try
{
RequestModel requestModel = new(FrontendRequest: frontendRequest, PathComponents: No.PathComponents, ResourceInfo: No.ResourceInfo,
ApiSchema: No.ApiSchema, ResourceSchema: No.ResourceSchema);
MiddlewareModel middlewareModel = new(RequestModel: requestModel, FrontendResponse: null);

var (finalRequestModel, frontendResponse) = middlewareModel
.SendTo(LoadApiSchema)
.AndThen(ParsePath)
.AndThen(EndpointValidation)
.AndThen(DocumentValidation);

// if there is a response posted by the stack, we are done
if (frontendResponse != null) return frontendResponse;

DocumentUuid documentUuid = new(Guid.NewGuid().ToString());
return await UpsertDb(new(documentUuid, finalRequestModel.ResourceInfo, frontendRequest.Body ?? new JObject()));
}
catch (Exception)
{
return new(StatusCode: 500, Body: "Fail");
}
}

/**
* Entry point for API get by id actions
*/
public static async Task<FrontendResponse> GetByIdCore(FrontendRequest frontendRequest)
{
try
{
Expand All @@ -25,8 +59,10 @@ public static FrontendResponse UpsertCore(FrontendRequest frontendRequest)
.AndThen(ParsePath)
.AndThen(EndpointValidation);

return frontendResponse ?? new(StatusCode: 200, Body: $"Success: {finalRequestModel.ResourceInfo.ToString()}");
// return frontendResponse ?? new(StatusCode: 500, Body: "FrontendResponse was null");
// if there is a response posted by the stack, we are done
if (frontendResponse != null) return frontendResponse;

return await GetByIdDb(new(finalRequestModel.PathComponents.DocumentUuid ?? new(""), finalRequestModel.ResourceInfo));
}
catch (Exception)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="DotNetEnv" Version="3.0.0" />
<PackageReference Include="Npgsql" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
Expand All @@ -17,4 +18,10 @@
</None>
</ItemGroup>

<ItemGroup>
<None Include=".env">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Meadowlark.Net.Core.Model;
using static Meadowlark.Net.Core.Validation.DocumentValidator;

namespace Meadowlark.Net.Core.Middleware;

public static class ValidateDocumentMiddleware
{
/**
* Validates JSON document shape
*/
public static MiddlewareModel DocumentValidation(MiddlewareModel middlewareModel)
{
var (requestModel, frontendResponse) = middlewareModel;

// if there is a response already posted, we are done
if (frontendResponse != null) return middlewareModel;

// if the body is null, this middleware shouldn't be involved
if (requestModel.FrontendRequest.Body == null) throw new Exception("requestModel.FrontendRequest.Body is null");

string[] errors = ValidateDocument(requestModel.ResourceSchema, requestModel.FrontendRequest.Body);

if (errors.Length > 0)
{
return middlewareModel with { FrontendResponse = new(StatusCode: 400, Body: String.Join(", ", errors)) };
}

return middlewareModel;
}
}
12 changes: 6 additions & 6 deletions Meadowlark.net/Meadowlark.Net.Frontend.MinimalAPI/CrudHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@ public static class CrudHandler
*/
public static async Task<IResult> Upsert(HttpRequest request)
{
// respondWith(await meadowlarkUpsert(fromRequest(request)), reply);

JObject? body = await ExtractJsonBodyFrom(request);

FrontendRequest frontendRequest = new(Action: "POST", Body: body, Path: request.Path, TraceId: System.Guid.NewGuid().ToString());
FrontendRequest frontendRequest = new(Action: "POST", Body: body, Path: request.Path, TraceId: Guid.NewGuid().ToString());

var frontendResponse = UpsertCore(frontendRequest);

Expand All @@ -43,11 +41,13 @@ public static async Task<IResult> Upsert(HttpRequest request)
/**
* Entry point for all API GET requests
*/
public static IResult Get(HttpRequest request)
public static async Task<IResult> GetById(HttpRequest request)
{
// respondWith(await meadowlarkGet(fromRequest(request)), reply);
FrontendRequest frontendRequest = new(Action: "GET", Body: null, Path: request.Path, TraceId: Guid.NewGuid().ToString());

var frontendResponse = await GetByIdCore(frontendRequest);

return Results.Ok(new { Message = "Get" });
return Results.Content(statusCode: frontendResponse.StatusCode, content: frontendResponse.Body);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
var app = builder.Build();

app.MapPost("/{**catchAll}", Upsert);
app.MapGet("/", Get);
app.MapGet("/{**catchAll}", GetById);
app.MapPut("/", Update);
app.MapDelete("/", DeleteIt);

Expand Down
4 changes: 4 additions & 0 deletions Meadowlark.net/Meadowlark.Net.sln
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ Global
{94432F90-DE9C-4E69-A4DD-D4EEA31DE5C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94432F90-DE9C-4E69-A4DD-D4EEA31DE5C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94432F90-DE9C-4E69-A4DD-D4EEA31DE5C3}.Release|Any CPU.Build.0 = Release|Any CPU
{F7B239AD-5A50-49FB-A6C7-36B40484CB6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7B239AD-5A50-49FB-A6C7-36B40484CB6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7B239AD-5A50-49FB-A6C7-36B40484CB6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7B239AD-5A50-49FB-A6C7-36B40484CB6E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Loading

0 comments on commit fc074bf

Please sign in to comment.