From 94f1c9fecac6ce826d630d9fe119b46ed2d1f91e Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 11:32:04 +0800 Subject: [PATCH 1/8] * Utils.cs: * SMSTest.cs: * LCCaptchaClient.cs: * CaptchaTest.cs: * Storage-Unity.csproj: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LCSMSClient.cs: chore: 支持验证码和短信 --- Storage/Storage-Unity/Storage-Unity.csproj | 6 ++ Storage/Storage.Test/CaptchaTest.cs | 36 ++++++++++ Storage/Storage.Test/SMSTest.cs | 40 +++++++++++ Storage/Storage.Test/Utils.cs | 4 ++ Storage/Storage/LCCaptchaClient.cs | 66 +++++++++++++++++++ Storage/Storage/LCSMSClient.cs | 77 ++++++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 Storage/Storage.Test/CaptchaTest.cs create mode 100644 Storage/Storage.Test/SMSTest.cs create mode 100644 Storage/Storage/LCCaptchaClient.cs create mode 100644 Storage/Storage/LCSMSClient.cs diff --git a/Storage/Storage-Unity/Storage-Unity.csproj b/Storage/Storage-Unity/Storage-Unity.csproj index 5eca288..1151018 100644 --- a/Storage/Storage-Unity/Storage-Unity.csproj +++ b/Storage/Storage-Unity/Storage-Unity.csproj @@ -117,6 +117,12 @@ Internal\Query\LCCompositionalCondition.cs + + LCCaptchaClient.cs + + + LCSMSClient.cs + diff --git a/Storage/Storage.Test/CaptchaTest.cs b/Storage/Storage.Test/CaptchaTest.cs new file mode 100644 index 0000000..773ed3a --- /dev/null +++ b/Storage/Storage.Test/CaptchaTest.cs @@ -0,0 +1,36 @@ +using NUnit.Framework; +using System.Threading.Tasks; +using LeanCloud; +using LeanCloud.Storage; + +using static NUnit.Framework.TestContext; + +namespace Storage.Test { + public class CaptchaTest { + [SetUp] + public void SetUp() { + LCLogger.LogDelegate += Utils.Print; + LCApplication.Initialize(Utils.AppId, Utils.AppKey, Utils.AppServer); + } + + [TearDown] + public void TearDown() { + LCLogger.LogDelegate -= Utils.Print; + } + + //[Test] + public async Task Request() { + LCCapture captcha = await LCCaptchaClient.RequestCaptcha(); + WriteLine($"url: {captcha.Url}"); + WriteLine($"token: {captcha.Token}"); + Assert.NotNull(captcha); + Assert.NotNull(captcha.Url); + Assert.NotNull(captcha.Token); + } + + //[Test] + public async Task Verify() { + await LCCaptchaClient.VerifyCaptcha("on2r", "1TUDkEMu"); + } + } +} diff --git a/Storage/Storage.Test/SMSTest.cs b/Storage/Storage.Test/SMSTest.cs new file mode 100644 index 0000000..cb76dad --- /dev/null +++ b/Storage/Storage.Test/SMSTest.cs @@ -0,0 +1,40 @@ +using NUnit.Framework; +using System.Collections.Generic; +using System.Threading.Tasks; +using LeanCloud; +using LeanCloud.Storage; + +namespace Storage.Test { + public class SMSTest { + [SetUp] + public void SetUp() { + LCLogger.LogDelegate += Utils.Print; + LCApplication.Initialize(Utils.AppId, Utils.AppKey, Utils.AppServer); + } + + [TearDown] + public void TearDown() { + LCLogger.LogDelegate -= Utils.Print; + } + + //[Test] + public async Task RequestSMS() { + await LCSMSClient.RequestSMSCode("15101006007", + template: "test_template", + signature: "flutter-test", + variables: new Dictionary { + { "k1", "v1" } + }); + } + + //[Test] + public async Task RequestVoice() { + await LCSMSClient.RequestVoiceCode("+8615101006007"); + } + + //[Test] + public async Task Verify() { + await LCSMSClient.VerifyMobilePhone("15101006007", ""); + } + } +} diff --git a/Storage/Storage.Test/Utils.cs b/Storage/Storage.Test/Utils.cs index e7e6df2..3876751 100644 --- a/Storage/Storage.Test/Utils.cs +++ b/Storage/Storage.Test/Utils.cs @@ -3,6 +3,10 @@ using LeanCloud; namespace Storage.Test { public static class Utils { + internal const string AppId = "ikGGdRE2YcVOemAaRbgp1xGJ-gzGzoHsz"; + internal const string AppKey = "NUKmuRbdAhg1vrb2wexYo1jo"; + internal const string AppServer = "https://ikggdre2.lc-cn-n1-shared.com"; + internal static void Print(LCLogLevel level, string info) { switch (level) { case LCLogLevel.Debug: diff --git a/Storage/Storage/LCCaptchaClient.cs b/Storage/Storage/LCCaptchaClient.cs new file mode 100644 index 0000000..968c730 --- /dev/null +++ b/Storage/Storage/LCCaptchaClient.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LeanCloud.Storage { + /// + /// 验证码 + /// + public class LCCapture { + public string Url { + get; set; + } + + public string Token { + get; set; + } + } + + /// + /// 验证码工具类 + /// + public static class LCCaptchaClient { + /// + /// 请求验证码 + /// + /// 验证码图片宽度 + /// 验证码图片高度 + /// + public static async Task RequestCaptcha(int width = 82, + int height = 39) { + string path = "requestCaptcha"; + Dictionary queryParams = new Dictionary { + { "width", width }, + { "height", height } + }; + Dictionary response = await LCApplication.HttpClient.Get>(path, queryParams: queryParams); + return new LCCapture { + Url = response["captcha_url"] as string, + Token = response["captcha_token"] as string + }; + } + + /// + /// 验证 + /// + /// + /// + /// + public static async Task VerifyCaptcha(string code, + string token) { + if (string.IsNullOrEmpty(code)) { + throw new ArgumentNullException(nameof(code)); + } + if (string.IsNullOrEmpty(token)) { + throw new ArgumentNullException(nameof(token)); + } + + string path = "verifyCaptcha"; + Dictionary data = new Dictionary { + { "captcha_code", code }, + { "captcha_token", token } + }; + await LCApplication.HttpClient.Post>(path, data: data); + } + } +} diff --git a/Storage/Storage/LCSMSClient.cs b/Storage/Storage/LCSMSClient.cs new file mode 100644 index 0000000..8688995 --- /dev/null +++ b/Storage/Storage/LCSMSClient.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LeanCloud.Storage { + /// + /// 短信工具类 + /// + public static class LCSMSClient { + /// + /// 请求短信验证码 + /// + /// + /// + /// + /// + /// + /// + public static async Task RequestSMSCode(string mobile, + string template = null, + string signature = null, + string captchaToken = null, + Dictionary variables = null) { + if (string.IsNullOrEmpty(mobile)) { + throw new ArgumentNullException(nameof(mobile)); + } + + string path = "requestSmsCode"; + Dictionary data = new Dictionary { + { "mobilePhoneNumber", mobile } + }; + if (!string.IsNullOrEmpty(template)) { + data["template"] = template; + } + if (!string.IsNullOrEmpty(signature)) { + data["sign"] = signature; + } + if (!string.IsNullOrEmpty(captchaToken)) { + data["validate_token"] = captchaToken; + } + if (variables != null) { + foreach (KeyValuePair kv in variables) { + data[kv.Key] = kv.Value; + } + } + await LCApplication.HttpClient.Post>(path, data: data); + } + + /// + /// 请求语音验证码 + /// + /// + /// + public static async Task RequestVoiceCode(string mobile) { + string path = "requestSmsCode"; + Dictionary data = new Dictionary { + { "mobilePhoneNumber", mobile }, + { "smsType", "voice" } + }; + await LCApplication.HttpClient.Post>(path, data: data); + } + + /// + /// 验证手机号 + /// + /// + /// + /// + public static async Task VerifyMobilePhone(string mobile, string code) { + string path = $"verifySmsCode/{code}"; + Dictionary data = new Dictionary { + { "mobilePhoneNumber", mobile } + }; + await LCApplication.HttpClient.Post>(path, data: data); + } + } +} From 7bd2cd21d32702b16a0bbcdf04f51d6bc1ff5bf3 Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 12:06:35 +0800 Subject: [PATCH 2/8] * LCObject.cs: * LCAddOperation.cs: * LCRemoveOperation.cs: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ObjectTest.cs: chore: 支持操作未赋值字段 --- Storage/Storage.Test/ObjectTest.cs | 22 +++++++++++++++++++ .../Internal/Operation/LCAddOperation.cs | 9 ++++++-- .../Internal/Operation/LCRemoveOperation.cs | 10 +++++++-- Storage/Storage/LCObject.cs | 10 ++++----- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Storage/Storage.Test/ObjectTest.cs b/Storage/Storage.Test/ObjectTest.cs index 399f2af..1d0f92b 100644 --- a/Storage/Storage.Test/ObjectTest.cs +++ b/Storage/Storage.Test/ObjectTest.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using LeanCloud; using LeanCloud.Storage; +using static NUnit.Framework.TestContext; + namespace Storage.Test { public class ObjectTest { [SetUp] @@ -144,5 +146,25 @@ namespace Storage.Test { TestContext.WriteLine(hello["content"]); Assert.IsNull(hello["content"]); } + + [Test] + public async Task OperateNullProperty() { + LCObject obj = new LCObject("Hello"); + obj.Increment("intValue", 123); + obj.Increment("intValue", 321); + obj.Add("intList", 1); + obj.Add("intList", 2); + obj.Add("intList", 3); + await obj.Save(); + + WriteLine(obj["intValue"]); + Assert.AreEqual(obj["intValue"], 444); + List intList = obj["intList"] as List; + WriteLine(intList.Count); + Assert.AreEqual(intList.Count, 3); + Assert.AreEqual(intList[0], 1); + Assert.AreEqual(intList[1], 2); + Assert.AreEqual(intList[2], 3); + } } } diff --git a/Storage/Storage/Internal/Operation/LCAddOperation.cs b/Storage/Storage/Internal/Operation/LCAddOperation.cs index 0d1ae4d..9591e65 100644 --- a/Storage/Storage/Internal/Operation/LCAddOperation.cs +++ b/Storage/Storage/Internal/Operation/LCAddOperation.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using LeanCloud.Storage.Internal.Codec; namespace LeanCloud.Storage.Internal.Operation { @@ -16,11 +17,15 @@ namespace LeanCloud.Storage.Internal.Operation { return previousOp; } if (previousOp is LCAddOperation addOp) { - valueList.AddRange(addOp.valueList); + List list = new List(addOp.valueList); + list.AddRange(valueList); + valueList = list; return this; } if (previousOp is LCAddUniqueOperation addUniqueOp) { - valueList.AddRange(addUniqueOp.values); + List list = addUniqueOp.values.ToList(); + list.AddRange(valueList); + valueList = list; return this; } throw new ArgumentException("Operation is invalid after previous operation."); diff --git a/Storage/Storage/Internal/Operation/LCRemoveOperation.cs b/Storage/Storage/Internal/Operation/LCRemoveOperation.cs index 4a2f57a..bf75437 100644 --- a/Storage/Storage/Internal/Operation/LCRemoveOperation.cs +++ b/Storage/Storage/Internal/Operation/LCRemoveOperation.cs @@ -17,7 +17,10 @@ namespace LeanCloud.Storage.Internal.Operation { return previousOp; } if (previousOp is LCRemoveOperation removeOp) { - valueList.AddRange(removeOp.valueList); + List list = new List(removeOp.valueList); + list.AddRange(valueList); + valueList = list; + return this; } throw new ArgumentException("Operation is invalid after previous operation."); } @@ -30,7 +33,10 @@ namespace LeanCloud.Storage.Internal.Operation { } public object Apply(object oldValue, string key) { - List list = new List(oldValue as IEnumerable); + List list = new List(); + if (oldValue != null) { + list.AddRange(oldValue as IEnumerable); + } list.RemoveAll(item => valueList.Contains(item)); return list; } diff --git a/Storage/Storage/LCObject.cs b/Storage/Storage/LCObject.cs index c61cd33..b99e7d6 100644 --- a/Storage/Storage/LCObject.cs +++ b/Storage/Storage/LCObject.cs @@ -420,11 +420,6 @@ namespace LeanCloud.Storage { } void ApplyOperation(string key, ILCOperation op) { - if (operationDict.TryGetValue(key, out ILCOperation previousOp)) { - operationDict[key] = op.MergeWithPrevious(previousOp); - } else { - operationDict[key] = op; - } if (op is LCDeleteOperation) { estimatedData.Remove(key); } else { @@ -434,6 +429,11 @@ namespace LeanCloud.Storage { estimatedData[key] = op.Apply(null, key); } } + if (operationDict.TryGetValue(key, out ILCOperation previousOp)) { + operationDict[key] = op.MergeWithPrevious(previousOp); + } else { + operationDict[key] = op; + } } internal void Merge(LCObjectData objectData) { From 9d3d6092f03a6282aafdb9db004ac4c571e553de Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 12:38:44 +0800 Subject: [PATCH 3/8] * ObjectTest.cs: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LCObject.cs: chore: 支持 FetchAll --- Storage/Storage.Test/ObjectTest.cs | 14 +++++++++++ Storage/Storage/LCObject.cs | 38 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/Storage/Storage.Test/ObjectTest.cs b/Storage/Storage.Test/ObjectTest.cs index 1d0f92b..4eacaad 100644 --- a/Storage/Storage.Test/ObjectTest.cs +++ b/Storage/Storage.Test/ObjectTest.cs @@ -166,5 +166,19 @@ namespace Storage.Test { Assert.AreEqual(intList[1], 2); Assert.AreEqual(intList[2], 3); } + + [Test] + public async Task FetchAll() { + List list = new List { + LCObject.CreateWithoutData("Hello", "5e8fe86938ed12000870ae82"), + LCObject.CreateWithoutData("Hello", "5e8fe867158a7a0006be0feb"), + LCObject.CreateWithoutData("Hello", "5e8fe84e5c385800081a1d64"), + }; + await LCObject.FetchAll(list); + Assert.Greater(list.Count, 0); + foreach (LCObject obj in list) { + Assert.NotNull(obj["intList"]); + } + } } } diff --git a/Storage/Storage/LCObject.cs b/Storage/Storage/LCObject.cs index b99e7d6..09ebb33 100644 --- a/Storage/Storage/LCObject.cs +++ b/Storage/Storage/LCObject.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Collections.ObjectModel; using LeanCloud.Storage.Internal.Object; using LeanCloud.Storage.Internal.Operation; using LeanCloud.Storage.Internal.Codec; @@ -412,6 +413,43 @@ namespace LeanCloud.Storage { return this; } + public static async Task> FetchAll(IEnumerable objects) { + if (objects == null || objects.Count() == 0) { + throw new ArgumentNullException(nameof(objects)); + } + + IEnumerable uniqueObjects = objects.Where(item => item.ObjectId != null); + List> requestList = uniqueObjects.Select(item => { + string path = $"/{LCApplication.APIVersion}/classes/{item.ClassName}/{item.ObjectId}"; + return new Dictionary { + { "path", path }, + { "method", "GET" } + }; + }).ToList(); + + Dictionary data = new Dictionary { + { "requests", LCEncoder.Encode(requestList) } + }; + List> results = await LCApplication.HttpClient.Post>>("batch", + data: data); + Dictionary dict = new Dictionary(); + foreach (Dictionary item in results) { + if (item.TryGetValue("error", out object error)) { + int code = (int)error; + string message = item["error"] as string; + throw new LCException(code, message); + } + Dictionary d = item["success"] as Dictionary; + string objectId = d["objectId"] as string; + dict[objectId] = LCObjectData.Decode(d); + } + foreach (LCObject obj in objects) { + LCObjectData objData = dict[obj.ObjectId]; + obj.Merge(objData); + } + return objects; + } + public static void RegisterSubclass(string className, Func constructor) where T : LCObject { Type classType = typeof(T); LCSubclassInfo subclassInfo = new LCSubclassInfo(className, classType, constructor); From 2266887b9a413780a77b0cfa3a7a224d76bef489 Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 12:48:58 +0800 Subject: [PATCH 4/8] =?UTF-8?q?chore:=20=E5=A2=9E=E5=8A=A0=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E5=8C=BF=E5=90=8D=E7=94=A8=E6=88=B7=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Storage/Storage/LCUser.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Storage/Storage/LCUser.cs b/Storage/Storage/LCUser.cs index ae24f13..7af2457 100644 --- a/Storage/Storage/LCUser.cs +++ b/Storage/Storage/LCUser.cs @@ -67,6 +67,12 @@ namespace LeanCloud.Storage { } } + /// + /// 是否是匿名登录 + /// + public bool IsAnonymous => AuthData != null && + AuthData.ContainsKey("anonymous"); + static LCUser currentUser; public static Task GetCurrent() { From 385a2613889adfd65d71d9401c3c5b675e0d2266 Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 14:07:35 +0800 Subject: [PATCH 5/8] * ObjectTest.cs: * LCObjectData.cs: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LCObject.cs: chore: 支持 LCObject 序列化 --- Storage/Storage.Test/ObjectTest.cs | 34 +++++++++++++++++++ .../Storage/Internal/Object/LCObjectData.cs | 14 +++++--- Storage/Storage/LCObject.cs | 22 +++++++++++- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/Storage/Storage.Test/ObjectTest.cs b/Storage/Storage.Test/ObjectTest.cs index 4eacaad..22c1fec 100644 --- a/Storage/Storage.Test/ObjectTest.cs +++ b/Storage/Storage.Test/ObjectTest.cs @@ -180,5 +180,39 @@ namespace Storage.Test { Assert.NotNull(obj["intList"]); } } + + [Test] + public async Task Serialization() { + LCObject obj = new LCObject("Hello"); + obj["intValue"] = 123; + obj["boolValue"] = true; + obj["stringValue"] = "hello, world"; + obj["time"] = DateTime.Now; + obj["intList"] = new List { 1, 1, 2, 3, 5, 8 }; + obj["stringMap"] = new Dictionary { + { "k1", 111 }, + { "k2", true }, + { "k3", "haha" } + }; + LCObject nestedObj = new LCObject("World"); + nestedObj["content"] = "7788"; + obj["objectValue"] = nestedObj; + obj["pointerList"] = new List { + new LCObject("World"), + nestedObj + }; + await obj.Save(); + + string json = obj.ToString(); + WriteLine(json); + LCObject newObj = LCObject.ParseObject(json); + Assert.NotNull(newObj.ObjectId); + Assert.NotNull(newObj.ClassName); + Assert.NotNull(newObj.CreatedAt); + Assert.NotNull(newObj.UpdatedAt); + Assert.AreEqual(newObj["intValue"], 123); + Assert.AreEqual(newObj["boolValue"], true); + Assert.AreEqual(newObj["stringValue"], "hello, world"); + } } } diff --git a/Storage/Storage/Internal/Object/LCObjectData.cs b/Storage/Storage/Internal/Object/LCObjectData.cs index 964758b..d941068 100644 --- a/Storage/Storage/Internal/Object/LCObjectData.cs +++ b/Storage/Storage/Internal/Object/LCObjectData.cs @@ -55,11 +55,17 @@ namespace LeanCloud.Storage.Internal.Object { return null; } Dictionary dict = new Dictionary { - { "className", objectData.ClassName }, - { "objectId", objectData.ObjectId }, - { "createdAt", objectData.CreatedAt }, - { "updatedAt", objectData.UpdatedAt }, + { "className", objectData.ClassName } }; + if (!string.IsNullOrEmpty(objectData.ObjectId)) { + dict["objectId"] = objectData.ObjectId; + } + if (objectData.CreatedAt != null) { + dict["createdAt"] = objectData.CreatedAt; + } + if (objectData.UpdatedAt != null) { + dict["updatedAt"] = objectData.UpdatedAt; + } if (objectData.CustomPropertyDict != null) { foreach (KeyValuePair kv in objectData.CustomPropertyDict) { string key = kv.Key; diff --git a/Storage/Storage/LCObject.cs b/Storage/Storage/LCObject.cs index 09ebb33..84ad22b 100644 --- a/Storage/Storage/LCObject.cs +++ b/Storage/Storage/LCObject.cs @@ -3,7 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Collections.ObjectModel; +using Newtonsoft.Json; using LeanCloud.Storage.Internal.Object; using LeanCloud.Storage.Internal.Operation; using LeanCloud.Storage.Internal.Codec; @@ -457,6 +457,26 @@ namespace LeanCloud.Storage { subclassTypeDict[classType] = subclassInfo; } + /// + /// 序列化为 json 字符串 + /// + /// + public override string ToString() { + return JsonConvert.SerializeObject(LCObjectData.Encode(data)); + } + + /// + /// 反序列化为 LCObject 对象 + /// + /// + /// + public static LCObject ParseObject(string json) { + LCObjectData objectData = LCObjectData.Decode(JsonConvert.DeserializeObject>(json)); + LCObject obj = Create(objectData.ClassName); + obj.Merge(objectData); + return obj; + } + void ApplyOperation(string key, ILCOperation op) { if (op is LCDeleteOperation) { estimatedData.Remove(key); From 8a6aff4199beea426aa4f029a6abeaf8ea2ecd33 Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 14:14:07 +0800 Subject: [PATCH 6/8] test --- Storage/Storage.Test/UserTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Storage/Storage.Test/UserTest.cs b/Storage/Storage.Test/UserTest.cs index 53a0cb5..53c9f7b 100644 --- a/Storage/Storage.Test/UserTest.cs +++ b/Storage/Storage.Test/UserTest.cs @@ -79,6 +79,7 @@ namespace Storage.Test { public async Task LoginAnonymous() { LCUser user = await LCUser.LoginAnonymously(); Assert.NotNull(user.ObjectId); + Assert.IsTrue(user.IsAnonymous); } [Test] From e801478fad80ef3db0ec1edbd02db2c8a58041b6 Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 16:02:26 +0800 Subject: [PATCH 7/8] * RoleTest.cs: * QueryTest.cs: * RelationTest.cs: * SubClassTest.cs: * LCIMConversationQuery.cs: * LCCompositionalCondition.cs: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LCQuery.cs: chore: 支持更丰富的查询 --- .../Conversation/LCIMConversationQuery.cs | 2 +- Storage/Storage.Test/QueryTest.cs | 143 +++++++++++++----- Storage/Storage.Test/RelationTest.cs | 8 +- Storage/Storage.Test/RoleTest.cs | 8 +- Storage/Storage.Test/SubClassTest.cs | 4 +- .../Query/LCCompositionalCondition.cs | 43 +++++- Storage/Storage/LCQuery.cs | 83 +++++++++- 7 files changed, 233 insertions(+), 58 deletions(-) diff --git a/Realtime/Realtime/Conversation/LCIMConversationQuery.cs b/Realtime/Realtime/Conversation/LCIMConversationQuery.cs index 5904ab1..700ba8d 100644 --- a/Realtime/Realtime/Conversation/LCIMConversationQuery.cs +++ b/Realtime/Realtime/Conversation/LCIMConversationQuery.cs @@ -184,7 +184,7 @@ namespace LeanCloud.Realtime { /// /// public LCIMConversationQuery OrderBy(string key) { - Condition.OrderBy(key); + Condition.OrderByAscending(key); return this; } diff --git a/Storage/Storage.Test/QueryTest.cs b/Storage/Storage.Test/QueryTest.cs index 8bd009a..27d3a89 100644 --- a/Storage/Storage.Test/QueryTest.cs +++ b/Storage/Storage.Test/QueryTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using System.Collections.ObjectModel; using System.Collections.Generic; using System.Threading.Tasks; using LeanCloud; @@ -21,7 +22,7 @@ namespace Storage.Test { public async Task BaseQuery() { LCQuery query = new LCQuery("Hello"); query.Limit(2); - List list = await query.Find(); + ReadOnlyCollection list = await query.Find(); TestContext.WriteLine(list.Count); Assert.AreEqual(list.Count, 2); @@ -53,8 +54,8 @@ namespace Storage.Test { [Test] public async Task OrderBy() { LCQuery query = new LCQuery("Account"); - query.OrderBy("balance"); - List results = await query.Find(); + query.OrderByAscending("balance"); + ReadOnlyCollection results = await query.Find(); Assert.LessOrEqual((int)results[0]["balance"], (int)results[1]["balance"]); query = new LCQuery("Account"); @@ -63,6 +64,22 @@ namespace Storage.Test { Assert.GreaterOrEqual((int)results[0]["balance"], (int)results[1]["balance"]); } + [Test] + public async Task AddOrder() { + LCQuery query = new LCQuery("Account"); + query.AddAscendingOrder("balance"); + query.AddDescendingOrder("createdAt"); + ReadOnlyCollection results = await query.Find(); + for (int i = 0; i + 1 < results.Count; i++) { + LCObject a1 = results[i]; + LCObject a2 = results[i + 1]; + int b1 = (int)a1["balance"]; + int b2 = (int)a2["balance"]; + Assert.IsTrue(b1 < b2 || + a1.CreatedAt.CompareTo(a2.CreatedAt) >= 0); + } + } + [Test] public async Task Include() { LCQuery query = new LCQuery("Hello"); @@ -91,7 +108,7 @@ namespace Storage.Test { public async Task GreaterQuery() { LCQuery query = new LCQuery("Account"); query.WhereGreaterThan("balance", 200); - List list = await query.Find(); + ReadOnlyCollection list = await query.Find(); TestContext.WriteLine(list.Count); Assert.Greater(list.Count, 0); } @@ -103,12 +120,12 @@ namespace Storage.Test { LCQuery q2 = new LCQuery("Account"); q2.WhereLessThan("balance", 500); LCQuery query = LCQuery.And(new List> { q1, q2 }); - List results = await query.Find(); + ReadOnlyCollection results = await query.Find(); TestContext.WriteLine(results.Count); - results.ForEach(item => { + foreach (LCObject item in results) { int balance = (int)item["balance"]; Assert.IsTrue(balance >= 100 || balance <= 500); - }); + } } [Test] @@ -118,12 +135,12 @@ namespace Storage.Test { LCQuery q2 = new LCQuery("Account"); q2.WhereGreaterThanOrEqualTo("balance", 500); LCQuery query = LCQuery.Or(new List> { q1, q2 }); - List results = await query.Find(); + ReadOnlyCollection results = await query.Find(); TestContext.WriteLine(results.Count); - results.ForEach(item => { + foreach (LCObject item in results) { int balance = (int)item["balance"]; Assert.IsTrue(balance <= 100 || balance >= 500); - }); + } } [Test] @@ -141,28 +158,28 @@ namespace Storage.Test { public async Task Exist() { LCQuery query = new LCQuery("Account"); query.WhereExists("user"); - List results = await query.Find(); - results.ForEach(item => { + ReadOnlyCollection results = await query.Find(); + foreach (LCObject item in results) { Assert.NotNull(item["user"]); - }); + } query = new LCQuery("Account"); query.WhereDoesNotExist("user"); results = await query.Find(); - results.ForEach(item => { + foreach (LCObject item in results) { Assert.IsNull(item["user"]); - }); + } } [Test] public async Task Select() { LCQuery query = new LCQuery("Account"); query.Select("balance"); - List results = await query.Find(); - results.ForEach(item => { + ReadOnlyCollection results = await query.Find(); + foreach (LCObject item in results) { Assert.NotNull(item["balance"]); Assert.IsNull(item["user"]); - }); + } } [Test] @@ -170,29 +187,29 @@ namespace Storage.Test { // Start LCQuery query = new LCQuery("Hello"); query.WhereStartsWith("stringValue", "hello"); - List results = await query.Find(); - results.ForEach(item => { + ReadOnlyCollection results = await query.Find(); + foreach (LCObject item in results) { string str = item["stringValue"] as string; Assert.IsTrue(str.StartsWith("hello")); - }); + } // End query = new LCQuery("Hello"); query.WhereEndsWith("stringValue", "world"); results = await query.Find(); - results.ForEach(item => { + foreach (LCObject item in results) { string str = item["stringValue"] as string; Assert.IsTrue(str.EndsWith("world")); - }); + } // Contains query = new LCQuery("Hello"); query.WhereContains("stringValue", ","); results = await query.Find(); - results.ForEach(item => { + foreach (LCObject item in results) { string str = item["stringValue"] as string; Assert.IsTrue(str.Contains(',')); - }); + } } [Test] @@ -200,46 +217,60 @@ namespace Storage.Test { // equal LCQuery query = new LCQuery("Book"); query.WhereEqualTo("pages", 3); - Listresults = await query.Find(); - results.ForEach(item => { + ReadOnlyCollectionresults = await query.Find(); + foreach (LCObject item in results) { List pages = item["pages"] as List; Assert.IsTrue(pages.Contains(3)); - }); + } // contain all List containAlls = new List { 1, 2, 3, 4, 5, 6, 7 }; query = new LCQuery("Book"); query.WhereContainsAll("pages", containAlls); results = await query.Find(); - results.ForEach(item => { + foreach (LCObject item in results) { List pages = item["pages"] as List; pages.ForEach(i => { Assert.IsTrue(pages.Contains(i)); }); - }); + } // contain in List containIns = new List { 4, 5, 6 }; query = new LCQuery("Book"); query.WhereContainedIn("pages", containIns); results = await query.Find(); - results.ForEach(item => { + foreach (LCObject item in results) { List pages = item["pages"] as List; bool f = false; containIns.ForEach(i => { f |= pages.Contains(i); }); Assert.IsTrue(f); - }); + } + + // not contain in + List notContainIns = new List { 1, 2, 3 }; + query = new LCQuery("Book"); + query.WhereNotContainedIn("pages", notContainIns); + results = await query.Find(); + foreach (LCObject item in results) { + List pages = item["pages"] as List; + bool f = true; + notContainIns.ForEach(i => { + f &= !pages.Contains(i); + }); + Assert.IsTrue(f); + } // size query = new LCQuery("Book"); query.WhereSizeEqualTo("pages", 7); results = await query.Find(); - results.ForEach(item => { + foreach (LCObject item in results) { List pages = item["pages"] as List; Assert.AreEqual(pages.Count, 7); - }); + } } [Test] @@ -253,7 +284,7 @@ namespace Storage.Test { LCQuery query = new LCQuery("Todo"); LCGeoPoint point = new LCGeoPoint(39.91, 116.41); query.WhereNear("location", point); - List results = await query.Find(); + ReadOnlyCollection results = await query.Find(); Assert.Greater(results.Count, 0); // in box @@ -264,5 +295,47 @@ namespace Storage.Test { results = await query.Find(); Assert.Greater(results.Count, 0); } + + [Test] + public async Task Regex() { + LCQuery query = new LCQuery("Hello"); + query.WhereMatches("stringValue", "^HEllo.*", modifiers: "i"); + ReadOnlyCollection results = await query.Find(); + Assert.Greater(results.Count, 0); + foreach (LCObject item in results) { + string str = item["stringValue"] as string; + Assert.IsTrue(str.StartsWith("hello")); + } + } + + [Test] + public async Task InQuery() { + LCQuery worldQuery = new LCQuery("World"); + worldQuery.WhereEqualTo("content", "7788"); + LCQuery helloQuery = new LCQuery("Hello"); + helloQuery.WhereMatchesQuery("objectValue", worldQuery); + helloQuery.Include("objectValue"); + ReadOnlyCollection hellos = await helloQuery.Find(); + Assert.Greater(hellos.Count, 0); + foreach (LCObject item in hellos) { + LCObject world = item["objectValue"] as LCObject; + Assert.AreEqual(world["content"], "7788"); + } + } + + [Test] + public async Task NotInQuery() { + LCQuery worldQuery = new LCQuery("World"); + worldQuery.WhereEqualTo("content", "7788"); + LCQuery helloQuery = new LCQuery("Hello"); + helloQuery.WhereDoesNotMatchQuery("objectValue", worldQuery); + helloQuery.Include("objectValue"); + ReadOnlyCollection hellos = await helloQuery.Find(); + Assert.Greater(hellos.Count, 0); + foreach (LCObject item in hellos) { + LCObject world = item["objectValue"] as LCObject; + Assert.IsTrue(world == null || world["content"] != "7788"); + } + } } } diff --git a/Storage/Storage.Test/RelationTest.cs b/Storage/Storage.Test/RelationTest.cs index 5ba3813..748bbcf 100644 --- a/Storage/Storage.Test/RelationTest.cs +++ b/Storage/Storage.Test/RelationTest.cs @@ -1,6 +1,6 @@ using NUnit.Framework; using System.Threading.Tasks; -using System.Collections.Generic; +using System.Collections.ObjectModel; using LeanCloud; using LeanCloud.Storage; @@ -56,11 +56,11 @@ namespace Storage.Test { Assert.NotNull(relation.TargetClass); LCQuery relationQuery = relation.Query; - List list = await relationQuery.Find(); - list.ForEach(item => { + ReadOnlyCollection results = await relationQuery.Find(); + foreach (LCObject item in results) { TestContext.WriteLine(item.ObjectId); Assert.NotNull(item.ObjectId); - }); + } } } } diff --git a/Storage/Storage.Test/RoleTest.cs b/Storage/Storage.Test/RoleTest.cs index 51460af..2a6b119 100644 --- a/Storage/Storage.Test/RoleTest.cs +++ b/Storage/Storage.Test/RoleTest.cs @@ -1,6 +1,6 @@ using NUnit.Framework; using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading.Tasks; using LeanCloud; using LeanCloud.Storage; @@ -34,8 +34,8 @@ namespace Storage.Test { [Test] public async Task Query() { LCQuery query = LCRole.GetQuery(); - List list = await query.Find(); - list.ForEach(item => { + ReadOnlyCollection results = await query.Find(); + foreach (LCRole item in results) { TestContext.WriteLine($"{item.ObjectId} : {item.Name}"); Assert.NotNull(item.ObjectId); Assert.NotNull(item.Name); @@ -43,7 +43,7 @@ namespace Storage.Test { TestContext.WriteLine(item.Users.GetType()); Assert.IsTrue(item.Roles is LCRelation); Assert.IsTrue(item.Users is LCRelation); - }); + } } } } diff --git a/Storage/Storage.Test/SubClassTest.cs b/Storage/Storage.Test/SubClassTest.cs index fb599af..8c67142 100644 --- a/Storage/Storage.Test/SubClassTest.cs +++ b/Storage/Storage.Test/SubClassTest.cs @@ -1,6 +1,6 @@ using NUnit.Framework; using System.Threading.Tasks; -using System.Collections.Generic; +using System.Collections.ObjectModel; using LeanCloud; using LeanCloud.Storage; @@ -63,7 +63,7 @@ namespace Storage.Test { LCObject.RegisterSubclass("Account", () => new Account()); LCQuery query = new LCQuery("Account"); query.WhereGreaterThan("balance", 500); - List list = await query.Find(); + ReadOnlyCollection list = await query.Find(); TestContext.WriteLine(list.Count); Assert.Greater(list.Count, 0); foreach (Account account in list) { diff --git a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs index 74a8039..54098e3 100644 --- a/Storage/Storage/Internal/Query/LCCompositionalCondition.cs +++ b/Storage/Storage/Internal/Query/LCCompositionalCondition.cs @@ -44,7 +44,7 @@ namespace LeanCloud.Storage.Internal.Query { } public void WhereNotContainedIn(string key, IEnumerable values) { - AddOperation(key, "nin", values); + AddOperation(key, "$nin", values); } public void WhereContainsAll(string key, IEnumerable values) { @@ -106,6 +106,32 @@ namespace LeanCloud.Storage.Internal.Query { AddOperation(key, "$regex", $".*{subString}.*"); } + public void WhereMatches(string key, string regex, string modifiers) { + Dictionary value = new Dictionary { + { "$regex", regex } + }; + if (modifiers != null) { + value["$options"] = modifiers; + } + Add(new LCEqualCondition(key, value)); + } + + public void WhereMatchesQuery(string key, LCQuery query) where K : LCObject { + Dictionary inQuery = new Dictionary { + { "where", query.condition }, + { "className", query.ClassName } + }; + AddOperation(key, "$inQuery", inQuery); + } + + public void WhereDoesNotMatchQuery(string key, LCQuery query) where K : LCObject { + Dictionary inQuery = new Dictionary { + { "where", query.condition }, + { "className", query.ClassName } + }; + AddOperation(key, "$notInQuery", inQuery); + } + void AddOperation(string key, string op, object value) { LCOperationCondition cond = new LCOperationCondition(key, op, value); Add(cond); @@ -123,15 +149,24 @@ namespace LeanCloud.Storage.Internal.Query { } // 筛选条件 - public void OrderBy(string key) { + public void OrderByAscending(string key) { + orderByList = new List(); + orderByList.Add(key); + } + + public void OrderByDescending(string key) { + OrderByAscending($"-{key}"); + } + + public void AddAscendingOrder(string key) { if (orderByList == null) { orderByList = new List(); } orderByList.Add(key); } - public void OrderByDescending(string key) { - OrderBy($"-{key}"); + public void AddDescendingOrder(string key) { + AddAscendingOrder($"-{key}"); } public void Include(string key) { diff --git a/Storage/Storage/LCQuery.cs b/Storage/Storage/LCQuery.cs index 78ae9df..f86d203 100644 --- a/Storage/Storage/LCQuery.cs +++ b/Storage/Storage/LCQuery.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading.Tasks; using LeanCloud.Storage.Internal.Query; using LeanCloud.Storage.Internal.Object; @@ -16,7 +17,7 @@ namespace LeanCloud.Storage { get; private set; } - LCCompositionalCondition condition; + internal LCCompositionalCondition condition; public LCQuery(string className) { ClassName = className; @@ -29,7 +30,7 @@ namespace LeanCloud.Storage { /// /// /// - public LCQuery WhereEqualTo(string key, object value) { + public LCQuery WhereEqualTo(string key, object value) { condition.WhereEqualTo(key, value); return this; } @@ -56,6 +57,17 @@ namespace LeanCloud.Storage { return this; } + /// + /// 不包含 + /// + /// + /// + /// + public LCQuery WhereNotContainedIn(string key, IEnumerable values) { + condition.WhereNotContainedIn(key, values); + return this; + } + /// /// 包含全部 /// @@ -209,13 +221,48 @@ namespace LeanCloud.Storage { return this; } + /// + /// 正则匹配 + /// + /// + /// + /// + /// + public LCQuery WhereMatches(string key, string regex, string modifiers = null) { + condition.WhereMatches(key, regex, modifiers); + return this; + } + + /// + /// 关系查询 + /// + /// + /// + /// + public LCQuery WhereMatchesQuery(string key, LCQuery query) where K : LCObject { + condition.WhereMatchesQuery(key, query); + return this; + } + + /// + /// 不满足子查询 + /// + /// + /// + /// + /// + public LCQuery WhereDoesNotMatchQuery(string key, LCQuery query) where K : LCObject { + condition.WhereDoesNotMatchQuery(key, query); + return this; + } + /// /// 按 key 升序 /// /// /// - public LCQuery OrderBy(string key) { - condition.OrderBy(key); + public LCQuery OrderByAscending(string key) { + condition.OrderByAscending(key); return this; } @@ -229,6 +276,26 @@ namespace LeanCloud.Storage { return this; } + /// + /// 增加按 key 升序 + /// + /// + /// + public LCQuery AddAscendingOrder(string key) { + condition.AddAscendingOrder(key); + return this; + } + + /// + /// 增加按 key 降序 + /// + /// + /// + public LCQuery AddDescendingOrder(string key) { + condition.AddDescendingOrder(key); + return this; + } + /// /// 拉取 key 的完整对象 /// @@ -284,7 +351,7 @@ namespace LeanCloud.Storage { } WhereEqualTo("objectId", objectId); Limit(1); - List results = await Find(); + ReadOnlyCollection results = await Find(); if (results != null) { if (results.Count == 0) { return null; @@ -294,7 +361,7 @@ namespace LeanCloud.Storage { return null; } - public async Task> Find() { + public async Task> Find() { string path = $"classes/{ClassName}"; Dictionary parameters = BuildParams(); Dictionary response = await LCApplication.HttpClient.Get>(path, queryParams: parameters); @@ -306,12 +373,12 @@ namespace LeanCloud.Storage { obj.Merge(objectData); list.Add(obj); } - return list; + return list.AsReadOnly(); } public async Task First() { Limit(1); - List results = await Find(); + ReadOnlyCollection results = await Find(); if (results != null && results.Count > 0) { return results[0]; } From 6bd34720e9ef53f1ce881314d489c79a258ab1f8 Mon Sep 17 00:00:00 2001 From: oneRain Date: Wed, 6 May 2020 17:33:37 +0800 Subject: [PATCH 8/8] chore --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d2c34a4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/Users/onerainyu/opt/anaconda3/bin/python" +} \ No newline at end of file