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 fbda062 commit c8a2077
Show file tree
Hide file tree
Showing 16 changed files with 238 additions and 11 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;
}
}
39 changes: 37 additions & 2 deletions Meadowlark.net/Meadowlark.Net.Core/Handler/FrontendFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
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 @@ -21,13 +25,44 @@ public static async Task<FrontendResponse> UpsertCore(FrontendRequest frontendRe
ApiSchema: No.ApiSchema, ResourceSchema: No.ResourceSchema);
MiddlewareModel middlewareModel = new(RequestModel: requestModel, FrontendResponse: null);

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

return frontendResponse ?? new(StatusCode: 200, Body: $"Success: {finalRequestModel.ResourceInfo.ToString()}");
// 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
{
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);

// 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 @@ -9,7 +9,8 @@
<ItemGroup>
<PackageReference Include="JsonSchema.Net" Version="5.5.0" />
<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 @@ -18,4 +19,10 @@
</None>
</ItemGroup>

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

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static class ValidateDocumentMiddleware
/**
* Validates JSON document shape
*/
public static async Task<MiddlewareModel> DocumentValidation(MiddlewareModel middlewareModel)
public static MiddlewareModel DocumentValidation(MiddlewareModel middlewareModel)
{
var (requestModel, frontendResponse) = middlewareModel;

Expand Down
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 = await 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
11 changes: 11 additions & 0 deletions Meadowlark.net/RestClient/test.http
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ POST http://localhost:3000/ed-fi/contentClassDescriptors/
"namespace": "uri://ed-fi.org/ContentClassDescriptor"
}

### Test GET of descriptor

GET http://localhost:3000/ed-fi/contentClassDescriptors/805e77ca-d917-4706-a0d4-8c36f2281d8b



### Test POST of an EducationContent

POST http://localhost:3000/ed-fi/educationContents
Expand All @@ -21,6 +27,11 @@ POST http://localhost:3000/ed-fi/educationContents
"learningResourceMetadataURI": "21430"
}

### Test GET of an EducationContent

GET http://localhost:3000/ed-fi/educationContents/2ddb19fb-9563-44d6-bced-b8218665258b


### Test POST of an EducationContent with two overposted fields
### Result is NoAdditionalPropertiesAllowed
POST http://localhost:3000/ed-fi/educationContents
Expand Down

0 comments on commit c8a2077

Please sign in to comment.