Skip to content

Commit d8b1b42

Browse files
more updates
1 parent cba9967 commit d8b1b42

14 files changed

+412
-52
lines changed

Magic.IndexedDb.sln

+10-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E2eTestWebApp", "E2eTestWeb
1111
EndProject
1212
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E2eTest", "E2eTest\E2eTest.csproj", "{BCC4F889-502A-43FF-B5C6-21C345E3EB2C}"
1313
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicIndexedDbBuildTool", "MagicIndexedDbBuildTool\MagicIndexedDbBuildTool.csproj", "{B4288282-3A6A-48DF-864A-1258FC02314D}"
15+
EndProject
1416
Global
1517
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1618
Debug|Any CPU = Debug|Any CPU
@@ -21,10 +23,10 @@ Global
2123
{A92429BE-E180-4150-BFC7-6C09558978D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
2224
{A92429BE-E180-4150-BFC7-6C09558978D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
2325
{A92429BE-E180-4150-BFC7-6C09558978D0}.Release|Any CPU.Build.0 = Release|Any CPU
24-
{4607CA8C-3AE3-4288-A52A-B3887B254657}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25-
{4607CA8C-3AE3-4288-A52A-B3887B254657}.Debug|Any CPU.Build.0 = Debug|Any CPU
26-
{4607CA8C-3AE3-4288-A52A-B3887B254657}.Release|Any CPU.ActiveCfg = Release|Any CPU
27-
{4607CA8C-3AE3-4288-A52A-B3887B254657}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Release|Any CPU.Build.0 = Release|Any CPU
2830
{D2025B3E-E14F-48E8-8B0E-3BA21762FB92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2931
{D2025B3E-E14F-48E8-8B0E-3BA21762FB92}.Debug|Any CPU.Build.0 = Debug|Any CPU
3032
{D2025B3E-E14F-48E8-8B0E-3BA21762FB92}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -33,10 +35,10 @@ Global
3335
{BCC4F889-502A-43FF-B5C6-21C345E3EB2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
3436
{BCC4F889-502A-43FF-B5C6-21C345E3EB2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
3537
{BCC4F889-502A-43FF-B5C6-21C345E3EB2C}.Release|Any CPU.Build.0 = Release|Any CPU
36-
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37-
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
38-
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
39-
{711DC6B6-9F00-4359-A046-9C5865B527C2}.Release|Any CPU.Build.0 = Release|Any CPU
38+
{B4288282-3A6A-48DF-864A-1258FC02314D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39+
{B4288282-3A6A-48DF-864A-1258FC02314D}.Debug|Any CPU.Build.0 = Debug|Any CPU
40+
{B4288282-3A6A-48DF-864A-1258FC02314D}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{B4288282-3A6A-48DF-864A-1258FC02314D}.Release|Any CPU.Build.0 = Release|Any CPU
4042
EndGlobalSection
4143
GlobalSection(SolutionProperties) = preSolution
4244
HideSolutionNode = FALSE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Magic.IndexedDb.Helpers;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Text.Json;
7+
using System.Threading.Tasks;
8+
9+
namespace Magic.IndexedDb.BuildTools
10+
{
11+
public class MagicIndexDbBuildTool
12+
{
13+
public static void GenerateSchemaJson(string projectPath)
14+
{
15+
// Get all schemas dynamically
16+
var allSchemas = SchemaHelper.GetAllSchemas();
17+
18+
//// Construct paths
19+
//string wwwrootPath = Path.Combine(projectPath, "wwwroot");
20+
//string magicIndexDbPath = Path.Combine(wwwrootPath, "MagicIndexedDb");
21+
22+
//// 🔹 Enforce `/wwwroot/` existence
23+
//if (!Directory.Exists(wwwrootPath))
24+
//{
25+
// Console.WriteLine($"ERROR: The `wwwroot` directory is missing in {projectPath}.");
26+
// Console.WriteLine("Please ensure this folder exists before building.");
27+
// Environment.Exit(1); // FAIL the build
28+
//}
29+
30+
//// Ensure `/wwwroot/MagicIndexedDb/` exists
31+
//if (!Directory.Exists(magicIndexDbPath))
32+
//{
33+
// Directory.CreateDirectory(magicIndexDbPath);
34+
//}
35+
36+
//// Iterate over each schema and save/update individual JSON files
37+
//foreach (var schema in allSchemas)
38+
//{
39+
// string schemaFilePath = Path.Combine(magicIndexDbPath, $"{schema.TableName}.json");
40+
41+
// List<StoreSchema> schemaList = new();
42+
43+
// // If file already exists, read it and merge existing schemas
44+
// if (File.Exists(schemaFilePath))
45+
// {
46+
// try
47+
// {
48+
// string existingJson = File.ReadAllText(schemaFilePath);
49+
// schemaList = JsonSerializer.Deserialize<List<StoreSchema>>(existingJson) ?? new List<StoreSchema>();
50+
// }
51+
// catch (Exception ex)
52+
// {
53+
// Console.WriteLine($"Warning: Failed to read {schemaFilePath}. Overwriting...");
54+
// }
55+
// }
56+
57+
// // Check if this schema already exists in the list
58+
// if (!schemaList.Any(s => s.TableName == schema.TableName))
59+
// {
60+
// schemaList.Add(schema);
61+
// }
62+
63+
// // Write the updated schema list back to JSON
64+
// string json = JsonSerializer.Serialize(schemaList, new JsonSerializerOptions { WriteIndented = true });
65+
// File.WriteAllText(schemaFilePath, json);
66+
67+
// Console.WriteLine($"Updated: {schemaFilePath}");
68+
//}
69+
70+
//Console.WriteLine("Magic IndexedDB schemas generated successfully!");
71+
}
72+
}
73+
}

Magic.IndexedDb/Extensions/ServiceCollectionExtensions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ public static IServiceCollection AddBlazorDB(this IServiceCollection services, l
3737
{
3838
services.AddSingleton<IMagicDbFactory>(sp => new MagicDbFactory(jsMessageSizeBytes));
3939

40+
#if DEBUG
4041
MagicValidator.ValidateTables();
4142

43+
var alltables = SchemaHelper.GetAllSchemas();
44+
var sdf = 3;
45+
#endif
46+
4247
return services;
4348
}
4449
}

Magic.IndexedDb/Helpers/MagicValidator.cs

+71-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public static void ValidateTables()
2727
continue;
2828
}
2929

30+
// Validate that there's a primary key set.
3031
if (primaryKeyProps.Count == 0)
3132
{
3233
errors.AppendLine($"Error: Class '{type.Name}' is marked as a Magic Table but has no primary key property. A primary key is required.");
@@ -36,6 +37,37 @@ public static void ValidateTables()
3637
errors.AppendLine($"Error: Class '{type.Name}' has multiple properties marked with MagicPrimaryKeyAttribute. Only one primary key is allowed.");
3738
}
3839

40+
foreach (var prop in primaryKeyProps)
41+
{
42+
var primaryKeyAttribute = prop.GetCustomAttribute<MagicPrimaryKeyAttribute>();
43+
44+
// Check if auto-increment is true, but type is not numeric
45+
if (primaryKeyAttribute != null && primaryKeyAttribute.AutoIncrement)
46+
{
47+
if (!IsNumericType(prop.PropertyType)
48+
//Future: Allow GUIDs as Primary Keys
49+
// Update the comment to say numeric or guid when uncommented
50+
//prop.PropertyType == typeof(Guid)
51+
)
52+
{
53+
errors.AppendLine($"Error: Primary key '{prop.Name}' in class '{type.Name}' is marked as auto-increment, but its type '{prop.PropertyType.Name}' is not numeric.");
54+
}
55+
}
56+
57+
// Ensure primary key is NOT nullable
58+
if (Nullable.GetUnderlyingType(prop.PropertyType) != null)
59+
{
60+
errors.AppendLine($"Error: Primary key '{prop.Name}' in class '{type.Name}' cannot be nullable.");
61+
}
62+
}
63+
64+
65+
/*
66+
* Prevent any Magic attribute to be
67+
* appended to more than one property.
68+
* This isn't allowed, only 1 magic
69+
* attribute per property!
70+
*/
3971
foreach (var prop in properties)
4072
{
4173
var magicAttributes = new List<Type>
@@ -58,6 +90,10 @@ public static void ValidateTables()
5890
}
5991
}
6092

93+
/*
94+
* Run the GetCompoundKey and GetCompoundIndexes on each class
95+
* to enforce constructor to fire all validations.
96+
*/
6197
var instance = Activator.CreateInstance(type);
6298
var getCompoundKeyMethod = type.GetMethod("GetCompoundKey", BindingFlags.Public | BindingFlags.Instance);
6399
var getCompoundIndexesMethod = type.GetMethod("GetCompoundIndexes", BindingFlags.Public | BindingFlags.Instance);
@@ -72,20 +108,53 @@ public static void ValidateTables()
72108
{
73109
// Call both methods and force any errors to surface
74110
var compoundKey = getCompoundKeyMethod.Invoke(instance, null);
75-
var compoundIndexes = getCompoundIndexesMethod.Invoke(instance, null);
111+
var compoundIndexesObj = getCompoundIndexesMethod.Invoke(instance, null);
112+
113+
// Validate that compound indexes don't have duplicate indexes that overlap
114+
var compoundIndexSets = new HashSet<string>();
115+
116+
// Ensure the returned object is a List<IMagicCompoundIndex>
117+
if (compoundIndexesObj is List<IMagicCompoundIndex> compoundIndexes)
118+
{
119+
foreach (var index in compoundIndexes)
120+
{
121+
string indexKey = string.Join(",", index.ColumnNamesInCompoundIndex.OrderBy(x => x));
122+
123+
if (compoundIndexSets.Contains(indexKey))
124+
{
125+
errors.AppendLine($"Error: Duplicate compound index detected in class '{type.Name}' with the same properties ({indexKey}).");
126+
}
127+
else
128+
{
129+
compoundIndexSets.Add(indexKey);
130+
}
131+
}
132+
}
76133
}
77134
catch (TargetInvocationException tie) when (tie.InnerException != null)
78135
{
79136
// Extract and log the **actual** exception instead of the generic wrapper
80137
errors.AppendLine($"Error: Class '{type.Name}' encountered an issue when calling 'GetCompoundKey()' or 'GetCompoundIndexes()'. Exception: {tie.InnerException.Message}");
81138
}
82-
}
139+
}
83140
}
84141

85142
if (errors.Length > 0)
86143
{
87144
throw new Exception($"Magic Table Validation Errors:\n{errors}");
88145
}
89146
}
147+
148+
private static bool IsNumericType(Type type)
149+
{
150+
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
151+
return underlyingType == typeof(byte) || underlyingType == typeof(sbyte) ||
152+
underlyingType == typeof(short) || underlyingType == typeof(ushort) ||
153+
underlyingType == typeof(int) || underlyingType == typeof(uint) ||
154+
underlyingType == typeof(long) || underlyingType == typeof(ulong) ||
155+
underlyingType == typeof(float) || underlyingType == typeof(double) ||
156+
underlyingType == typeof(decimal);
157+
}
158+
90159
}
91160
}

