Skip to content

Commit

Permalink
References #48: Implemented new attribute Reference which supports…
Browse files Browse the repository at this point in the history
… nested querying and allows for insert/upsert interaction on root resource.
  • Loading branch information
acupofjose committed Sep 21, 2022
1 parent bc228b5 commit 950e420
Show file tree
Hide file tree
Showing 11 changed files with 425 additions and 23 deletions.
21 changes: 20 additions & 1 deletion Postgrest/Attributes/ColumnAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,32 @@ namespace Postgrest.Attributes
[AttributeUsage(AttributeTargets.Property)]
public class ColumnAttribute : Attribute
{
/// <summary>
/// The name in postgres of this column.
/// </summary>
public string ColumnName { get; set; }

/// <summary>
/// Specifies what should be serialied in the event this column's value is NULL
/// </summary>
public NullValueHandling NullValueHandling { get; set; }

public ColumnAttribute([CallerMemberName] string columnName = null, NullValueHandling nullValueHandling = NullValueHandling.Include)
/// <summary>
/// If the performed query is an Insert or Upsert, should this value be ignored?
/// </summary>
public bool IgnoreOnInsert { get; set; }

/// <summary>
/// If the performed query is an Update, should this value be ignored?
/// </summary>
public bool IgnoreOnUpdate { get; set; }

public ColumnAttribute([CallerMemberName] string columnName = null, NullValueHandling nullValueHandling = NullValueHandling.Include, bool ignoreOnInsert = false, bool ignoreOnUpdate = false)
{
ColumnName = columnName;
NullValueHandling = nullValueHandling;
IgnoreOnInsert = ignoreOnInsert;
IgnoreOnUpdate = ignoreOnUpdate;
}
}
}
97 changes: 97 additions & 0 deletions Postgrest/Attributes/ReferenceAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
using Postgrest.Models;

