diff --git a/.gitignore b/.gitignore index 01c7134..fea3cac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,16 @@ -[Ll]ibrary/ -[Tt]emp/ -[Oo]bj/ -[Bb]uild/ +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/Assets/AssetStoreTools* -# Autogenerated VS/MD solution and project files +# Visual Studio 2015 cache directory +/.vs/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ *.csproj *.unityproj *.sln @@ -13,6 +20,8 @@ *.userprefs *.pidb *.booproj +*.svd +*.pdb # Unity3D generated meta files *.pidb.meta @@ -20,3 +29,12 @@ # Unity3D Generated File On Crash Reports sysinfo.txt + +# Builds +*.apk +*.unitypackage + +# IDEs +/.vscode +omnisharp.json +.editorconfig diff --git a/AppServiceClient.cs b/AppServiceClient.cs new file mode 100644 index 0000000..3d706a5 --- /dev/null +++ b/AppServiceClient.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; +using UnityEngine; + +namespace Azure.AppServices { + public sealed class AppServiceClient : ZumoClient { + + public AppServiceClient(string url) : base(url) { + } + + public static AppServiceClient Create(string account) { + string url = AppUrl(account); + return new AppServiceClient(url); + } + + private const string URI_API = "api"; + + public AppServiceTable GetTable(string tableName) where E : class { + return new AppServiceTable(tableName, this); + } + + /// + /// GET custom API + /// + public IEnumerator InvokeApi(string apiName, Action> callback = null) where T : new() { + return InvokeApi(apiName, Method.GET, callback); + } + + /// + /// Invokes custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE + /// + public IEnumerator InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new() { + string url = ApiUrl(apiName); + Debug.Log(httpMethod.ToString() + " custom API Request Url: " + url); + ZumoRequest request = new ZumoRequest(url, httpMethod, true, User); + yield return request.Request.Send(); + request.ParseJson(callback); + } + + /// + /// Invokes custom API with body (of type B) and returning response (of type T) + /// + public IEnumerator InvokeApi(string apiName, Method httpMethod, B body, Action> callback = null) where T : new() { + string url = ApiUrl(apiName); + Debug.Log(httpMethod.ToString() + " custom API Request Url: " + url); + ZumoRequest request = new ZumoRequest(url, httpMethod, true, User); + request.AddBody(body); + yield return request.Request.Send(); + request.ParseJson(callback); + } + + private string ApiUrl(string apiName) { + return string.Format("{0}/{1}/{2}", Url, URI_API, apiName); + } + + } +} diff --git a/IAppServiceClient.cs b/IAppServiceClient.cs new file mode 100644 index 0000000..c67bf08 --- /dev/null +++ b/IAppServiceClient.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; + +namespace Azure.AppServices { + public interface IAppServiceClient : IZumoClient { + AppServiceTable GetTable(string tableName) where E : class; + + /// + /// Invokes a custom API using GET HTTP method + /// + IEnumerator InvokeApi(string apiName, Action> callback = null) where T : new(); + + /// + /// Invokes a custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE + /// + IEnumerator InvokeApi(string apiName, Method httpMethod, Action> callback = null) where T : new(); + + /// + /// Invokes a custom API with body object + /// + IEnumerator InvokeApi(string apiName, Method httpMethod, B body, Action> callback = null) where T : new(); + } +} diff --git a/IZumoClient.cs b/IZumoClient.cs new file mode 100644 index 0000000..77c94b3 --- /dev/null +++ b/IZumoClient.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; + +namespace Azure.AppServices { + public interface IZumoClient { + + AuthenticatedUser User { get; } + + IEnumerator LoginWithFacebook(string accessToken, Action> callback = null); + + IEnumerator LoginWithTwitter(string accessToken, string accessTokenSecret, Action> callback = null); + + IEnumerator LoginWithGoogle(string accessToken, string idToken, Action> callback = null); + + IEnumerator LoginWithMicrosoftAccount(string accessToken, Action> callback = null); + + IEnumerator LoginWithAAD(string accessToken, Action> callback = null); + + IEnumerator Logout(Action> callback = null); + } +} diff --git a/README.md b/README.md index 727e076..2b30a91 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,139 @@ -# Azure App Services for Unity3d -For game developers looking to use Azure App Services (previously Mobile Services) in their Unity project. +# Azure App Services for Unity +For Unity developers looking to use Azure App Services (previously Mobile Services) in their Unity game / app. + +## What's new +Please be aware this latest update of the library brings a number of changes to the namespace, some class names and method calls have been updated as Mobile Services is replaced by App Services. There is also a dependency on a shared [RESTClient for Unity](https://github.com/Unity3dAzure/RESTClient) so that multiple Unity libraries for Azure can be supported without adding duplicate scripts. + +## External dependencies +**First download the shared [REST Client library for Unity](https://github.com/Unity3dAzure/RESTClient) and extract the contents into your Unity project "Assets" folder.** +* [RESTClient](https://github.com/Unity3dAzure/RESTClient) + +## Requirements +Requires Unity v5.3 or greater as [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) and [JsonUtility](https://docs.unity3d.com/ScriptReference/JsonUtility.html) features are used. Unity will be extending platform support for UnityWebRequest so keep Unity up to date if you need to support these additional platforms. ## How to setup App Services with a new Unity project -1. [Download UnityRestClient](https://github.com/ProjectStratus/UnityRestClient/archive/master.zip) - * Copy JsonFx 'plugins' and the UnityRestClient 'Source' into project `Assets` folder -2. [Download AppServices](https://github.com/Unity3dAzure/AppServices/archive/master.zip) - * Copy 'AppServices' into project `Assets` folder. -3. Create an Azure App Service [Mobile App](https://portal.azure.com) +1. [Download AppServices](https://github.com/Unity3dAzure/AppServices/archive/master.zip) and [REST Client](https://github.com/Unity3dAzure/RESTClient/archive/master.zip) for Unity. + * Copy 'AppServices' and 'RESTClient' into project `Assets` folder. +2. Create an Azure App Service [Mobile App](https://portal.azure.com) * Create a Table (using Easy Tables) for app data. ## Azure App Services Demos for Unity 5 -Try the [Azure App Services Demos](https://github.com/Unity3dAzure/AppServicesDemo) project for Unity 5 on Mac / Windows. (The demo project has got everything already bundled in and does not require any additional assets to work. Just wire it up with your [Azure App Service](https://portal.azure.com) and run it right inside the Unity Editor.) +Try the [Azure App Services Demos](https://github.com/Unity3dAzure/AppServicesDemo) project for Unity v5.4.x on Mac / Windows. (The demo project has got everything already bundled in and does not require any additional assets to work. Just wire it up with your [Azure App Service](https://portal.azure.com) and run it right inside the Unity Editor.) For detailed instructions read my developer blog on [how to setup Azure App Services and Unity demo project](http://www.deadlyfingers.net/azure/azure-app-services-for-unity3d/). ## Supported Features -### MobileServiceClient +### AppServiceClient API API | Description --- | ----------- -Login | Client-directed login using access token. +LoginWithFacebook | Client-directed login using user access token. +LoginWithTwitter | Client-directed login using access token and access token secret. +LoginWithGoogle | Client-directed login using access token and id token. +LoginWithMicrosoftAccount | Client-directed login using access token. +LoginWithAAD | Client-directed login using access token. +Logout | Logout current user InvokeApi | Invoke custom API (Easy API) using GET, POST, PUT, PATCH or DELETE -### MobileServiceTable +### AppServiceTable API API | Description --- | ----------- Insert | Create a new item. Read | Get a list of items. Update | Update an item’s data using id property. -Delete | Delete an item using id property. +Delete | Delete an item using id property. Query | Get a list of results using a custom query. Lookup | Get an item’s data using id property. -### MobileServiceClient Interface - void Login(MobileServiceAuthenticationProvider provider, string token, Action> callback = null); - void InvokeApi(string apiName, Action> callback = null) where T : new(); +### [AppServiceClient Interface](https://github.com/Unity3dAzure/AppServices/blob/master/IAppServiceClient.cs) + LoginWithFacebook(string accessToken, Action> callback = null); + LoginWithTwitter(string accessToken, string accessTokenSecret, Action> callback = null); + LoginWithGoogle(string accessToken, string idToken,Action> callback = null); + LoginWithMicrosoftAccount(string accessToken,Action> callback = null); + LoginWithAAD(string accessToken, Action> callback = null); + Logout(Action> callback = null); -### MobileServiceTable Interface - void Insert(T item, Action> callback = null) where T : new(); - void Read(Action>> callback = null) where T : new(); - void Update(T item, Action> callback = null) where T : new(); - void Delete(string id, Action> callback = null) where T : new(); - void Query(CustomQuery query, Action>> callback = null) where T : new(); - void Query(CustomQuery query, Action> callback = null) where T : INestedResults, new(); - void Lookup(string id, Action> callback = null) where T : new(); +### [AppServiceClient (Easy APIs) Interface](https://github.com/Unity3dAzure/AppServices/blob/master/IAppServiceClient.cs) + InvokeApi (string apiName, Action> callback = null) where T : new(); + InvokeApi (string apiName, Method httpMethod, Action> callback = null) where T : new(); + InvokeApi (string apiName, Method httpMethod, B body, Action> callback = null) where T : new(); + +### [AppServiceTable (Easy Tables) Interface](https://github.com/Unity3dAzure/AppServices/blob/master/table/IAppServiceTable.cs) + Insert(T item, Action> callback = null) where T : new(); + Read(Action> callback = null) where T : new(); + Update(T item, Action> callback = null) where T : new(); + Delete(string id, Action> callback = null) where T : new(); + Query(TableQuery query, Action> callback = null) where T : new(); + Query(TableQuery query, Action> callback = null) where T : INestedResults, new(); + Lookup(string id, Action> callback = null) where T : new(); ## Sample usage + - Data model for **TodoItem.cs** +``` +using Azure.AppServices; +[System.Serializable] +public class TodoItem : DataModel { + public string text; + public bool complete; +} ``` + + - Unity script +``` +using System.Collections +using System.Collections.Generic; using UnityEngine; using System; -using System.Net; -using System.Collections.Generic; -using RestSharp; -using Pathfinding.Serialization.JsonFx; -using Unity3dAzure.AppServices; - +using RESTClient; +using Azure.AppServices; ``` ``` -private MobileServiceClient _client; -private MobileServiceTable _table; +private AppServiceClient _client; +private AppServiceTable _table; ``` ``` void Start () { - _client = new MobileServiceClient(appUrl); // <- add your app url here. - _table = _client.GetTable("TodoItem"); + _client = AppServiceClient.Create("unityapp"); // <- add your App Service account here! + _table = _client.GetTable("TodoItem"); + ReadItems(); } ``` ``` private void ReadItems() { - _table.Read(OnReadItemsCompleted); + StartCoroutine( _table.Read(OnReadItemsCompleted) ); } -private void OnReadItemsCompleted(IRestResponse> response) { - if ( response.StatusCode == HttpStatusCode.OK) { - Debug.Log("OnReadItemsCompleted data: " + response.Content); - List items = response.Data; - Debug.Log( "Todo items count: " + items.Count); - } else { - ResponseError err = JsonReader.Deserialize(response.Content); - Debug.Log("Error " + err.code.ToString() + " " + err.error + " Uri: " + response.ResponseUri); - } +private void OnReadItemsCompleted(IRestResponse response) { + if (!response.IsError) { + Debug.Log ("OnReadCompleted: " + response.Url + " data: " + response.Content); + TodoItem[] items = response.Data; + Debug.Log ( "Todo items count: " + items.Length); + } else { + Debug.LogWarning ("Read Error Status:" + response.StatusCode + " Url: " + response.Url); + } } ``` -## Dependencies -The REST service implements [UnityRestClient](https://github.com/ProjectStratus/UnityRestClient) which uses [JsonFx](https://bitbucket.org/TowerOfBricks/jsonfx-for-unity3d-git/) to parse JSON data. +## Known issues +* There is an issue with [PATCH on Android using UnityWebRequest with Azure App Services](http://answers.unity3d.com/questions/1230067/trying-to-use-patch-on-a-unitywebrequest-on-androi.html). Android won't recognize the "PATCH" http method currently required to update an item in App Services. One workaround is to enable the `X-HTTP-Method-Override` header. Here's the quick fix for App Services running node backend: + 1. Install the "method-override" package. + ``` + npm install method-override --save + ``` + 2. In 'app.js' file insert: + ``` + var methodOverride = require('method-override'); + // after the line "var app = express();" add + app.use(methodOverride('X-HTTP-Method-Override')); + ``` + +This will enable PATCH requests to be sent on Android. -## Supports +## Supported platforms +Intended to work on all the platforms [UnityWebRequest](https://docs.unity3d.com/Manual/UnityWebRequest.html) supports including: +* Unity Editor (Mac/PC) and Standalone players * iOS * Android * Windows -Questions or tweet #Azure #GameDev [@deadlyfingers](https://twitter.com/deadlyfingers) +Questions or tweet [@deadlyfingers](https://twitter.com/deadlyfingers) diff --git a/ZumoClient.cs b/ZumoClient.cs new file mode 100644 index 0000000..d233050 --- /dev/null +++ b/ZumoClient.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; +using System.Net; +using UnityEngine.Assertions; + +namespace Azure.AppServices { + public abstract class ZumoClient : RestClient, IZumoClient { + + public AuthenticatedUser User { get; private set; } + + public ZumoClient(string url) : base(url) { + } + + public static string AppUrl(string account) { + return string.Format("https://{0}.azurewebsites.net", account); + } + + /// + /// Client-directed single sign on for Facebook (using access token) + /// + public IEnumerator LoginWithFacebook(string accessToken, Action> callback = null) { + return Login(AuthenticationProvider.Facebook, accessToken, callback); + } + + /// + /// Client-directed single sign on for Twitter (using access token and access token secret) + /// + public IEnumerator LoginWithTwitter(string accessToken, string accessTokenSecret, Action> callback = null) { + string url = AppServiceAuthenticationProviderUrl(AuthenticationProvider.Twitter); + var request = new ZumoRequest(url, Method.POST, false); + request.AddBodyAccessTokenSecret(accessToken, accessTokenSecret); + yield return request.Request.Send(); + LoggedIn(request, callback); + } + + /// + /// Client-directed single sign on for Google+ (using access token and id token) + /// + public IEnumerator LoginWithGoogle(string accessToken, string idToken, Action> callback = null) { + string url = AppServiceAuthenticationProviderUrl(AuthenticationProvider.Google); + var request = new ZumoRequest(url, Method.POST, false); + request.AddBodyAccessTokenId(accessToken, idToken); + yield return request.Request.Send(); + LoggedIn(request, callback); + } + + /// + /// Client-directed single sign on for Microsoft Account (using access token) + /// + public IEnumerator LoginWithMicrosoftAccount(string accessToken, Action> callback = null) { + return Login(AuthenticationProvider.MicrosoftAccount, accessToken, callback); + } + + /// + /// Client-directed single sign on for Azure Active Directory (using access token) + /// + public IEnumerator LoginWithAAD(string accessToken, Action> callback = null) { + return Login(AuthenticationProvider.AAD, accessToken, callback); + } + + private IEnumerator Login(AuthenticationProvider authenticationProvider, + string accessToken, + Action> callback = null) { + string url = AppServiceAuthenticationProviderUrl(authenticationProvider); + var request = new ZumoRequest(url, Method.POST, false); + request.AddBodyAccessToken(accessToken); + yield return request.Request.Send(); + LoggedIn(request, callback); + } + + public IEnumerator Logout(Action> callback = null) { + if (User == null) { + Debug.LogWarning("Error, requires user login."); + yield break; + } + string url = string.Format("{0}/.auth/logout", Url); + var request = new ZumoRequest(url, Method.POST, false, User); + yield return request.Request.Send(); + if (callback == null) { + yield break; + } + HttpStatusCode statusCode = (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), request.Request.responseCode.ToString()); + RestResponse response = new RestResponse(statusCode, request.Request.url, request.Request.downloadHandler.text); + if (!statusCode.Equals(HttpStatusCode.OK)) { + Debug.LogWarning("Error, logout request failed."); + yield break; + } + // Detect result of logout webpage (using title tag to verify sign out success) with callback response + var match = Regex.Match(request.Request.downloadHandler.text, @"(.+)<\/title>", RegexOptions.IgnoreCase); + if (match.Groups.Count == 2 && !string.IsNullOrEmpty(match.Groups[1].Value)) { + string title = match.Groups[1].Value; + Assert.IsTrue(string.Equals(title, "You have been signed out")); + response = new RestResponse<string>(statusCode, request.Request.url, title); + } + callback(response); + User = null; + request.Dispose(); + } + + private string AppServiceAuthenticationProviderUrl(AuthenticationProvider authenticationProvider) { + return string.Format("{0}/.auth/login/{1}", Url, authenticationProvider.ToString().ToLower()); + } + + private void LoggedIn(ZumoRequest request, Action<IRestResponse<AuthenticatedUser>> callback = null) { + string message = request.Request.downloadHandler.text; + IRestResponse<AuthenticatedUser> response = request.ParseJson<AuthenticatedUser>(); + if (!response.IsError) { + User = response.Data; + if (callback != null) { + callback(response); + } + } else { + Debug.LogWarning("Login error message:" + message); + } + request.Dispose(); + } + + } +} diff --git a/authentication/AuthenticationProvider.cs b/authentication/AuthenticationProvider.cs new file mode 100644 index 0000000..597f7f9 --- /dev/null +++ b/authentication/AuthenticationProvider.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + // App Service authentication identity providers: Microsoft Account, Google Plus, Twitter, Facebook, Azure Active Directory + public enum AuthenticationProvider { + MicrosoftAccount = 0, + Google = 1, + Twitter = 2, + Facebook = 3, + AAD = 4 + } +} diff --git a/authentication/MobileServiceAuthenticationProvider.cs b/authentication/MobileServiceAuthenticationProvider.cs deleted file mode 100644 index bf7c8ed..0000000 --- a/authentication/MobileServiceAuthenticationProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Unity3dAzure.AppServices -{ - public enum MobileServiceAuthenticationProvider - { - MicrosoftAccount = 0, - Google = 1, - Twitter = 2, - Facebook = 3, - WindowsAzureActiveDirectory = 4 - } -} \ No newline at end of file diff --git a/client/IAzureMobileServiceClient.cs b/client/IAzureMobileServiceClient.cs deleted file mode 100644 index bcceb4a..0000000 --- a/client/IAzureMobileServiceClient.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System; -using RestSharp; - -namespace Unity3dAzure.AppServices -{ - [CLSCompliant(false)] - public interface IAzureMobileServiceClient - { - MobileServiceTable<E> GetTable<E>(string tableName) where E : class; - - /// <summary> - /// Client-directed login (sdk) for single sign on - /// https://msdn.microsoft.com/en-us/library/azure/jj710106.aspx - /// </summary> - void Login(MobileServiceAuthenticationProvider provider, string token, Action<IRestResponse<MobileServiceUser>> callback = null); - - /// <summary> - /// TODO: Service-directed login (webview) - /// </summary> - //void Login(MobileServiceAuthenticationProvider provider); - - /// <summary> - /// Invokes a custom API using GET HTTP method - /// </summary> - void InvokeApi<T>(string apiName, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Invokes a custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE - /// </summary> - void InvokeApi<T>(string apiName, Method httpMethod, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Invokes a custom API with body object - /// </summary> - void InvokeApi<T> (string apiName, Method httpMethod, T body, Action<IRestResponse<T>> callback = null) where T : new(); - } -} \ No newline at end of file diff --git a/client/MobileServiceClient.cs b/client/MobileServiceClient.cs deleted file mode 100644 index 8c07046..0000000 --- a/client/MobileServiceClient.cs +++ /dev/null @@ -1,162 +0,0 @@ -using UnityEngine; -using System.Collections; -using RestSharp; -using System.Collections.Generic; -using System; -using System.Text.RegularExpressions; - - -#if !NETFX_CORE || UNITY_ANDROID -using System.Net; -using System.Security.Cryptography.X509Certificates; -using System.Net.Security; -#endif - -namespace Unity3dAzure.AppServices -{ - [CLSCompliant(false)] - public class MobileServiceClient : RestClient, IAzureMobileServiceClient - { - public string AppUrl { get; private set; } - public string AppKey { get; private set; } - - public MobileServiceUser User { get; set; } - - public const string URI_API = "api/"; - - /// <summary> - /// Creates a new RestClient using Azure Mobile Service's Application Url and App Key - /// NB: Mobile Services should be migrated to use Azure App Service with constructor below. - /// </summary> - public MobileServiceClient(string appUrl, string appKey) : base(appUrl) - { - AppUrl = appUrl; - AppKey = appKey; - - // required for running in Windows and Android - #if !NETFX_CORE || UNITY_ANDROID - Debug.Log("ServerCertificateValidation"); - ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; - #endif - } - - /// <summary> - /// Creates a new RestClient using Azure App Service's Application Url - /// </summary> - public MobileServiceClient(string appUrl) : base(appUrl) - { - AppUrl = appUrl; - Debug.Log("AppUrl: " + AppUrl); - - // required for running in Windows and Android - #if !NETFX_CORE || UNITY_ANDROID - Debug.Log("ServerCertificateValidation"); - ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback; - #endif - } - - /// <summary> - /// Using factory method forces app url to be changed from 'http' to 'https' url - /// </summary> - public static MobileServiceClient Create(string appUrl) - { - return new MobileServiceClient (ForceHttps (appUrl)); - } - - public override string ToString() - { - return this.BaseUrl; - } - - public MobileServiceTable<E> GetTable<E>(string tableName) where E : class - { - return new MobileServiceTable<E>(tableName, this); - } - - /// <summary> - /// Client-directed single sign on (POST with access token) - /// </summary> - public void Login(MobileServiceAuthenticationProvider provider, string token, Action<IRestResponse<MobileServiceUser>> callback = null) - { - string p = provider.ToString().ToLower(); - string uri = IsAppService() ? ".auth/login/" + p : "login/" + p; - ZumoRequest request = new ZumoRequest(this, uri, Method.POST); - Debug.Log("Login Request Uri: " + uri + " access token: " + token); - request.AddBodyAccessToken(token); - this.ExecuteAsync(request, callback); - } - - /// <summary> - /// TODO: Service login (using GET via webview) - /// </summary> - /* - public void Login(MobileServiceAuthenticationProvider provider) - { - Debug.Log("TODO"); - } - //*/ - - /// <summary> - /// GET custom API - /// </summary> - public void InvokeApi<T>(string apiName, Action<IRestResponse<T>> callback = null) where T : new() - { - InvokeApi<T> (apiName, Method.GET, callback); - } - - /// <summary> - /// Invokes custom API for HTTP Methods: GET, POST, PUT, PATCH, DELETE - /// </summary> - public void InvokeApi<T>(string apiName, Method httpMethod, Action<IRestResponse<T>> callback = null) where T : new() - { - string uri = URI_API + apiName; - ZumoRequest request = new ZumoRequest(this, uri, httpMethod); - Debug.Log( httpMethod.ToString() + " custom API Request Uri: " + uri ); - this.ExecuteAsync(request, callback); - } - - /// <summary> - /// Invokes custom API with body - /// </summary> - public void InvokeApi<T>(string apiName, Method httpMethod, T body, Action<IRestResponse<T>> callback = null) where T : new() - { - string uri = URI_API + apiName; - ZumoRequest request = new ZumoRequest(this, uri, httpMethod); - request.AddBody (body); - Debug.Log( httpMethod.ToString() + " custom API Request Uri: " + uri ); - this.ExecuteAsync(request, callback); - } - - /// <summary> - /// When you copy the URL is is 'http' by default, but its preferable to use 'https' - /// </summary> - private static string ForceHttps(string appUrl) - { - return Regex.Replace(appUrl, "(?m)http://", "https://"); - } - - /// <summary> - /// Mobile Service uses an AppKey, but App Service does not. - /// </summary> - public bool IsAppService() - { - return String.IsNullOrEmpty(AppKey); - } - - #if !NETFX_CORE || UNITY_ANDROID - private bool RemoteCertificateValidationCallback(System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - // Check the certificate to see if it was issued from Azure - if (certificate.Subject.Contains("azurewebsites.net")) - { - return true; - } - else - { - return false; - } - } - #endif - - } -} diff --git a/helpers/Model.cs b/helpers/Model.cs deleted file mode 100644 index aa98c62..0000000 --- a/helpers/Model.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; - -namespace Unity3dAzure.AppServices -{ - /// <summary> - /// Helper methods to check and get object properties - /// </summary> - public class Model { - public static bool HasProperty(object obj, string propertyName) - { - return GetProperty(obj, propertyName) != null; - } - - public static PropertyInfo GetProperty(object obj, string propertyName) - { - #if NETFX_CORE - return obj.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); // workaround for GetProperty on Windows - #else - return obj.GetType().GetProperty(propertyName); - #endif - } - } -} \ No newline at end of file diff --git a/http/ZumoRequest.cs b/http/ZumoRequest.cs index ca57400..04f5e7b 100644 --- a/http/ZumoRequest.cs +++ b/http/ZumoRequest.cs @@ -1,38 +1,55 @@ -using UnityEngine; -using System.Collections; -using RestSharp; -using System; - -namespace Unity3dAzure.AppServices -{ - [CLSCompliant(false)] - public class ZumoRequest : RestRequest - { - public ZumoRequest(MobileServiceClient client, string uri, Method httpMethod) : base(uri, httpMethod) - { - if (client.IsAppService()) - { - // App Service headers - this.AddHeader("ZUMO-API-VERSION", "2.0.0"); - } else { - // Mobile Service headers - this.AddHeader("X-ZUMO-APPLICATION", client.AppKey); - } - - this.AddHeader("Accept", "application/json"); - this.RequestFormat = DataFormat.Json; - if (client.User != null && !string.IsNullOrEmpty(client.User.authenticationToken)) - { - this.AddHeader("X-ZUMO-AUTH", client.User.authenticationToken); - Debug.Log("Auth UserId:" + client.User.user.userId); - } - } - - public void AddBodyAccessToken(string token) - { - AccessToken accessToken = new AccessToken(token); - this.AddBody(accessToken); - } +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using UnityEngine; +using System.Text; +using RESTClient; + +namespace Azure.AppServices { + public sealed class ZumoRequest : RestRequest { + + // NB: Exclude system properties is a workaround to ignore read-only system properties during JSON utility deserialization. + // If set to true then App Services System Properties will be stripped from the request body during deserialization. + private bool excludeSystemProperties; + + public ZumoRequest(string url, Method httpMethod = Method.GET, bool excludeSystemProperties = true, AuthenticatedUser user = null) : base(url, httpMethod) { + this.excludeSystemProperties = excludeSystemProperties; + this.AddHeader("ZUMO-API-VERSION", "2.0.0"); + this.AddHeader("Accept", "application/json"); + this.AddHeader("Content-Type", "application/json; charset=utf-8"); + // User Authentictated request + if (user != null && !string.IsNullOrEmpty(user.authenticationToken)) { + this.AddHeader("X-ZUMO-AUTH", user.authenticationToken); + } + } + + // Facebook, Microsoft Account, Azure Active Directory + public void AddBodyAccessToken(string token) { + AccessToken accessToken = new AccessToken(token); + this.AddBody<AccessToken>(accessToken); + } + + // Twitter + public void AddBodyAccessTokenSecret(string token, string tokenSecret) { + AccessTokenSecret accessTokenSecret = new AccessTokenSecret(token, tokenSecret); + this.AddBody<AccessTokenSecret>(accessTokenSecret); + } + + // Google+ + public void AddBodyAccessTokenId(string token, string idToken) { + AccessTokenId accessTokenId = new AccessTokenId(token, idToken); + this.AddBody<AccessTokenId>(accessTokenId); } + + public override void AddBody<T>(T data, string contentType = "application/json; charset=utf-8") { + if (typeof(T) == typeof(string)) { + this.AddBody(data.ToString(), contentType); + return; + } + string jsonString = excludeSystemProperties ? JsonHelper.ToJsonExcludingSystemProperties(data) : JsonUtility.ToJson(data); + byte[] bytes = Encoding.UTF8.GetBytes(jsonString); + this.AddBody(bytes, contentType); + } + + } } diff --git a/model/AccessToken.cs b/model/AccessToken.cs index d74aea6..65ca563 100644 --- a/model/AccessToken.cs +++ b/model/AccessToken.cs @@ -1,18 +1,16 @@ -namespace Unity3dAzure.AppServices -{ - public class AccessToken - { - public string access_token { get; set; } - - /// <summary> - /// Facebook, Google, AAD access_token request - /// </summary> - public AccessToken(string accessTokenValue) - { - access_token = accessTokenValue; - } +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. - /// Needed only for Serialization (Fixes error: AccessToken cannot be serialized. Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute) - public AccessToken() {} +namespace Azure.AppServices { + [System.Serializable] + public class AccessToken { + public string access_token; + + /// <summary> + /// Facebook, Microsoft Account, Azure Active Directory "access_token" request body + /// </summary> + public AccessToken(string accessTokenValue) { + access_token = accessTokenValue.Trim(); } -} \ No newline at end of file + } +} diff --git a/model/AccessTokenId.cs b/model/AccessTokenId.cs new file mode 100644 index 0000000..6a9c0df --- /dev/null +++ b/model/AccessTokenId.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class AccessTokenId { + public string access_token; + public string id_token; + + /// <summary> + /// Google+ "access_token" and "id_token" request body + /// </summary> + public AccessTokenId(string accessTokenValue, string idTokenValue) { + access_token = accessTokenValue.Trim(); + id_token = idTokenValue.Trim(); + } + } +} diff --git a/model/AccessTokenSecret.cs b/model/AccessTokenSecret.cs new file mode 100644 index 0000000..a171fa7 --- /dev/null +++ b/model/AccessTokenSecret.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class AccessTokenSecret { + public string access_token; + public string access_token_secret; + + /// <summary> + /// Twitter with "access_token" and "access_token_secret" request body + /// </summary> + public AccessTokenSecret(string accessTokenValue, string accessTokenSecret) { + access_token = accessTokenValue.Trim(); + access_token_secret = accessTokenSecret.Trim(); + } + } +} diff --git a/model/AuthenticatedUser.cs b/model/AuthenticatedUser.cs new file mode 100644 index 0000000..5a6bb88 --- /dev/null +++ b/model/AuthenticatedUser.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class AuthenticatedUser { + public string authenticationToken; + public User user; + } +} diff --git a/model/MobileServiceUser.cs b/model/MobileServiceUser.cs deleted file mode 100644 index c025765..0000000 --- a/model/MobileServiceUser.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Unity3dAzure.AppServices -{ - public class MobileServiceUser - { - public string authenticationToken { get; set; } - public User user { get; set; } - } -} \ No newline at end of file diff --git a/model/ResponseError.cs b/model/ResponseError.cs index d630991..7dcbc5d 100644 --- a/model/ResponseError.cs +++ b/model/ResponseError.cs @@ -1,8 +1,9 @@ -namespace Unity3dAzure.AppServices -{ - public class ResponseError - { - public int code { get; set; } - public string error { get; set; } - } -} \ No newline at end of file +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class ResponseError { + public string error; + } +} diff --git a/model/User.cs b/model/User.cs index 61e107a..3b7fdcd 100644 --- a/model/User.cs +++ b/model/User.cs @@ -1,7 +1,9 @@ -namespace Unity3dAzure.AppServices -{ - public class User - { - public string userId { get; set; } - } -} \ No newline at end of file +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + [System.Serializable] + public class User { + public string userId; + } +} diff --git a/model/base/DataModel.cs b/model/base/DataModel.cs index 920a4bf..16f6f65 100644 --- a/model/base/DataModel.cs +++ b/model/base/DataModel.cs @@ -1,37 +1,74 @@ -using System; -using System.ComponentModel; -using Pathfinding.Serialization.JsonFx; - -namespace Unity3dAzure.AppServices -{ - [Serializable] - [CLSCompliant(false)] - public class DataModel : IDataModel - { - public string id { get; set; } - - // system properties - [JsonIgnore] public DateTime createdAt { get; private set; } - [JsonIgnore] public DateTime updatedAt { get; private set; } - [JsonIgnore] public string version { get; private set; } - [JsonIgnore] public bool deleted { get; private set; } - - // `$inlinecount=allpages` property - [EditorBrowsable(EditorBrowsableState.Never)] - [JsonIgnore] public uint ROW_NUMBER { get; private set; } - public uint GetIndex() - { - return ROW_NUMBER; - } - - public string GetId() - { - return id; - } - - public override string ToString () - { - return string.Format ("id:{0}, createdAt:{1}, updatedAt:{2}, version:{3}, deleted:{4}, index:{5}", id, createdAt, updatedAt, version, deleted, GetIndex()); - } - } -} \ No newline at end of file +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using UnityEngine; + +namespace Azure.AppServices { + [Serializable] + public abstract class DataModel : IDataModel { + [SerializeField] + public string id; + + public string GetId() { + return id; + } + + public void SetId(string id) { + this.id = id; + } + + // System Properties (read only) + [SerializeField] + private string createdAt; + private DateTime? _createdAt; + + public DateTime? CreatedAt() { + if (_createdAt == null) { + _createdAt = Convert.ToDateTime(createdAt); + } + return _createdAt; + } + + [SerializeField] + private string updatedAt; + private DateTime? _updatedAt; + + public DateTime? UpdatedAt() { + if (_updatedAt == null) { + _updatedAt = Convert.ToDateTime(updatedAt); + } + return _updatedAt; + } + + [SerializeField] + private string version; + + public string Version() { + return version; + } + + [SerializeField] + private bool deleted; + + public bool Deleted() { + return deleted; + } + + [SerializeField] + private string ROW_NUMBER; + + public uint RowNumber() { + return Convert.ToUInt32(ROW_NUMBER, 10); + } + + public override string ToString() { + return JsonUtility.ToJson(this); + } + + public string ToJSON() { + return JsonHelper.ToJsonExcludingSystemProperties(this); + } + } +} diff --git a/model/base/IDataModel.cs b/model/base/IDataModel.cs index 370ca6b..b64afd3 100644 --- a/model/base/IDataModel.cs +++ b/model/base/IDataModel.cs @@ -1,7 +1,8 @@ -namespace Unity3dAzure.AppServices -{ - public interface IDataModel - { - string GetId(); - } -} \ No newline at end of file +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Azure.AppServices { + public interface IDataModel { + string GetId(); + } +} diff --git a/model/nested/INestedResults.cs b/model/nested/INestedResults.cs deleted file mode 100644 index d6223ac..0000000 --- a/model/nested/INestedResults.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System; -using RestSharp; - -namespace Unity3dAzure.AppServices -{ - /// <summary> - /// Interface to support table Query with `$inlinecount=allpages` - /// </summary> - public interface INestedResults - { - } -} \ No newline at end of file diff --git a/model/nested/NestedResults.cs b/model/nested/NestedResults.cs deleted file mode 100644 index c8eb813..0000000 --- a/model/nested/NestedResults.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Unity3dAzure.AppServices -{ - /// <summary> - /// Wrap your data model with this object to call the table Query with `$inlinecount=allpages` param. - /// </summary> - [CLSCompliant(false)] - [Serializable] - public class NestedResults<T> : INestedResults - { - public uint count { get; set; } - public List<T> results { get; set; } - } -} \ No newline at end of file diff --git a/table/AppServiceTable.cs b/table/AppServiceTable.cs new file mode 100644 index 0000000..304372b --- /dev/null +++ b/table/AppServiceTable.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using UnityEngine; +using System.Collections; +using System; + +namespace Azure.AppServices { + public class AppServiceTable<E> : IAppServiceTable { + private AppServiceClient _client; + private string _name; + + private const string URI_TABLES = "tables"; + + public AppServiceTable(string tableName, AppServiceClient client) { + _client = client; + _name = tableName; // NB: The table name could be infered from the Table's DataModel using typeof(E).Name; but passing Table name as a string allows for the case when the Classname is not the same as the Table name. + } + + public override string ToString() { + return _name; + } + + public IEnumerator Insert<T>(T item, Action<IRestResponse<T>> callback = null) where T : new() { + string url = TableUrl(_name); + ZumoRequest request = new ZumoRequest(url, Method.POST, true, _client.User); + Debug.Log("Insert Request: " + url); + request.AddBody(item); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + public IEnumerator Read<T>(Action<IRestResponse<T[]>> callback = null) where T : new() { + string url = TableUrl(_name); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + Debug.Log("Read Request: " + url); + yield return request.Request.Send(); + request.ParseJsonArray<T>(callback); + } + + public IEnumerator Query<T>(TableQuery query, Action<IRestResponse<T[]>> callback = null) where T : new() { + string url = TableQueryUrl(_name, query.ToString()); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + Debug.Log("Query Request: " + url + " Query:" + query); + yield return request.Request.Send(); + request.ParseJsonArray<T>(callback); + } + + public IEnumerator Query<T>(TableQuery query, Action<IRestResponse<NestedResults<T>>> callback = null) where T : new() { + string q = query.ToString(); + string nestedQueryString = string.Format("{0}{1}$inlinecount=allpages", q, (q.Length > 1) ? "&" : "?"); + string url = TableQueryUrl(_name, nestedQueryString); + Debug.Log("Query Request: " + url + " Paginated Query:" + query); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + yield return request.Request.Send(); + request.ParseJsonNestedArray<T, NestedResults<T>>("results", callback); + } + + public IEnumerator Update<T>(T item, Action<IRestResponse<T>> callback = null) where T : new() { + string id = null; + // Check if item uses the 'IDataModel' Interface to get 'id' property, otherwise try Refelection (using Reflection helper). + IDataModel model = item as IDataModel; + if (model != null) { + id = model.GetId(); + } else if (ReflectionHelper.HasField(item, "id")) { + var x = ReflectionHelper.GetField(item, "id"); + id = x.GetValue(item) as string; + } else { + Debug.LogError("Unable to get 'id' data model property"); + } + if (string.IsNullOrEmpty(id)) { + Debug.LogError("Error 'id' value is missing"); + yield return null; + } + string url = TableUrl(_name, id); + ZumoRequest request = new ZumoRequest(url, Method.PATCH, true, _client.User); + request.AddBody(item); + Debug.Log("Update Request Url: " + url + " patch:" + item); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + public IEnumerator Delete<T>(string id, Action<IRestResponse<T>> callback = null) where T : new() { + string url = TableUrl(_name, id); + ZumoRequest request = new ZumoRequest(url, Method.DELETE, true, _client.User); + Debug.Log("Delete Request Url: " + url); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + public IEnumerator Lookup<T>(string id, Action<IRestResponse<T>> callback = null) where T : new() { + string url = TableUrl(_name, id); + ZumoRequest request = new ZumoRequest(url, Method.GET, true, _client.User); + Debug.Log("Lookup Request Url: " + url); + yield return request.Request.Send(); + request.ParseJson<T>(callback); + } + + private string TableUrl(string tableName, string path = "") { + string resourcePath = string.IsNullOrEmpty(path) ? tableName : tableName + "/" + path; + return string.Format("{0}/{1}/{2}", _client.Url, URI_TABLES, resourcePath); + } + + private string TableQueryUrl(string tableName, string query) { + return string.Format("{0}/{1}/{2}{3}", _client.Url, URI_TABLES, tableName, query); + } + + } +} diff --git a/table/IAppServiceTable.cs b/table/IAppServiceTable.cs new file mode 100644 index 0000000..2841d7d --- /dev/null +++ b/table/IAppServiceTable.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using RESTClient; +using System; +using System.Collections; + +namespace Azure.AppServices { + public interface IAppServiceTable { + /// <summary> + /// Create a new item. + /// </summary> + IEnumerator Insert<T>(T item, Action<IRestResponse<T>> callback = null) where T : new(); + + /// <summary> + /// Get a list of items. + /// </summary> + IEnumerator Read<T>(Action<IRestResponse<T[]>> callback = null) where T : new(); + + /// <summary> + /// Update an item's data using id property. + /// </summary> + IEnumerator Update<T>(T item, Action<IRestResponse<T>> callback = null) where T : new(); + + /// <summary> + /// Delete an item using id property. + /// </summary> + IEnumerator Delete<T>(string id, Action<IRestResponse<T>> callback = null) where T : new(); + + /// <summary> + /// Get a list of results using an App Service table query. + /// More info about queries here: + /// https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx + /// </summary> + IEnumerator Query<T>(TableQuery query, Action<IRestResponse<T[]>> callback = null) where T : new(); + + /// <summary> + /// Returns a 'count' and nested list of 'results' (appends `$inlinecount=allpages` parameter to the query) + /// </summary> + IEnumerator Query<T>(TableQuery query, Action<IRestResponse<NestedResults<T>>> callback = null) where T : new(); + + /// <summary> + /// Get an item's data using id property. + /// </summary> + IEnumerator Lookup<T>(string id, Action<IRestResponse<T>> callback = null) where T : new(); + } +} diff --git a/table/IAzureMobileServiceTable.cs b/table/IAzureMobileServiceTable.cs deleted file mode 100644 index 676b959..0000000 --- a/table/IAzureMobileServiceTable.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System; -using RestSharp; - -namespace Unity3dAzure.AppServices -{ - /// <remarks> - /// NB: Interface uses callbacks as Unity does not support async methods right now. - /// An async method would mean we could just return result as success or failure and eliminate need for a callback. - /// <example> - /// List<T> Read<T>() where T : new(); - /// </example> - /// </remarks> - [CLSCompliant(false)] - public interface IAzureMobileServiceTable - { - /// <summary> - /// Create a new item. - /// </summary> - void Insert<T>(T item, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Get a list of items. - /// </summary> - void Read<T>(Action<IRestResponse<List<T>>> callback = null) where T : new(); - - /// <summary> - /// Update an item's data using id property. - /// </summary> - void Update<T>(T item, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Delete an item using id property. - /// </summary> - void Delete<T>(string id, Action<IRestResponse<T>> callback = null) where T : new(); - - /// <summary> - /// Get a list of results using a custom query. - /// More info about custom queries here: - /// https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx - /// </summary> - void Query<T>(CustomQuery query, Action<IRestResponse<List<T>>> callback = null) where T : new(); - - /// <summary> - /// Returns a 'count' and nested list of 'results' (appends `$inlinecount=allpages` parameter to the query) - /// </summary> - void Query<T>(CustomQuery query, Action<IRestResponse<T>> callback = null) where T : INestedResults, new(); - - /// <summary> - /// Get an item's data using id property. - /// </summary> - void Lookup<T>(string id, Action<IRestResponse<T>> callback = null) where T : new(); - } -} \ No newline at end of file diff --git a/table/MobileServiceTable.cs b/table/MobileServiceTable.cs deleted file mode 100644 index 188bcd7..0000000 --- a/table/MobileServiceTable.cs +++ /dev/null @@ -1,111 +0,0 @@ -using UnityEngine; -using System.Collections; -using RestSharp; -using System.Collections.Generic; -using System; - -namespace Unity3dAzure.AppServices -{ - [CLSCompliant(false)] - public class MobileServiceTable<E> : IAzureMobileServiceTable - { - private MobileServiceClient _client; - private string _name; - - public const string URI_TABLES = "tables/"; - - public MobileServiceTable(string tableName, MobileServiceClient client) - { - _client = client; - _name = tableName; // NB: The table name could be infered from the Table's DataModel using typeof(E).Name; but passing Table name as a string allows for the case when the Classname is not the same as the Table name. - } - - public override string ToString() - { - return _name; - } - - public void Insert<T>(T item, Action<IRestResponse<T>> callback = null) where T : new() - { - string uri = URI_TABLES + _name; - ZumoRequest request = new ZumoRequest(_client, uri, Method.POST); - Debug.Log( "Insert Request: " + uri ); - request.AddBody(item); - _client.ExecuteAsync<T>(request, callback); - } - - public void Read<T>(Action<IRestResponse<List<T>>> callback = null) where T : new() - { - string uri = URI_TABLES + _name; - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Read Request: " + uri ); - _client.ExecuteAsync<List<T>>(request, callback); - } - - public void Query<T>(CustomQuery query, Action<IRestResponse<List<T>>> callback = null) where T : new() - { - string uri = string.Format("{0}{1}{2}", URI_TABLES, _name, query); - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Query Request: " + uri +" Query:"+ query ); - _client.ExecuteAsync<List<T>> (request, callback); - } - - public void Query<T>(CustomQuery query, Action<IRestResponse<T>> callback = null) where T : INestedResults, new() - { - string queryResults = query.ToString (); - string q = queryResults.Length > 0 ? "&" : "?"; - queryResults += string.Format("{0}$inlinecount=allpages", q); - string uri = string.Format("{0}{1}{2}", URI_TABLES, _name, queryResults); - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Query Request: " + uri +" Query with inlinecount:"+ queryResults ); - _client.ExecuteAsync<T> (request, callback); - } - - public void Update<T>(T item, Action<IRestResponse<T>> callback = null) where T : new() - { - string id = null; - // Check if model uses the 'IDataModel' Interface to get id property, otherwise try Refelection (using 'Model' helper). - IDataModel model = item as IDataModel; - if (model != null) - { - id = model.GetId(); - } - else if ( Model.HasProperty(item, "id") ) - { - var x = Model.GetProperty(item, "id"); - id = x.GetValue(item, null) as string; - } - else - { - Debug.LogError("Unable to get 'id' data model property"); - } - if (string.IsNullOrEmpty(id)) - { - Debug.LogError("Error 'id' value is missing"); - return; - } - string uri = URI_TABLES + _name + "/" + id; - ZumoRequest request = new ZumoRequest(_client, uri, Method.PATCH); - Debug.Log("Update Request Uri: " + uri); - request.AddBody(item); - _client.ExecuteAsync<T>(request, callback); - } - - public void Delete<T>(string id, Action<IRestResponse<T>> callback = null) where T : new() - { - string uri = URI_TABLES + _name + "/" + id; - ZumoRequest request = new ZumoRequest(_client, uri, Method.DELETE); - Debug.Log( "Delete Request Uri: " + uri ); - _client.ExecuteAsync<T>(request, callback); - } - - public void Lookup<T>(string id, Action<IRestResponse<T>> callback = null) where T : new() - { - string uri = URI_TABLES + _name + "/" + id; - ZumoRequest request = new ZumoRequest(_client, uri, Method.GET); - Debug.Log( "Lookup Request Uri: " + uri ); - _client.ExecuteAsync<T>(request, callback); - } - - } -} \ No newline at end of file diff --git a/table/query/CustomQuery.cs b/table/query/CustomQuery.cs deleted file mode 100644 index a70ad24..0000000 --- a/table/query/CustomQuery.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Text; -using UnityEngine; - -/// <summary> -/// Implemention of Query records operation https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx -/// There is a maximum of 50 records returned in a query - use top and skip params to return additional pages of results. -/// NB: `$inlinecount` (which returns count of all items without paging applied) is not set here as it changes the data model shape and the way the REST decode callback works makes it non-trival to decode. -/// Rather the '$inlinecount=allpages' param is automatically set when using the table's Query method and wrapping your data model with the NestedResults object wrapper. -/// </summary> -namespace Unity3dAzure.AppServices -{ - [Flags] - public enum MobileServiceSystemProperty - { - nil = 0x0, - createdAt = 0x1, - updatedAt = 0x2, - version = 0x4, - deleted = 0x8 - } - - [CLSCompliant (false)] - public class CustomQuery - { - // query option parameters defined by the Open Data Protocol (OData) - public string Filter; - public string OrderBy; - public uint Top; - public uint Skip; - public string Select; - // other params - public MobileServiceSystemProperty SystemProperties; - public bool IncludeDeleted; - - public CustomQuery (string filter = "", string orderBy = null, uint top = 0, uint skip = 0, string select = null, MobileServiceSystemProperty systemProperties = MobileServiceSystemProperty.nil, bool includeDeleted = false) - { - this.Filter = filter; // return only rows that satisfy the specified filter predicate - this.OrderBy = orderBy; // sort column by one or more columns: order can be specified in 'desc' or 'asc' order ('asc' is default) - this.Top = top; // return the top n entities for any query - this.Skip = skip; // the n of records to skip (used for paging results) - this.Select = select; // defines new projection of data by specifying the columns - this.SystemProperties = systemProperties; // list of system properties to be included in the response - this.IncludeDeleted = includeDeleted; // if table has soft delete enabled then deleted records will be included in the results - } - - public static CustomQuery CreateWithOrderBy (string orderBy) - { - return new CustomQuery ("", orderBy); - } - - public override string ToString () - { - string queryString = ""; - string q = "?"; - if (!string.IsNullOrEmpty (this.Filter)) { - queryString += string.Format ("{0}$filter=({1})", q, this.Filter); - q = "&"; - } - if (!string.IsNullOrEmpty (this.OrderBy)) { - queryString += string.Format ("{0}$orderby={1}", q, this.OrderBy); - q = "&"; - } - if (this.Top > 0) { - queryString += string.Format ("{0}$top={1}", q, this.Top.ToString ()); - q = "&"; - } - if (this.Skip > 0) { - queryString += string.Format ("{0}$skip={1}", q, this.Skip.ToString ()); - q = "&"; - } - if (!string.IsNullOrEmpty (this.Select)) { - queryString += string.Format ("{0}$select={1}", q, this.Select); - q = "&"; - } - if (this.SystemProperties != MobileServiceSystemProperty.nil) { - // NB: setting __systemproperties param doesn't seem to do anything different as these properties are all included by default, but we can append values to the 'select' param. - if (!string.IsNullOrEmpty (this.Select)) { - queryString += string.Format (",{0}", SystemPropertiesValues (this.SystemProperties)); - } - queryString += string.Format ("{0}__systemproperties={1}", q, SystemPropertiesValues (this.SystemProperties)); - q = "&"; - } - if (this.IncludeDeleted) { - queryString += string.Format ("{0}__includeDeleted=true", q); - } - return EscapeURL (queryString); - } - - private string EscapeURL (string query) - { - string q = WWW.EscapeURL (query); - StringBuilder sb = new StringBuilder (q); - sb.Replace ("+", "%20"); // NB: replace space with '%20' instead of '+' - // keep query params: ?&=$ - sb.Replace ("%3f", "?"); - sb.Replace ("%26", "&"); - sb.Replace ("%3d", "="); - sb.Replace ("%24", "$"); - return sb.ToString (); - } - - private string SystemPropertiesValues (MobileServiceSystemProperty systemProperties) - { - if (systemProperties == MobileServiceSystemProperty.nil) { - return ""; - } - return systemProperties.ToString ().Replace (" ", ""); // remove spaces from string - } - } -} \ No newline at end of file diff --git a/table/query/OrderBy.cs b/table/query/OrderBy.cs new file mode 100644 index 0000000..1ea0bcf --- /dev/null +++ b/table/query/OrderBy.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using UnityEngine; + +namespace Azure.AppServices { + + public enum SortDirection { + asc, + desc + } + + public class OrderBy { + private string column; + private SortDirection? columnSortDirection; + + public OrderBy(string column, SortDirection? sortDirection = null) { + this.column = WWW.EscapeURL(column); + this.columnSortDirection = sortDirection; + } + + public override string ToString() { + if (!string.IsNullOrEmpty(column)) { + string sort = (columnSortDirection != null) ? "+" + columnSortDirection : ""; + return column + sort; + } + return ""; + } + + } +} diff --git a/table/query/TableQuery.cs b/table/query/TableQuery.cs new file mode 100644 index 0000000..5775154 --- /dev/null +++ b/table/query/TableQuery.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text; +using UnityEngine; + +/// <summary> +/// Implemention of Query records operation https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx +/// By default there is a maximum of 50 records returned in a query - use top and skip params to return additional pages of results. +/// NB: The `$inlinecount` param (which returns total count of all items without paging applied) is not set here as it alters the data model shape. +/// Rather the '$inlinecount=allpages' param is automatically set when using the table's Query method and wrapping your data model with the NestedResults object. +/// </summary> +namespace Azure.AppServices { + [Flags] + public enum TableSystemProperty { + nil = 0x0, + createdAt = 0x1, + updatedAt = 0x2, + version = 0x4, + deleted = 0x8 + } + + public class TableQuery { + public string Filter; + public OrderBy[] OrderBy; + public string Select; + public uint Skip; + public uint Top; + + // other params + public bool IncludeDeleted; + public TableSystemProperty SystemProperties; + public bool NoScript = false; // When a value of `true` is supplied, the execution of registered scripts is suppressed. + // NB: To suppress script execution, you must also supply the service master key with the "X-ZUMO-MASTER" header. + // For more info and security note refer to: https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx + + public TableQuery(string filterPredicate = "", uint top = 0, uint skip = 0, string selectColumns = null, TableSystemProperty systemProperties = TableSystemProperty.nil, bool includeDeleted = false, params OrderBy[] orderBy) { + // supports the following subset of the query option parameters defined by the Open Data Protocol (OData) + this.Filter = filterPredicate; // return only rows that satisfy the specified filter predicate + this.OrderBy = orderBy; // sort column by one or more columns: order can be specified in 'desc' or 'asc' order ('asc' is default) + this.Select = selectColumns; // defines new projection of data by specifying the columns + this.Skip = skip; // the n of records to skip (used for paging results) + this.Top = top; // return the top n entities for any query + + this.IncludeDeleted = includeDeleted; // if table has soft delete enabled then deleted records will be included in the results + this.SystemProperties = systemProperties; // list of system properties to be included in the response + } + + public static TableQuery CreateWithOrderBy(params OrderBy[] orderBy) { + return new TableQuery("", 0, 0, null, TableSystemProperty.nil, false, orderBy); + } + + public override string ToString() { + StringBuilder sb = new StringBuilder("?"); + if (!string.IsNullOrEmpty(Filter)) { + sb.Append(string.Format("$filter=({0})&", WWW.EscapeURL(Filter))); + } + if (OrderBy != null && OrderBy.Length > 0) { + sb.Append(string.Format("$orderby=")); + foreach (OrderBy order in OrderBy) { + sb.Append(order.ToString() + ","); + } + sb.Remove(sb.Length - 1, 1); // remove trailing ',' + sb.Append(string.Format("&")); + } + if (Top > 0) { + sb.Append(string.Format("$top={0}&", Top.ToString())); + } + if (Skip > 0) { + sb.Append(string.Format("$skip={0}&", Skip.ToString())); + } + if (!string.IsNullOrEmpty(Select)) { + sb.Append(string.Format("$select={0}&", WWW.EscapeURL(Select))); + if (SystemProperties != TableSystemProperty.nil) { + // NB: setting `__systemproperties` param doesn't appear to do anything so I've added a workaround to append these values onto the 'select' param. + sb.Remove(sb.Length - 1, 1); // remove trailing '&' + sb.Append(string.Format(",{0}&", SystemPropertiesValues(this.SystemProperties))); + } + } + if (SystemProperties != TableSystemProperty.nil) { + sb.Append(string.Format("__systemproperties={0}&", SystemPropertiesValues(SystemProperties))); + } + if (this.IncludeDeleted) { + sb.Append("__includeDeleted=true&"); + } + if (this.NoScript) { + sb.Append("noscript=true&"); + } + sb.Remove(sb.Length - 1, 1); // remove trailing '&' + return sb.ToString(); + } + + private string SystemPropertiesValues(TableSystemProperty systemProperties) { + if (systemProperties == TableSystemProperty.nil) { + return ""; + } + return systemProperties.ToString().Replace(" ", ""); // remove spaces from string + } + } +}