Magic.IndexedDb/Helpers/SchemaHelper.cs

+33-38
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ namespace Magic.IndexedDb.Helpers
1111
{
1212
public static class SchemaHelper
1313
{
14-
//internal static readonly ConcurrentDictionary<string, MagicTableAttribute?> _schemaCache = new();
1514
//private static readonly ConcurrentDictionary<string, List<StoreSchema>> _databaseSchemasCache = new();
16-
//private static bool _schemasScanned = false;
17-
//private static readonly object _lock = new();
1815
internal static readonly ConcurrentDictionary<Type, IMagicTableBase?> _schemaCache = new();
1916

2017
internal static void EnsureSchemaIsCached(Type type)
@@ -104,48 +101,29 @@ public static bool HasMagicTableInterface(Type type)
104101
/// <summary>
105102
/// Retrieves all schemas for a given database name.
106103
/// </summary>
107-
/* public static List<StoreSchema> GetAllSchemas(string databaseName = null)
104+
public static List<StoreSchema> GetAllSchemas(string? databaseName = null)
108105
{
109-
lock (_lock)
110-
{
111-
// If we've already scanned all schemas, return the cached list.
112-
if (_schemasScanned && _databaseSchemasCache.TryGetValue(databaseName ?? "DefaultedNone", out var cachedSchemas))
113-
return cachedSchemas;
114-
115106
var schemas = new List<StoreSchema>();
116-
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
117107

118-
foreach (var assembly in assemblies)
108+
// 🔹 Retrieve all valid magic tables
109+
var magicTables = GetAllMagicTables();
110+
111+
foreach (var type in magicTables)
119112
{
120-
foreach (var type in assembly.GetTypes())
121-
{
122-
if (!type.IsClass || type.IsAbstract) continue;
123-
124-
// 🚀 **Only process classes that actually have the [MagicTable] attribute**
125-
var schemaAttribute = type.GetCustomAttribute<MagicTableAttribute>();
126-
if (schemaAttribute == null) continue;
127-
128-
// 🚀 Now that we confirmed it's a schema, ensure it's cached
129-
PropertyMappingCache.EnsureTypeIsCached(type);
130-
EnsureSchemaIsCached(type);
131-
132-
// Determine if the schema belongs to the target database
133-
string dbName = !string.IsNullOrWhiteSpace(databaseName) ? databaseName : "DefaultedNone";
134-
if (schemaAttribute.DatabaseName.Equals(dbName, StringComparison.OrdinalIgnoreCase))
135-
{
136-
var schema = GetStoreSchema(type);
137-
schemas.Add(schema);
138-
}
139-
}
140-
}
113+
// Ensure schema is cached
114+
EnsureSchemaIsCached(type);
141115

142-
// Cache results for future calls
143-
_databaseSchemasCache[databaseName ?? "DefaultedNone"] = schemas;
144-
_schemasScanned = true;
116+
// Retrieve cached entry
117+
if (!_schemaCache.TryGetValue(type, out var instance) || instance == null)
118+
continue; // Skip if the type is not a valid IMagicTableBase
119+
120+
// 🚀 Get the store schema
121+
var schema = GetStoreSchema(type);
122+
schemas.Add(schema);
123+
}
145124

146125
return schemas;
147-
}
148-
}*/
126+
}
149127

150128

151129
/// <summary>
@@ -205,8 +183,25 @@ public static StoreSchema GetStoreSchema(Type type)
205183
.Select(prop => PropertyMappingCache.GetJsPropertyName(prop, type))
206184
.ToList();
207185

186+
// Extract Compound Key
187+
var compoundKey = instance.GetCompoundKey();
188+
if (compoundKey != null)
189+
{
190+
schema.ColumnNamesInCompoundKey = compoundKey.ColumnNamesInCompoundKey.ToList();
191+
}
192+
193+
// Extract Compound Indexes
194+
var compoundIndexes = instance.GetCompoundIndexes();
195+
if (compoundIndexes != null)
196+
{
197+
schema.ColumnNamesInCompoundIndex = compoundIndexes
198+
.Select(index => index.ColumnNamesInCompoundIndex.ToList())
199+
.ToList();
200+
}
201+
208202
return schema;
209203
}
210204

205+
211206
}
212207
}

Magic.IndexedDb/Interfaces/IMagicCompoundIndex.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ namespace Magic.IndexedDb
88
{
99
public interface IMagicCompoundIndex
1010
{
11-
string[] ColumnNamesInCompoundKey { get; }
11+
string[] ColumnNamesInCompoundIndex { get; }
1212
}
1313
}

Magic.IndexedDb/Magic.IndexedDb.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@
5050
<Folder Include="Models\Abstract\" />
5151
</ItemGroup>
5252

53+
<Target Name="GenerateMagicSchema" AfterTargets="Build">
54+
<Exec Command="dotnet run --project $(MSBuildThisFileDirectory)/../MagicIndexedDbBuildTool/MagicIndexedDbBuildTool.csproj -- --generate-schema $(MSBuildThisFileDirectory)" />
55+
</Target>
5356
</Project>

Magic.IndexedDb/Models/InternalMagicCompoundIndex.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Magic.IndexedDb
1515

1616
internal class InternalMagicCompoundIndex<T> : IMagicCompoundIndex
1717
{
18-
public string[] ColumnNamesInCompoundKey { get; }
18+
public string[] ColumnNamesInCompoundIndex { get; }
1919

2020
private InternalMagicCompoundIndex(params Expression<Func<T, object>>[] properties)
2121
{
@@ -24,9 +24,15 @@ private InternalMagicCompoundIndex(params Expression<Func<T, object>>[] properti
2424
throw new ArgumentException("Compound keys require at least 2 properties.", nameof(properties));
2525
}
2626

27-
ColumnNamesInCompoundKey = properties
27+
ColumnNamesInCompoundIndex = properties
2828
.Select(GetPropertyName)
2929
.ToArray();
30+
31+
if (ColumnNamesInCompoundIndex.Distinct().Count() != ColumnNamesInCompoundIndex.Length)
32+
{
33+
throw new InvalidOperationException(
34+
$"Duplicate properties detected in the compound index for type '{typeof(T).Name}'. Each property must be unique.");
35+
}
3036
}
3137

3238
internal static IMagicCompoundIndex Create(params Expression<Func<T, object>>[] keySelectors)
@@ -58,6 +64,13 @@ private static string ValidateAndGetPropertyName(MemberExpression memberExpr)
5864
throw new ArgumentException($"Property '{memberExpr.Member.Name}' does not exist on type '{typeof(T).Name}'.");
5965
}
6066

67+
if (memberExpr.Expression is MemberExpression nestedExpr)
68+
{
69+
throw new InvalidOperationException(
70+
$"Cannot compound index nested properties like '{memberExpr.Member.Name}' in compound keys or indexes on type '{typeof(T).Name}'. " +
71+
"Only top-level properties can be indexed.");
72+
}
73+
6174
if (property.GetCustomAttribute<MagicNotMappedAttribute>() != null)
6275
{
6376
throw new InvalidOperationException(

0 commit comments

Comments
 (0)