namespace Postgrest.Attributes
{
/// <summary>
/// Used to specify that a foreign key relationship exists in PostgreSQL
///
/// See: https://postgrest.org/en/stable/api.html#resource-embedding
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ReferenceAttribute : Attribute
{
/// <summary>
/// Type of the model referenced
/// </summary>
public Type Model { get; }

/// <summary>
/// Associated property name
/// </summary>
public string PropertyName { get; private set; }

/// <summary>
/// Table name of model
/// </summary>
public string TableName { get; private set; }

/// <summary>
/// Columns that exist on the model we will select from.
/// </summary>
public List<string> Columns { get; } = new List<string>();

/// <summary>
/// If the performed query is an Insert or Upsert, should this value be ignored? (DEFAULT TRUE)
/// </summary>
public bool IgnoreOnInsert { get; set; }

/// <summary>
/// If the performed query is an Update, should this value be ignored? (DEFAULT TRUE)
/// </summary>
public bool IgnoreOnUpdate { get; set; }

/// <summary>
/// If Reference should automatically be included in queries on this reference. (DEFAULT TRUE)
/// </summary>
public bool IncludeInQuery { get; set; }

/// <param name="model">Model referenced</param>
/// <param name="includeInQuery">Should referenced be included in queries?</param>
/// <param name="ignoreOnInsert">Should reference data be excluded from inserts/upserts?</param>
/// <param name="ignoreOnUpdate">Should reference data be excluded from updates?</param>
/// <param name="propertyName"></param>
/// <exception cref="Exception"></exception>
public ReferenceAttribute(Type model, bool includeInQuery = true, bool ignoreOnInsert = true, bool ignoreOnUpdate = true, [CallerMemberName] string propertyName = "")
{
if (model.BaseType != typeof(BaseModel))
{
throw new Exception("RefernceAttribute must be used with Postgrest BaseModels.");
}

Model = model;
IncludeInQuery = includeInQuery;
IgnoreOnInsert = ignoreOnInsert;
IgnoreOnUpdate = ignoreOnUpdate;
PropertyName = propertyName;

var attr = GetCustomAttribute(model, typeof(TableAttribute));
if (attr is TableAttribute tableAttr)
{
TableName = tableAttr.Name;
}
else
{
TableName = model.Name;
}

foreach (var property in model.GetProperties())
{
var attrs = property.GetCustomAttributes(true);

foreach (var item in attrs)
{
if (item is ColumnAttribute col)
Columns.Add(col.ColumnName);
else if (item is PrimaryKeyAttribute pk)
Columns.Add(pk.ColumnName);
else if (item is ReferenceAttribute ra)
Columns.Add($"{ra.TableName}!inner({string.Join(",", ra.Columns.ToArray())})");
}
}
}
}
}
6 changes: 6 additions & 0 deletions Postgrest/Attributes/callerName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Postgrest.Attributes
{
public class callerName
{
}
}
1 change: 0 additions & 1 deletion Postgrest/Postgrest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ The bulk of this library is a translation and c-sharp-ification of the supabase/
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Attributes\" />
<Folder Include="Converters\" />
</ItemGroup>
</Project>
33 changes: 32 additions & 1 deletion Postgrest/PostgrestContractResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ namespace Postgrest.Attributes
/// </summary>
public class PostgrestContractResolver : DefaultContractResolver
{
public bool IsUpdate { get; private set; } = false;
public bool IsInsert { get; private set; } = false;

public void SetState(bool isInsert = false, bool isUpdate = false)
{
IsUpdate = isUpdate;
IsInsert = isInsert;
}

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty prop = base.CreateProperty(member, memberSerialization);
Expand Down Expand Up @@ -51,6 +60,28 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ
{
prop.PropertyName = columnAttribute.ColumnName;
prop.NullValueHandling = columnAttribute.NullValueHandling;

if (IsInsert && columnAttribute.IgnoreOnInsert)
prop.Ignored = true;

if (IsUpdate && columnAttribute.IgnoreOnUpdate)
prop.Ignored = true;

return prop;
}

var referenceAttr = member.GetCustomAttribute<ReferenceAttribute>();

if (referenceAttr != null)
{
prop.PropertyName = referenceAttr.TableName;

if (IsInsert && referenceAttr.IgnoreOnInsert)
prop.Ignored = true;

if (IsUpdate && referenceAttr.IgnoreOnUpdate)
prop.Ignored = true;

return prop;
}

Expand All @@ -60,7 +91,7 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ
{
return prop;
}

prop.PropertyName = primaryKeyAttribute.ColumnName;
prop.ShouldSerialize = instance => primaryKeyAttribute.ShouldInsert;
return prop;
Expand Down
93 changes: 78 additions & 15 deletions Postgrest/Table.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
Expand Down Expand Up @@ -40,6 +41,9 @@ namespace Postgrest

private List<QueryFilter> filters = new List<QueryFilter>();
private List<QueryOrderer> orderers = new List<QueryOrderer>();
private List<string> columns = new List<string>();

private List<ReferenceAttribute> references = new List<ReferenceAttribute>();

private int rangeFrom = int.MinValue;
private int rangeTo = int.MinValue;
Expand Down Expand Up @@ -67,15 +71,15 @@ public Table(string baseUrl, ClientOptions options = null)

serializerSettings = StatelessClient.SerializerSettings(options);

var attr = Attribute.GetCustomAttribute(typeof(T), typeof(TableAttribute));

if (attr is TableAttribute tableAttr)
foreach (var property in typeof(T).GetProperties())
{
TableName = tableAttr.Name;
return;
var attrs = property.GetCustomAttributes(typeof(ReferenceAttribute), true);

if (attrs.Length > 0)
references.Add((ReferenceAttribute)attrs.First());
}

TableName = typeof(T).Name;
TableName = FindTableName();
}

/// <summary>
Expand Down Expand Up @@ -326,6 +330,23 @@ public Table<T> OnConflict(string columnName)
return this;
}

/// <summary>
/// By using the columns query parameter it’s possible to specify the payload keys that will be inserted and ignore the rest of the payload.
///
/// The rest of the JSON keys will be ignored.
/// Using this also has the side-effect of being more efficient for Bulk Insert since PostgREST will not process the JSON and it’ll send it directly to PostgreSQL.
///
/// See: https://postgrest.org/en/stable/api.html#specifying-columns
/// </summary>
/// <param name="columns"></param>
/// <returns></returns>
public Table<T> Columns(string[] columns)
{
foreach (var column in columns)
this.columns.Add(column);

return this;
}

/// <summary>
/// Sets an offset with an optional foreign table reference.
Expand Down Expand Up @@ -423,7 +444,7 @@ public Task<ModeledResponse<T>> Update(T model, QueryOptions options = null, Can

filters.Add(new QueryFilter(model.PrimaryKeyColumn, Operator.Equals, model.PrimaryKeyValue.ToString()));

var request = Send<T>(method, model, options.ToHeaders(), cancellationToken);
var request = Send<T>(method, model, options.ToHeaders(), cancellationToken, isUpdate: true);

Clear();

Expand Down Expand Up @@ -591,6 +612,11 @@ internal string GenerateUrl()
query.Add("apikey", options.Headers["apikey"]);
}

