com.backnd.database 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/Attributes/ColumnAttribute.cs +46 -0
  2. package/Attributes/ColumnAttribute.cs.meta +11 -0
  3. package/Attributes/PrimaryKeyAttribute.cs +12 -0
  4. package/Attributes/PrimaryKeyAttribute.cs.meta +11 -0
  5. package/Attributes/TableAttribute.cs +44 -0
  6. package/Attributes/TableAttribute.cs.meta +11 -0
  7. package/Attributes.meta +8 -0
  8. package/BACKND.Database.asmdef +14 -0
  9. package/BACKND.Database.asmdef.meta +7 -0
  10. package/BaseModel.cs +22 -0
  11. package/BaseModel.cs.meta +11 -0
  12. package/Client.cs +490 -0
  13. package/Client.cs.meta +11 -0
  14. package/Editor/BACKND.Database.Editor.asmdef +18 -0
  15. package/Editor/BACKND.Database.Editor.asmdef.meta +7 -0
  16. package/Editor/PackageAssetInstaller.cs +251 -0
  17. package/Editor/PackageAssetInstaller.cs.meta +11 -0
  18. package/Editor.meta +8 -0
  19. package/Exceptions/DatabaseException.cs +34 -0
  20. package/Exceptions/DatabaseException.cs.meta +11 -0
  21. package/Exceptions.meta +8 -0
  22. package/Internal/ExpressionAnalyzer.cs +433 -0
  23. package/Internal/ExpressionAnalyzer.cs.meta +11 -0
  24. package/Internal/QueryTypes.cs +61 -0
  25. package/Internal/QueryTypes.cs.meta +11 -0
  26. package/Internal/SqlBuilder.cs +375 -0
  27. package/Internal/SqlBuilder.cs.meta +11 -0
  28. package/Internal/ValueFormatter.cs +103 -0
  29. package/Internal/ValueFormatter.cs.meta +11 -0
  30. package/Internal.meta +8 -0
  31. package/Network/DatabaseExecutor.cs +171 -0
  32. package/Network/DatabaseExecutor.cs.meta +11 -0
  33. package/Network/DatabaseRequest.cs +16 -0
  34. package/Network/DatabaseRequest.cs.meta +11 -0
  35. package/Network/DatabaseResponse.cs +81 -0
  36. package/Network/DatabaseResponse.cs.meta +11 -0
  37. package/Network.meta +8 -0
  38. package/QueryBuilder.cs +1001 -0
  39. package/QueryBuilder.cs.meta +11 -0
  40. package/README.md +24 -0
  41. package/TheBackend~/Plugins/Android/Backend.aar +0 -0
  42. package/TheBackend~/Plugins/Backend.dll +0 -0
  43. package/TheBackend~/Plugins/Editor/TheBackendMultiSettingEditor.dll +0 -0
  44. package/TheBackend~/Plugins/Editor/TheBackendSettingEditor.dll +0 -0
  45. package/TheBackend~/Plugins/LitJSON.dll +0 -0
  46. package/TheBackend~/Plugins/Settings/TheBackendHashKeySettings.dll +0 -0
  47. package/TheBackend~/Plugins/Settings/TheBackendMultiSettings.dll +0 -0
  48. package/TheBackend~/Plugins/Settings/TheBackendSettings.dll +0 -0
  49. package/Tools/BTask.cs +905 -0
  50. package/Tools/BTask.cs.meta +11 -0
  51. package/Tools/DatabaseLoop.cs +110 -0
  52. package/Tools/DatabaseLoop.cs.meta +11 -0
  53. package/Tools/JsonHelper.cs +82 -0
  54. package/Tools/JsonHelper.cs.meta +11 -0
  55. package/Tools.meta +8 -0
  56. package/TransactionBuilder.cs +154 -0
  57. package/TransactionBuilder.cs.meta +11 -0
  58. package/TransactionQueryBuilder.cs +205 -0
  59. package/TransactionQueryBuilder.cs.meta +11 -0
  60. package/TransactionResult.cs +33 -0
  61. package/TransactionResult.cs.meta +11 -0
  62. package/package.json +20 -0
  63. package/package.json.meta +7 -0
