diff --git a/Src/Notion.Client/Api/ApiEndpoints.cs b/Src/Notion.Client/Api/ApiEndpoints.cs new file mode 100644 index 00000000..de8c9cdb --- /dev/null +++ b/Src/Notion.Client/Api/ApiEndpoints.cs @@ -0,0 +1,18 @@ +namespace Notion.Client +{ + public static class ApiEndpoints + { + public static class DatabasesApiUrls + { + public static string Retrieve(string databaseId) => $"/v1/databases/{databaseId}"; + public static string List() => "/v1/databases"; + public static string Query(string databaseId) => $"/v1/databases/{databaseId}/query"; + } + + public static class UsersApiUrls + { + public static string Retrieve(string userId) => $"/v1/users/{userId}"; + public static string List() => "/v1/users"; + } + } +} \ No newline at end of file diff --git a/Src/Notion.Client/Api/Databases/DatabasesClient.cs b/Src/Notion.Client/Api/Databases/DatabasesClient.cs new file mode 100644 index 00000000..deb4798e --- /dev/null +++ b/Src/Notion.Client/Api/Databases/DatabasesClient.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using static Notion.Client.ApiEndpoints; + +namespace Notion.Client +{ + public interface IDatabasesClient + { + Task RetrieveAsync(string databaseId); + Task> QueryAsync(string databaseId, DatabasesQueryParameters databasesQueryParameters); + Task> ListAsync(DatabasesListParameters databasesListParameters = null); + } + + public class DatabasesClient : IDatabasesClient + { + private readonly IRestClient _client; + + public DatabasesClient(IRestClient client) + { + _client = client; + } + + public async Task RetrieveAsync(string databaseId) + { + return await _client.GetAsync(DatabasesApiUrls.Retrieve(databaseId)); + } + + public async Task> ListAsync(DatabasesListParameters databasesListParameters = null) + { + var databasesListQueryParmaters = (IDatabasesListQueryParmaters)databasesListParameters; + + var queryParams = new Dictionary() + { + { "start_cursor", databasesListQueryParmaters?.StartCursor }, + { "page_size", databasesListQueryParmaters?.PageSize } + }; + + return await _client.GetAsync>(DatabasesApiUrls.List(), queryParams); + } + + public async Task> QueryAsync(string databaseId, DatabasesQueryParameters databasesQueryParameters) + { + var body = (IDatabaseQueryBodyParameters)databasesQueryParameters; + + return await _client.PostAsync>(DatabasesApiUrls.Query(databaseId), body); + } + } +} \ No newline at end of file diff --git a/Src/Notion.Client/UsersClient.cs b/Src/Notion.Client/Api/Users/UsersClient.cs similarity index 58% rename from Src/Notion.Client/UsersClient.cs rename to Src/Notion.Client/Api/Users/UsersClient.cs index 73bed377..ba30301c 100644 --- a/Src/Notion.Client/UsersClient.cs +++ b/Src/Notion.Client/Api/Users/UsersClient.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using static Notion.Client.ApiEndpoints; namespace Notion.Client { @@ -20,26 +21,12 @@ public UsersClient(IRestClient client) public async Task RetrieveAsync(string userId) { - try - { - return await _client.GetAsync($"users/{userId}"); - } - catch (Exception e) - { - return null; - } + return await _client.GetAsync(UsersApiUrls.Retrieve(userId)); } public async Task> ListAsync() { - try - { - return await _client.GetAsync>("users"); - } - catch (Exception e) - { - return null; - } + return await _client.GetAsync>(UsersApiUrls.List()); } } } diff --git a/Src/Notion.Client/Constants.cs b/Src/Notion.Client/Constants.cs index 3078de60..209e3b91 100644 --- a/Src/Notion.Client/Constants.cs +++ b/Src/Notion.Client/Constants.cs @@ -2,7 +2,7 @@ { internal class Constants { - internal static string BASE_URL = "https://api.notion.com/v1/"; + internal static string BASE_URL = "https://api.notion.com/"; internal static string DEFAULT_NOTION_VERSION = "2021-05-13"; } } diff --git a/Src/Notion.Client/DatabasesClient.cs b/Src/Notion.Client/DatabasesClient.cs deleted file mode 100644 index d3bc6f00..00000000 --- a/Src/Notion.Client/DatabasesClient.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Notion.Client -{ - public interface IDatabasesClient - { - Task RetrieveAsync(string databaseId); - Task> QueryAsync(string databaseId, DatabasesQueryParameters databasesQueryParameters); - Task> ListAsync(DatabasesListParameters databasesListParameters = null); - } - - public class DatabasesClient : IDatabasesClient - { - private readonly IRestClient _client; - - public DatabasesClient(IRestClient client) - { - _client = client; - } - - public async Task RetrieveAsync(string databaseId) - { - try - { - return await _client.GetAsync($"databases/{databaseId}"); - } - catch (Exception e) - { - // Todo: Throw Custom Exception - return null; - } - } - - public async Task> ListAsync(DatabasesListParameters databasesListParameters = null) - { - try - { - var databasesListQueryParmaters = (IDatabasesListQueryParmaters)databasesListParameters; - var queryParams = new Dictionary() - { - { "start_cursor", databasesListQueryParmaters?.StartCursor }, - { "page_size", databasesListQueryParmaters?.PageSize } - }; - - return await _client.GetAsync>("databases", queryParams); - } - catch (Exception e) - { - // Todo: Throw Custom Exception - return null; - } - } - - public async Task> QueryAsync(string databaseId, DatabasesQueryParameters databasesQueryParameters) - { - try - { - var body = (IDatabaseQueryBodyParameters)databasesQueryParameters; - return await _client.PostAsync>($"databases/{databaseId}/query", body); - } - catch (Exception e) - { - return null; - } - } - } -} \ No newline at end of file diff --git a/Src/Notion.Client/Extensions/HttpResponseMessageExtensions.cs b/Src/Notion.Client/Extensions/HttpResponseMessageExtensions.cs new file mode 100644 index 00000000..d7f2b894 --- /dev/null +++ b/Src/Notion.Client/Extensions/HttpResponseMessageExtensions.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Notion.Client.Extensions +{ + internal static class HttpResponseMessageExtensions + { + internal static async Task ParseStreamAsync(this HttpResponseMessage response, JsonSerializerSettings serializerSettings = null) + { + using (Stream stream = await response.Content.ReadAsStreamAsync()) + { + using (StreamReader streamReader = new StreamReader(stream)) + { + using (JsonReader jsonReader = new JsonTextReader(streamReader)) + { + JsonSerializer serializer = null; + + if (serializerSettings == null) + { + serializer = JsonSerializer.CreateDefault(); + } + else + { + serializer = JsonSerializer.Create(serializerSettings); + } + + return serializer.Deserialize(jsonReader); + } + } + } + } + } +} \ No newline at end of file diff --git a/Src/Notion.Client/NotionApiException.cs b/Src/Notion.Client/NotionApiException.cs new file mode 100644 index 00000000..aa8ffa4a --- /dev/null +++ b/Src/Notion.Client/NotionApiException.cs @@ -0,0 +1,17 @@ +using System; +using System.Net; + +namespace Notion.Client +{ + class NotionApiException : Exception + { + public NotionApiException(HttpStatusCode statusCode, string message) : this(statusCode, message, null) + { + } + + public NotionApiException(HttpStatusCode statusCode, string message, Exception innerException) : base(message, innerException) + { + Data.Add("StatusCode", statusCode); + } + } +} \ No newline at end of file diff --git a/Src/Notion.Client/RestClient.cs b/Src/Notion.Client/RestClient.cs index ab536b03..93d761a0 100644 --- a/Src/Notion.Client/RestClient.cs +++ b/Src/Notion.Client/RestClient.cs @@ -3,25 +3,38 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; +using Notion.Client.Extensions; using Notion.Client.http; namespace Notion.Client { public interface IRestClient { - Task GetAsync(string uri, Dictionary queryParams = null); - Task PostAsync(string uri, object body); + Task GetAsync( + string uri, + IDictionary queryParams = null, + IDictionary headers = null, + JsonSerializerSettings serializerSettings = null, + CancellationToken cancellationToken = default); + + Task PostAsync( + string uri, + object body, + IDictionary queryParams = null, + IDictionary headers = null, + JsonSerializerSettings serializerSettings = null, + CancellationToken cancellationToken = default); } public class RestClient : IRestClient { private HttpClient _httpClient; private readonly ClientOptions _options; - private readonly List jsonConverters = new List(); - private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings + private readonly JsonSerializerSettings defaultSerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; @@ -41,32 +54,89 @@ private static ClientOptions MergeOptions(ClientOptions options) }; } - public async Task GetAsync(string uri, Dictionary queryParams = null) + public async Task GetAsync( + string uri, + IDictionary queryParams = null, + IDictionary headers = null, + JsonSerializerSettings serializerSettings = null, + CancellationToken cancellationToken = default) { EnsureHttpClient(); - uri = queryParams == null ? uri : QueryHelpers.AddQueryString(uri, queryParams); + string requestUri = queryParams == null ? uri : QueryHelpers.AddQueryString(uri, queryParams); - using (var stream = await _httpClient.GetStreamAsync(uri)) + var response = await SendAsync(requestUri, HttpMethod.Get, headers, cancellationToken: cancellationToken); + + if (response.IsSuccessStatusCode) { - return SerializerHelper.Deserialize(stream, jsonConverters); + return await response.ParseStreamAsync(serializerSettings); } + + var message = !string.IsNullOrWhiteSpace(response.ReasonPhrase) + ? response.ReasonPhrase + : await response.Content.ReadAsStringAsync(); + + throw new NotionApiException(response.StatusCode, message); } - public async Task PostAsync(string uri, object body) + private async Task SendAsync( + string requestUri, + HttpMethod httpMethod, + IDictionary headers = null, + Action attachContent = null, + CancellationToken cancellationToken = default) { - EnsureHttpClient(); + HttpRequestMessage httpRequest = new HttpRequestMessage(httpMethod, requestUri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.AuthToken); + httpRequest.Headers.Add("Notion-Version", _options.NotionVersion); + + if (headers != null) + { + AddHeaders(httpRequest, headers); + } + + attachContent?.Invoke(httpRequest); + + return await _httpClient.SendAsync(httpRequest, cancellationToken); + } + + private static void AddHeaders(HttpRequestMessage request, IDictionary headers) + { + foreach (var header in headers) + { + request.Headers.Add(header.Key, header.Value); + } + } - var content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, "application/json"); + public async Task PostAsync( + string uri, + object body, + IDictionary queryParams = null, + IDictionary headers = null, + JsonSerializerSettings serializerSettings = null, + CancellationToken cancellationToken = default) + { + EnsureHttpClient(); - using (var response = await _httpClient.PostAsync(uri, content)) + void AttachContent(HttpRequestMessage httpRequest) { - response.EnsureSuccessStatusCode(); + httpRequest.Content = new StringContent(JsonConvert.SerializeObject(body, defaultSerializerSettings), Encoding.UTF8, "application/json"); + } + + string requestUri = queryParams == null ? uri : QueryHelpers.AddQueryString(uri, queryParams); - var stream = await response.Content.ReadAsStreamAsync(); + var response = await SendAsync(requestUri, HttpMethod.Post, headers, AttachContent, cancellationToken: cancellationToken); - return SerializerHelper.Deserialize(stream, jsonConverters); + if (response.IsSuccessStatusCode) + { + return await response.ParseStreamAsync(serializerSettings); } + + var message = !string.IsNullOrWhiteSpace(response.ReasonPhrase) + ? response.ReasonPhrase + : await response.Content.ReadAsStringAsync(); + + throw new NotionApiException(response.StatusCode, message); } private HttpClient EnsureHttpClient() @@ -75,8 +145,6 @@ private HttpClient EnsureHttpClient() { _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri(_options.BaseUrl); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _options.AuthToken); - _httpClient.DefaultRequestHeaders.Add("Notion-Version", _options.NotionVersion); } return _httpClient; diff --git a/Src/Notion.Client/SerializerHelper.cs b/Src/Notion.Client/SerializerHelper.cs deleted file mode 100644 index 4134a358..00000000 --- a/Src/Notion.Client/SerializerHelper.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Notion.Client -{ - internal class SerializerHelper - { - internal static T Deserialize(Stream stream, List converters) - { - using (StreamReader sr = new StreamReader(stream)) - { - using (JsonReader reader = new JsonTextReader(sr)) - { - JsonSerializer serializer = JsonSerializer.CreateDefault(); - - if (converters.Any()) - { - foreach (var converter in converters) - { - serializer.Converters.Add(converter); - } - } - - return serializer.Deserialize(reader); - } - } - } - } -}