if (columns.Count > 0)
{
query["columns"] = string.Join(",", columns);
}

foreach (var filter in filters)
{
var parsedFilter = PrepareFilter(filter);
Expand All @@ -613,6 +639,21 @@ internal string GenerateUrl()
query["select"] = Regex.Replace(columnQuery, @"\s", "");
}

if (references.Count > 0)
{
if (query["select"] == null)
query["select"] = "*";

foreach (var reference in references)
{
if (reference.IncludeInQuery)
{
var columns = string.Join(",", reference.Columns.ToArray());
query["select"] = query["select"] + $",{reference.TableName}!inner({columns})";
}
}
}

if (!string.IsNullOrEmpty(onConflict))
{
query["on_conflict"] = onConflict;
Expand All @@ -639,19 +680,25 @@ internal string GenerateUrl()
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
internal object PrepareRequestData(object data)
internal object PrepareRequestData(object data, bool isInsert = false, bool isUpdate = false)
{
if (data == null) return new Dictionary<string, string>();

var resolver = (PostgrestContractResolver)serializerSettings.ContractResolver;

resolver.SetState(isInsert, isUpdate);

var serialized = JsonConvert.SerializeObject(data, serializerSettings);

resolver.SetState();

// Check if data is a Collection for the Insert Bulk case
if (data is ICollection<T>)
{
var serialized = JsonConvert.SerializeObject(data, serializerSettings);
return JsonConvert.DeserializeObject<List<object>>(serialized);
}
else
{
var serialized = JsonConvert.SerializeObject(data, serializerSettings);
return JsonConvert.DeserializeObject<Dictionary<string, object>>(serialized, serializerSettings);
}
}
Expand Down Expand Up @@ -766,6 +813,7 @@ public void Clear()

filters.Clear();
orderers.Clear();
columns.Clear();

rangeFrom = int.MinValue;
rangeTo = int.MinValue;
Expand Down Expand Up @@ -798,23 +846,38 @@ private Task<ModeledResponse<T>> PerformInsert(object data, QueryOptions options
OnConflict(options.OnConflict);
}

var request = Send<T>(method, data, options.ToHeaders(), cancellationToken);
var request = Send<T>(method, data, options.ToHeaders(), cancellationToken, isInsert: true);

Clear();

return request;
}

private Task<BaseResponse> Send(HttpMethod method, object data, Dictionary<string, string> headers = null, CancellationToken cancellationToken = default)
private Task<BaseResponse> Send(HttpMethod method, object data, Dictionary<string, string> headers = null, CancellationToken cancellationToken = default, bool isInsert = false, bool isUpdate = false)
{
var requestHeaders = Helpers.PrepareRequestHeaders(method, headers, options, rangeFrom, rangeTo);
return Helpers.MakeRequest(method, GenerateUrl(), serializerSettings, PrepareRequestData(data), requestHeaders, cancellationToken);
var preparedData = PrepareRequestData(data, isInsert, isUpdate);
return Helpers.MakeRequest(method, GenerateUrl(), serializerSettings, preparedData, requestHeaders, cancellationToken);
}

private Task<ModeledResponse<U>> Send<U>(HttpMethod method, object data, Dictionary<string, string> headers = null, CancellationToken cancellationToken = default) where U : BaseModel, new()
private Task<ModeledResponse<U>> Send<U>(HttpMethod method, object data, Dictionary<string, string> headers = null, CancellationToken cancellationToken = default, bool isInsert = false, bool isUpdate = false) where U : BaseModel, new()
{
var requestHeaders = Helpers.PrepareRequestHeaders(method, headers, options, rangeFrom, rangeTo);
return Helpers.MakeRequest<U>(method, GenerateUrl(), serializerSettings, PrepareRequestData(data), requestHeaders, cancellationToken);
var preparedData = PrepareRequestData(data, isInsert, isUpdate);
return Helpers.MakeRequest<U>(method, GenerateUrl(), serializerSettings, preparedData, requestHeaders, cancellationToken);
}

internal static string FindTableName(object obj = null)
{
var type = obj == null ? typeof(T) : obj is Type t ? t : obj.GetType();
var attr = Attribute.GetCustomAttribute(type, typeof(TableAttribute));

if (attr is TableAttribute tableAttr)
{
return tableAttr.Name;
}

return type.Name;
}
}
}
Loading

0 comments on commit 950e420

Please sign in to comment.