@@ -0,0 +1,375 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Linq;
4
+ using System.Text;
5
+
6
+ namespace BACKND.Database.Internal
7
+ {
8
+ /// <summary>
9
+ /// SQL 쿼리 생성 유틸리티
10
+ /// </summary>
11
+ public static class SqlBuilder
12
+ {
13
+ #region WHERE 절 생성
14
+
15
+ /// <summary>
16
+ /// WhereCondition 목록에서 WHERE 절 생성
17
+ /// </summary>
18
+ public static string BuildWhereClause(
19
+ List<WhereCondition> whereConditions,
20
+ bool isOfCurrentUser = false,
21
+ TableType? tableType = null)
22
+ {
23
+ if (whereConditions.Count == 0 && !isOfCurrentUser)
24
+ return null;
25
+
26
+ var sb = new StringBuilder();
27
+
28
+ var hasUserUuidCondition = whereConditions.Any(c => c.ColumnName == "user_uuid");
29
+ if (isOfCurrentUser && tableType == TableType.UserTable && !hasUserUuidCondition)
30
+ {
31
+ sb.Append("user_uuid = @current_user_uuid");
32
+ if (whereConditions.Count > 0)
33
+ sb.Append(" AND ");
34
+ }
35
+
36
+ for (int i = 0; i < whereConditions.Count; i++)
37
+ {
38
+ var condition = whereConditions[i];
39
+
40
+ if (condition.IsGroupStart)
41
+ {
42
+ sb.Append("(");
43
+ }
44
+
45
+ if (i > 0 && !condition.IsGroupStart)
46
+ {
47
+ sb.Append(condition.LogicalOperator == LogicalOperator.And ? " AND " : " OR ");
48
+ }
49
+
50
+ sb.Append(BuildConditionString(condition));
51
+
52
+ if (condition.IsGroupEnd)
53
+ {
54
+ sb.Append(")");
55
+ }
56
+ }
57
+
58
+ return sb.ToString();
59
+ }
60
+
61
+ /// <summary>
62
+ /// 단일 WhereCondition을 SQL 조건 문자열로 변환
63
+ /// </summary>
64
+ public static string BuildConditionString(WhereCondition condition)
65
+ {
66
+ // NULL 비교 시 자동으로 IS NULL / IS NOT NULL로 변환
67
+ if (condition.Value == null)
68
+ {
69
+ return condition.Operator switch
70
+ {
71
+ CompareOperator.Equal => $"{condition.ColumnName} IS NULL",
72
+ CompareOperator.NotEqual => $"{condition.ColumnName} IS NOT NULL",
73
+ CompareOperator.IsNull => $"{condition.ColumnName} IS NULL",
74
+ CompareOperator.IsNotNull => $"{condition.ColumnName} IS NOT NULL",
75
+ _ => throw new NotSupportedException($"NULL comparison with operator {condition.Operator} is not supported. Use 'IS NULL' or 'IS NOT NULL'")
76
+ };
77
+ }
78
+
79
+ return condition.Operator switch
80
+ {
81
+ CompareOperator.Equal => $"{condition.ColumnName} = {ValueFormatter.FormatValueForQuery(condition.Value)}",
82
+ CompareOperator.NotEqual => $"{condition.ColumnName} != {ValueFormatter.FormatValueForQuery(condition.Value)}",
83
+ CompareOperator.GreaterThan => $"{condition.ColumnName} > {ValueFormatter.FormatValueForQuery(condition.Value)}",
84
+ CompareOperator.GreaterThanOrEqual => $"{condition.ColumnName} >= {ValueFormatter.FormatValueForQuery(condition.Value)}",
85
+ CompareOperator.LessThan => $"{condition.ColumnName} < {ValueFormatter.FormatValueForQuery(condition.Value)}",
86
+ CompareOperator.LessThanOrEqual => $"{condition.ColumnName} <= {ValueFormatter.FormatValueForQuery(condition.Value)}",
87
+ CompareOperator.Between => $"{condition.ColumnName} BETWEEN {ValueFormatter.FormatValueForQuery(condition.Value)} AND {ValueFormatter.FormatValueForQuery(condition.SecondValue)}",
88
+ CompareOperator.In => BuildInClause(condition),
89
+ CompareOperator.IsNull => $"{condition.ColumnName} IS NULL",
90
+ CompareOperator.IsNotNull => $"{condition.ColumnName} IS NOT NULL",
91
+ _ => throw new NotSupportedException($"Operator {condition.Operator} is not supported")
92
+ };
93
+ }
94
+
95
+ /// <summary>
96
+ /// IN 절 생성
97
+ /// </summary>
98
+ public static string BuildInClause(WhereCondition condition)
99
+ {
100
+ if (condition.Value is Array array)
101
+ {
102
+ var values = new List<string>();
103
+ foreach (var item in array)
104
+ {
105
+ values.Add(ValueFormatter.FormatValueForQuery(item));
106
+ }
107
+ return $"{condition.ColumnName} IN ({string.Join(", ", values)})";
108
+ }
109
+
110
+ return $"{condition.ColumnName} = {ValueFormatter.FormatValueForQuery(condition.Value)}";
111
+ }
112
+
113
+ #endregion
114
+
115
+ #region ORDER BY 절 생성
116
+
117
+ /// <summary>
118
+ /// OrderByInfo 목록에서 ORDER BY 절 생성
119
+ /// </summary>
120
+ public static string BuildOrderByClause(List<OrderByInfo> orderByList)
121
+ {
122
+ if (orderByList == null || orderByList.Count == 0)
123
+ return null;
124
+
125
+ var clauses = orderByList.Select(o => $"{o.Column} {(o.Descending ? "DESC" : "ASC")}");
126
+ return string.Join(", ", clauses);
127
+ }
128
+
129
+ #endregion
130
+
131
+ #region INSERT 쿼리 생성
132
+
133
+ /// <summary>
134
+ /// INSERT 쿼리 생성
135
+ /// </summary>
136
+ public static string BuildInsertQuery(BaseModel model, out Dictionary<string, object> parameters)
137
+ {
138
+ parameters = new Dictionary<string, object>();
139
+ var columns = new List<string>();
140
+ var values = new List<string>();
141
+
142
+ var tableName = model.GetTableName();
143
+ var columnList = model.GetColumnList().Split(',').Select(c => c.Trim()).ToArray();
144
+ var autoIncrementColumn = model.GetAutoIncrementColumnName();
145
+
146
+ foreach (var columnName in columnList)
147
+ {
148
+ // AutoIncrement 컬럼은 INSERT에서 제외
149
+ if (!string.IsNullOrEmpty(autoIncrementColumn) &&
150
+ autoIncrementColumn.Equals(columnName, StringComparison.OrdinalIgnoreCase))
151
+ continue;
152
+
153
+ var value = model.GetValue(columnName);
154
+ var isNullableType = model.IsPropertyNullableType(columnName);
155
+ var isColumnNullable = model.IsColumnNullable(columnName);
156
+
157
+ // Nullable<T> 타입(int?, DateTime? 등)이고 값이 null인 경우
158
+ if (isNullableType && value == null)
159
+ {
160
+ if (isColumnNullable)
161
+ {
162
+ // NULL 허용 컬럼이면 NULL 값으로 INSERT
163
+ columns.Add(columnName);
164
+ values.Add("NULL");
165
+ continue;
166
+ }
167
+ else
168
+ {
169
+ // NotNull 컬럼인데 값이 null이면 기본값 확인
170
+ var defaultValue = model.GetColumnDefaultValue(columnName);
171
+ if (!string.IsNullOrEmpty(defaultValue))
172
+ continue;
173
+
174
+ throw new InvalidOperationException($"Column '{columnName}' cannot be null");
175
+ }
176
+ }
177
+
178
+ // 참조 타입(string, class 등)이 null인 경우
179
+ if (!isColumnNullable && value == null)
180
+ {
181
+ var defaultValue = model.GetColumnDefaultValue(columnName);
182
+ if (!string.IsNullOrEmpty(defaultValue))
183
+ continue;
184
+
185
+ throw new InvalidOperationException($"Column '{columnName}' cannot be null");
186
+ }
187
+
188
+ columns.Add(columnName);
189
+ values.Add(ValueFormatter.FormatValueForQuery(value));
190
+ }
191
+
192
+ if (columns.Count == 0)
193
+ throw new InvalidOperationException("No columns to insert");
194
+
195
+ return $"INSERT INTO {tableName} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
196
+ }
197
+
198
+ #endregion
199
+
200
+ #region UPDATE 쿼리 생성
201
+
202
+ /// <summary>
203
+ /// 모델 기반 UPDATE 쿼리 생성
204
+ /// </summary>
205
+ public static string BuildUpdateQuery(BaseModel model, string whereClause, out Dictionary<string, object> parameters)
206
+ {
207
+ parameters = new Dictionary<string, object>();
208
+ var setClauses = new List<string>();
209
+
210
+ var tableName = model.GetTableName();
211
+ var columnList = model.GetColumnList().Split(',').Select(c => c.Trim()).ToArray();
212
+ var primaryKeyColumns = model.GetPrimaryKeyColumnNames();
213
+
214
+ foreach (var columnName in columnList)
215
+ {
216
+ // PK 컬럼은 UPDATE SET 절에서 제외
217
+ if (primaryKeyColumns.Any(pk => pk.Equals(columnName, StringComparison.OrdinalIgnoreCase)))
218
+ continue;
219
+
220
+ var value = model.GetValue(columnName);
221
+ var isNullableType = model.IsPropertyNullableType(columnName);
222
+ var isColumnNullable = model.IsColumnNullable(columnName);
223
+
224
+ // Nullable<T> 타입(int?, DateTime? 등)이고 값이 null인 경우
225
+ if (isNullableType && value == null)
226
+ {
227
+ if (isColumnNullable)
228
+ {
229
+ // NULL 허용 컬럼이면 NULL로 UPDATE
230
+ setClauses.Add($"{columnName} = NULL");
231
+ continue;
232
+ }
233
+ else
234
+ {
235
+ throw new InvalidOperationException($"Column '{columnName}' cannot be null");
236
+ }
237
+ }
238
+
239
+ // 참조 타입(string, class 등)이 null인 경우
240
+ if (!isColumnNullable && value == null)
241
+ throw new InvalidOperationException($"Column '{columnName}' cannot be null");
242
+
243
+ setClauses.Add($"{columnName} = {ValueFormatter.FormatValueForQuery(value)}");
244
+ }
245
+
246
+ if (setClauses.Count == 0)
247
+ throw new InvalidOperationException("No columns to update");
248
+
249
+ return $"UPDATE {tableName} SET {string.Join(", ", setClauses)} WHERE {whereClause}";
250
+ }
251
+
252
+ /// <summary>
253
+ /// SetClause 목록 기반 UPDATE 쿼리 생성 (Inc/Dec용)
254
+ /// </summary>
255
+ public static string BuildUpdateQueryFromSetClauses(string tableName, List<SetClause> setClauses, string whereClause)
256
+ {
257
+ if (setClauses == null || setClauses.Count == 0)
258
+ throw new InvalidOperationException("No set clauses specified");
259
+
260
+ var setStatements = setClauses.Select(clause =>
261
+ $"{clause.ColumnName} = {clause.ColumnName} {clause.Operator} {ValueFormatter.FormatValueForQuery(clause.Value)}"
262
+ );
263
+
264
+ return $"UPDATE {tableName} SET {string.Join(", ", setStatements)} WHERE {whereClause}";
265
+ }
266
+
267
+ #endregion
268
+
269
+ #region DELETE 쿼리 생성
270
+
271
+ /// <summary>
272
+ /// DELETE 쿼리 생성
273
+ /// </summary>
274
+ public static string BuildDeleteQuery(string tableName, string whereClause)
275
+ {
276
+ if (string.IsNullOrEmpty(whereClause))
277
+ throw new InvalidOperationException("DELETE requires a WHERE clause");
278
+
279
+ return $"DELETE FROM {tableName} WHERE {whereClause}";
280
+ }
281
+
282
+ #endregion
283
+
284
+ #region SELECT 쿼리 생성
285
+
286
+ /// <summary>
287
+ /// SELECT 쿼리 생성
288
+ /// </summary>
289
+ public static string BuildSelectQuery(
290
+ string tableName,
291
+ string columnList,
292
+ List<WhereCondition> whereConditions,
293
+ List<OrderByInfo> orderByList,
294
+ int? limit = null,
295
+ int? offset = null,
296
+ bool isOfCurrentUser = false,
297
+ TableType? tableType = null)
298
+ {
299
+ var sb = new StringBuilder();
300
+ sb.Append("SELECT ");
301
+ sb.Append(columnList);
302
+ sb.Append($" FROM {tableName}");
303
+
304
+ var whereClause = BuildWhereClause(whereConditions, isOfCurrentUser, tableType);
305
+ if (!string.IsNullOrEmpty(whereClause))
306
+ sb.Append($" WHERE {whereClause}");
307
+
308
+ var orderByClause = BuildOrderByClause(orderByList);
309
+ if (!string.IsNullOrEmpty(orderByClause))
310
+ sb.Append($" ORDER BY {orderByClause}");
311
+
312
+ if (limit.HasValue)
313
+ sb.Append($" LIMIT {limit.Value}");
314
+
315
+ if (offset.HasValue)
316
+ sb.Append($" OFFSET {offset.Value}");
317
+
318
+ return sb.ToString();
319
+ }
320
+
321
+ /// <summary>
322
+ /// COUNT 쿼리 생성
323
+ /// </summary>
324
+ public static string BuildCountQuery(
325
+ string tableName,
326
+ List<WhereCondition> whereConditions,
327
+ bool isOfCurrentUser = false,
328
+ TableType? tableType = null)
329
+ {
330
+ var sb = new StringBuilder();
331
+ sb.Append($"SELECT COUNT(1) FROM {tableName}");
332
+
333
+ var whereClause = BuildWhereClause(whereConditions, isOfCurrentUser, tableType);
334
+ if (!string.IsNullOrEmpty(whereClause))
335
+ sb.Append($" WHERE {whereClause}");
336
+
337
+ return sb.ToString();
338
+ }
339
+
340
+ #endregion
341
+
342
+ #region Primary Key WHERE 절 생성
343
+
344
+ /// <summary>
345
+ /// 모델의 Primary Key로 WHERE 조건 목록 생성
346
+ /// </summary>
347
+ public static List<WhereCondition> BuildPrimaryKeyConditions(BaseModel model)
348
+ {
349
+ var conditions = new List<WhereCondition>();
350
+ var primaryKeyColumns = model.GetPrimaryKeyColumnNames();
351
+
352
+ if (primaryKeyColumns.Length == 0)
353
+ throw new InvalidOperationException("Model has no Primary Key defined");
354
+
355
+ foreach (var primaryKeyColumn in primaryKeyColumns)
356
+ {
357
+ var primaryKeyValue = model.GetValue(primaryKeyColumn);
358
+ if (primaryKeyValue == null)
359
+ throw new InvalidOperationException($"Primary Key '{primaryKeyColumn}' has no value");
360
+
361
+ conditions.Add(new WhereCondition
362
+ {
363
+ ColumnName = primaryKeyColumn,
364
+ Operator = CompareOperator.Equal,
365
+ Value = primaryKeyValue,
366
+ LogicalOperator = LogicalOperator.And
367
+ });
368
+ }
369
+
370
+ return conditions;
371
+ }
372
+
373
+ #endregion
374
+ }
375
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 9bbc5e4e593424e099fe23ce2ab6d69a
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,103 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+
4
+ using Newtonsoft.Json;
5
+
6
+ namespace BACKND.Database.Internal
7
+ {
8
+ /// <summary>
9
+ /// SQL 쿼리용 값 포맷팅 유틸리티
10
+ /// </summary>
11
+ public static class ValueFormatter
12
+ {
13
+ /// <summary>
14
+ /// SQL 함수 및 키워드 목록
15
+ /// </summary>
16
+ private static readonly HashSet<string> SqlFunctionsAndKeywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
17
+ {
18
+ "NOW()", "CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()", "UUID()",
19
+ "CURRENT_DATE", "CURRENT_DATE()", "CURRENT_TIME", "CURRENT_TIME()",
20
+ "NULL"
21
+ };
22
+
23
+ /// <summary>
24
+ /// 값이 SQL 함수 또는 키워드인지 확인
25
+ /// </summary>
26
+ public static bool IsSqlFunctionOrKeyword(string value)
27
+ {
28
+ if (string.IsNullOrWhiteSpace(value)) return false;
29
+ return SqlFunctionsAndKeywords.Contains(value.Trim());
30
+ }
31
+
32
+ /// <summary>
33
+ /// 값을 SQL 쿼리용 문자열로 포맷팅
34
+ /// </summary>
35
+ public static string FormatValueForQuery(object value)
36
+ {
37
+ if (value == null) return "NULL";
38
+
39
+ // 값을 문자열로 변환
40
+ var stringValue = ConvertValueToString(value);
41
+
42
+ // SQL 함수 및 키워드는 따옴표 없이 그대로 반환
43
+ if (IsSqlFunctionOrKeyword(stringValue))
44
+ {
45
+ return stringValue.Trim();
46
+ }
47
+
48
+ // 길이 제한 검사
49
+ const int maxLength = 1024 * 12; // 12KB
50
+ if (stringValue.Length > maxLength)
51
+ {
52
+ throw new ArgumentException($"Value too long. Maximum length is {maxLength} characters, but got {stringValue.Length}");
53
+ }
54
+
55
+ // 함수와 NULL을 제외한 모든 값은 따옴표로 감싸기
56
+ return $"'{stringValue.Replace("'", "''").Replace("\\", "\\\\")}'";
57
+ }
58
+
59
+ /// <summary>
60
+ /// 값을 문자열로 변환
61
+ /// </summary>
62
+ public static string ConvertValueToString(object value)
63
+ {
64
+ if (value == null) return "NULL";
65
+
66
+ var valueType = value.GetType();
67
+
68
+ // DateTime → ISO 8601 포맷
69
+ if (valueType == typeof(DateTime))
70
+ return ((DateTime)value).ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
71
+
72
+ // bool → 소문자
73
+ if (valueType == typeof(bool))
74
+ return ((bool)value) ? "true" : "false";
75
+
76
+ // Enum → 숫자
77
+ if (valueType.IsEnum)
78
+ return ((int)value).ToString();
79
+
80
+ // 복합 객체 → JSON
81
+ if (!valueType.IsPrimitive && valueType != typeof(string) && valueType != typeof(Guid))
82
+ {
83
+ try
84
+ {
85
+ return JsonConvert.SerializeObject(value, new JsonSerializerSettings
86
+ {
87
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
88
+ NullValueHandling = NullValueHandling.Ignore,
89
+ DefaultValueHandling = DefaultValueHandling.Ignore,
90
+ Formatting = Formatting.None
91
+ });
92
+ }
93
+ catch (JsonException ex)
94
+ {
95
+ throw new InvalidOperationException($"Failed to serialize object of type {valueType.Name} to JSON", ex);
96
+ }
97
+ }
98
+
99
+ // 기본: ToString()
100
+ return value.ToString();
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: ef79cfe7f35884c249bf8d5435da580d
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
package/Internal.meta ADDED
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 619842c6cc805448380803b23bf2b370
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
@@ -0,0 +1,171 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Text;
4
+ using System.Threading;
5
+
6
+ using Newtonsoft.Json;
7
+
8
+ using UnityEngine;
9
+ using UnityEngine.Networking;
10
+
11
+ namespace BACKND.Database.Network
12
+ {
13
+ public static class DatabaseExecutor
14
+ {
15
+ //private static readonly string SERVER_URL = "http://localhost:10002";
16
+ //private static readonly string SERVER_URL = "https://api.alpha.thebackend.io";
17
+ //private static readonly string SERVER_URL = "https://api.beta.thebackend.io";
18
+
19
+ private static readonly string SERVER_URL = "https://api.thebackend.io";
20
+ private static readonly string ENDPOINT = "/v1/database/store";
21
+ private static readonly int MAX_RETRIES = 3;
22
+ private static readonly float RETRY_DELAY = 1.0f;
23
+ private static readonly int REQUEST_TIMEOUT = 60;
24
+
25
+ public static async BTask<Response> Execute(DatabaseRequest request, Dictionary<string, string> headers, CancellationToken cancellationToken = default)
26
+ {
27
+ var query = ReplacePlaceholders(request.Query, request.Parameters);
28
+
29
+ var requestBody = new StoreRequest
30
+ {
31
+ query = query
32
+ };
33
+
34
+ var json = JsonConvert.SerializeObject(requestBody);
35
+ var bytes = Encoding.UTF8.GetBytes(json);
36
+
37
+ int retryCount = 0;
38
+ Exception lastException = null;
39
+
40
+ while (retryCount <= MAX_RETRIES)
41
+ {
42
+ using var webRequest = new UnityWebRequest($"{SERVER_URL}{ENDPOINT}", "POST");
43
+ webRequest.uploadHandler = new UploadHandlerRaw(bytes);
44
+ webRequest.downloadHandler = new DownloadHandlerBuffer();
45
+ webRequest.timeout = REQUEST_TIMEOUT;
46
+
47
+ webRequest.SetRequestHeader("Content-Type", "application/json");
48
+ foreach (var header in headers)
49
+ {
50
+ webRequest.SetRequestHeader(header.Key, header.Value);
51
+ }
52
+
53
+ try
54
+ {
55
+ using var timeoutCts = new CancellationTokenSource();
56
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
57
+
58
+ timeoutCts.CancelAfter(REQUEST_TIMEOUT * 1000);
59
+
60
+ var operation = webRequest.SendWebRequestAsBTask(linkedCts.Token);
61
+
62
+ try
63
+ {
64
+ var result = await operation;
65
+
66
+ if (result.result == UnityWebRequest.Result.Success)
67
+ {
68
+ var responseText = result.downloadHandler.text;
69
+ var response = JsonConvert.DeserializeObject<Response>(responseText);
70
+
71
+ return response;
72
+ }
73
+ else
74
+ {
75
+ lastException = new Exception($"Network error: {result.error}");
76
+ retryCount++;
77
+
78
+ if (retryCount <= MAX_RETRIES)
79
+ {
80
+ Debug.LogWarning($"[DatabaseExecutor] Retry {retryCount}/{MAX_RETRIES}: {lastException.Message}");
81
+ await BTask.Delay(RETRY_DELAY * retryCount);
82
+ continue;
83
+ }
84
+ }
85
+ }
86
+ catch (OperationCanceledException)
87
+ {
88
+ webRequest.Abort();
89
+
90
+ if (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
91
+ {
92
+ lastException = new Exception($"Request timeout after {REQUEST_TIMEOUT} seconds");
93
+ retryCount++;
94
+
95
+ if (retryCount <= MAX_RETRIES)
96
+ {
97
+ Debug.LogWarning($"[DatabaseExecutor] Retry {retryCount}/{MAX_RETRIES}: {lastException.Message}");
98
+ await BTask.Delay(RETRY_DELAY * retryCount);
99
+ continue;
100
+ }
101
+ }
102
+ else
103
+ {
104
+ throw;
105
+ }
106
+ }
107
+ }
108
+ catch (OperationCanceledException)
109
+ {
110
+ throw;
111
+ }
112
+ catch (Exception ex)
113
+ {
114
+ lastException = ex;
115
+ retryCount++;
116
+
117
+ if (retryCount <= MAX_RETRIES)
118
+ {
119
+ Debug.LogWarning($"[DatabaseExecutor] Retry {retryCount}/{MAX_RETRIES}: {ex.Message}");
120
+ await BTask.Delay(RETRY_DELAY * retryCount);
121
+ continue;
122
+ }
123
+ }
124
+ }
125
+
126
+ return new Response
127
+ {
128
+ Success = false,
129
+ Error = $"Request failed after {MAX_RETRIES} retries: {lastException?.Message ?? "Unknown error"}"
130
+ };
131
+ }
132
+
133
+ private static string ReplacePlaceholders(string query, Dictionary<string, object> parameters)
134
+ {
135
+ if (parameters == null || parameters.Count == 0)
136
+ return query;
137
+
138
+ var result = query;
139
+ foreach (var param in parameters)
140
+ {
141
+ var value = FormatValue(param.Value);
142
+ result = result.Replace(param.Key, value);
143
+ }
144
+
145
+ return result;
146
+ }
147
+
148
+ private static string FormatValue(object value)
149
+ {
150
+ if (value == null)
151
+ return "NULL";
152
+
153
+ if (value is string str)
154
+ return $"'{str.Replace("'", "''")}'";
155
+
156
+ if (value is bool b)
157
+ return b ? "true" : "false";
158
+
159
+ if (value is DateTime dt)
160
+ return $"'{dt:yyyy-MM-ddTHH:mm:ss.fffZ}'";
161
+
162
+ return value.ToString();
163
+ }
164
+
165
+ private class StoreRequest
166
+ {
167
+ [JsonProperty("query")]
168
+ public string query;
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: a3d4cdef477a7429a9658bad2bb26262
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: