Skip to content

Commit f65f6d5

Browse files
authored
Refactor: Replace GUID string IDs with uint32 Key + IdentifierValue (#611)
Refactor: Replace GUID string IDs with uint32 Key + IdentifierValue
2 parents 2063efe + 7d8bca9 commit f65f6d5

91 files changed

Lines changed: 2167 additions & 1637 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BareMetalWeb.CLI/Program.cs

Lines changed: 359 additions & 12 deletions
Large diffs are not rendered by default.

BareMetalWeb.Core/wwwroot/static/js/vnext-app.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,19 @@
9393
// ── Lookup cache ──────────────────────────────────────────────────────────
9494
var _lookupCache = {};
9595

96-
function fetchLookupOptions(targetSlug, queryField, queryValue, sortField, sortDir) {
97-
var key = targetSlug + '|' + (queryField || '') + '|' + (queryValue || '') + '|' + (sortField || '') + '|' + (sortDir || '');
96+
function fetchLookupOptions(targetSlug, queryField, queryValue, sortField, sortDir, queryOperator) {
97+
var key = targetSlug + '|' + (queryField || '') + '|' + (queryOperator || '') + '|' + (queryValue || '') + '|' + (sortField || '') + '|' + (sortDir || '');
9898
if (_lookupCache[key]) return Promise.resolve(_lookupCache[key]);
9999

100100
var params = [];
101-
if (queryField && queryValue) params.push('f_' + encodeURIComponent(queryField) + '=' + encodeURIComponent(queryValue));
101+
if (queryField && queryValue) {
102+
params.push('f_' + encodeURIComponent(queryField) + '=' + encodeURIComponent(queryValue));
103+
if (queryOperator && queryOperator !== 'Equals') {
104+
var opMap = { NotEquals: 'ne', Contains: 'contains', StartsWith: 'startswith', GreaterThan: 'gt', LessThan: 'lt' };
105+
var opKey = opMap[queryOperator];
106+
if (opKey) params.push('op_' + encodeURIComponent(queryField) + '=' + encodeURIComponent(opKey));
107+
}
108+
}
102109
if (sortField) { params.push('sort=' + encodeURIComponent(sortField)); }
103110
if (sortDir) { params.push('dir=' + encodeURIComponent(sortDir)); }
104111
params.push('top=500');
@@ -144,6 +151,7 @@
144151
function apiPost(url, body) { return apiFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); }
145152
function apiPut(url, body) { return apiFetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); }
146153
function apiDelete(url) { return apiFetch(url, { method: 'DELETE' }); }
154+
function apiGet(url) { return apiFetch(url); }
147155

148156
// ── UI helpers ────────────────────────────────────────────────────────────
149157
var content = null;
@@ -245,6 +253,17 @@
245253
return cur;
246254
}
247255

256+
function findCellCaseInsensitive(row, fieldName) {
257+
var cell = row.querySelector('td[data-field="' + fieldName + '"]');
258+
if (cell) return cell;
259+
var lower = fieldName.toLowerCase();
260+
var cells = row.querySelectorAll('td[data-field]');
261+
for (var i = 0; i < cells.length; i++) {
262+
if (cells[i].getAttribute('data-field').toLowerCase() === lower) return cells[i];
263+
}
264+
return null;
265+
}
266+
248267
// ── Navigation builder ────────────────────────────────────────────────────
249268
function buildNav(entities) {
250269
var navEl = document.getElementById('vnext-nav-items');
@@ -1961,7 +1980,7 @@
19611980
var sel = form.querySelector('select[data-field="' + sf.name + '"]');
19621981
if (!sel) return;
19631982
var lk = sf.lookup;
1964-
fetchLookupOptions(lk.targetSlug, lk.queryField, lk.queryValue, lk.sortField, lk.sortDirection)
1983+
fetchLookupOptions(lk.targetSlug, lk.queryField, lk.queryValue, lk.sortField, lk.sortDirection, lk.queryOperator)
19651984
.then(function (items) {
19661985
sel.innerHTML = '<option value="">— Select —</option>';
19671986
items.forEach(function (opt) {
@@ -2129,6 +2148,10 @@
21292148
// Load lookup options async
21302149
formFields.forEach(function (f) {
21312150
if (f.type === 'LookupList' && f.lookup && f.lookup.targetSlug) {
2151+
// Self-referencing lookup: inject current entity ID as queryValue to exclude self
2152+
if (f.lookup.queryField && !f.lookup.queryValue && f.lookup.targetSlug === slug && id) {
2153+
f = Object.assign({}, f, { lookup: Object.assign({}, f.lookup, { queryValue: id }) });
2154+
}
21322155
var curVal = item ? (nestedGet(item, f.name)) : null;
21332156
loadLookupSelect(f, curVal);
21342157
}
@@ -2314,7 +2337,7 @@
23142337
var sel = document.querySelector('select#f_' + field.name);
23152338
if (!sel) return;
23162339
var lk = field.lookup;
2317-
fetchLookupOptions(lk.targetSlug, lk.queryField, lk.queryValue, lk.sortField, lk.sortDirection)
2340+
fetchLookupOptions(lk.targetSlug, lk.queryField, lk.queryValue, lk.sortField, lk.sortDirection, lk.queryOperator)
23182341
.then(function (items) {
23192342
if (items.length > LOOKUP_CARDINALITY_THRESHOLD) {
23202343
// Replace select with search-based input

BareMetalWeb.Data.Tests/AuditServiceTests.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public async Task AuditCreateAsync_CreatesAuditEntry()
4848
// Arrange
4949
var testEntity = new TestEntity("testuser")
5050
{
51+
Key = 1,
5152
Name = "Test Entity",
5253
Value = 42
5354
};
@@ -57,10 +58,10 @@ public async Task AuditCreateAsync_CreatesAuditEntry()
5758

5859
// Assert
5960
var auditEntries = await _store.QueryAsync<AuditEntry>();
60-
var entry = auditEntries.FirstOrDefault(e => e.EntityId == testEntity.Id);
61+
var entry = auditEntries.FirstOrDefault(e => e.EntityKey == testEntity.Key);
6162
Assert.NotNull(entry);
6263
Assert.Equal(typeof(TestEntity).Name, entry.EntityType);
63-
Assert.Equal(testEntity.Id, entry.EntityId);
64+
Assert.Equal(testEntity.Key, entry.EntityKey);
6465
Assert.Equal(AuditOperation.Create, entry.Operation);
6566
Assert.Equal("testuser", entry.UserName);
6667
}
@@ -71,13 +72,14 @@ public async Task AuditUpdateAsync_DetectsFieldChanges()
7172
// Arrange
7273
var oldEntity = new TestEntity("testuser")
7374
{
75+
Key = 2,
7476
Name = "Old Name",
7577
Value = 10
7678
};
7779

7880
var newEntity = new TestEntity("testuser")
7981
{
80-
Id = oldEntity.Id,
82+
Key = oldEntity.Key,
8183
CreatedOnUtc = oldEntity.CreatedOnUtc,
8284
CreatedBy = oldEntity.CreatedBy,
8385
Name = "New Name",
@@ -89,7 +91,7 @@ public async Task AuditUpdateAsync_DetectsFieldChanges()
8991

9092
// Assert
9193
var auditEntries = (await _store.QueryAsync<AuditEntry>()).ToList();
92-
var entry = auditEntries.FirstOrDefault(e => e.EntityId == oldEntity.Id && e.Operation == AuditOperation.Update);
94+
var entry = auditEntries.FirstOrDefault(e => e.EntityKey == oldEntity.Key && e.Operation == AuditOperation.Update);
9395
Assert.NotNull(entry);
9496
Assert.Equal(AuditOperation.Update, entry.Operation);
9597
Assert.Equal(2, entry.FieldChanges.Count);
@@ -111,13 +113,14 @@ public async Task AuditUpdateAsync_SkipsWhenNoMeaningfulChanges()
111113
// Arrange
112114
var oldEntity = new TestEntity("testuser")
113115
{
116+
Key = 3,
114117
Name = "Same Name",
115118
Value = 42
116119
};
117120

118121
var newEntity = new TestEntity("testuser")
119122
{
120-
Id = oldEntity.Id,
123+
Key = oldEntity.Key,
121124
CreatedOnUtc = oldEntity.CreatedOnUtc,
122125
CreatedBy = oldEntity.CreatedBy,
123126
Name = "Same Name",
@@ -130,7 +133,7 @@ public async Task AuditUpdateAsync_SkipsWhenNoMeaningfulChanges()
130133

131134
// Assert - no audit entry should be created since only metadata changed
132135
var auditEntries = await _store.QueryAsync<AuditEntry>();
133-
var entry = auditEntries.FirstOrDefault(e => e.EntityId == oldEntity.Id);
136+
var entry = auditEntries.FirstOrDefault(e => e.EntityKey == oldEntity.Key);
134137

135138
Assert.Null(entry); // No audit entry for metadata-only changes
136139
}
@@ -139,14 +142,14 @@ public async Task AuditUpdateAsync_SkipsWhenNoMeaningfulChanges()
139142
public async Task AuditDeleteAsync_CreatesAuditEntry()
140143
{
141144
// Arrange
142-
var entityId = Guid.NewGuid().ToString("N");
145+
var entityKey = (uint)Random.Shared.Next(1, int.MaxValue);
143146

144147
// Act
145-
await _auditService.AuditDeleteAsync<TestEntity>(entityId, "testuser");
148+
await _auditService.AuditDeleteAsync<TestEntity>(entityKey, "testuser");
146149

147150
// Assert
148151
var auditEntries = await _store.QueryAsync<AuditEntry>();
149-
var entry = auditEntries.FirstOrDefault(e => e.EntityId == entityId);
152+
var entry = auditEntries.FirstOrDefault(e => e.EntityKey == entityKey);
150153

151154
Assert.NotNull(entry);
152155
Assert.Equal(typeof(TestEntity).Name, entry.EntityType);
@@ -160,6 +163,7 @@ public async Task AuditRemoteCommandAsync_CreatesAuditEntry()
160163
// Arrange
161164
var testEntity = new TestEntity("testuser")
162165
{
166+
Key = 4,
163167
Name = "Test Entity",
164168
Value = 42
165169
};
@@ -170,7 +174,7 @@ public async Task AuditRemoteCommandAsync_CreatesAuditEntry()
170174

171175
// Assert
172176
var auditEntries = await _store.QueryAsync<AuditEntry>();
173-
var entry = auditEntries.FirstOrDefault(e => e.EntityId == testEntity.Id);
177+
var entry = auditEntries.FirstOrDefault(e => e.EntityKey == testEntity.Key);
174178

175179
Assert.NotNull(entry);
176180
Assert.Equal(AuditOperation.RemoteCommand, entry.Operation);
@@ -182,18 +186,18 @@ public async Task AuditRemoteCommandAsync_CreatesAuditEntry()
182186
public async Task GetEntityHistoryAsync_ReturnsAuditEntriesForEntity()
183187
{
184188
// Arrange
185-
var testEntity = new TestEntity("testuser") { Name = "Test", Value = 1 };
189+
var testEntity = new TestEntity("testuser") { Key = 5, Name = "Test", Value = 1 };
186190

187191
await _auditService.AuditCreateAsync(testEntity, "testuser");
188192

189-
var updatedEntity = new TestEntity("testuser") { Id = testEntity.Id, CreatedOnUtc = testEntity.CreatedOnUtc, CreatedBy = testEntity.CreatedBy, Name = "Updated", Value = 2 };
193+
var updatedEntity = new TestEntity("testuser") { Key = testEntity.Key, CreatedOnUtc = testEntity.CreatedOnUtc, CreatedBy = testEntity.CreatedBy, Name = "Updated", Value = 2 };
190194
await _auditService.AuditUpdateAsync(testEntity, updatedEntity, "testuser");
191195

192-
await _auditService.AuditDeleteAsync<TestEntity>(testEntity.Id, "testuser");
196+
await _auditService.AuditDeleteAsync<TestEntity>(testEntity.Key, "testuser");
193197

194198
// Act
195199
var allEntries = await _store.QueryAsync<AuditEntry>();
196-
var history = allEntries.Where(e => e.EntityId == testEntity.Id && e.EntityType == "TestEntity").ToList();
200+
var history = allEntries.Where(e => e.EntityKey == testEntity.Key && e.EntityType == "TestEntity").ToList();
197201

198202
// Assert
199203
Assert.Equal(3, history.Count);

BareMetalWeb.Data.Tests/BinaryObjectSerializerTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public void Deserialize_WithSchemaHashMismatch_StrictMode_Throws()
128128
// Arrange - simulates what happens when an entity class changes (new field added)
129129
// and old records are read with a schema whose hash no longer matches the current type.
130130
var serializer = new BinaryObjectSerializer();
131-
var original = new Customer { Id = "c1", Name = "Acme Corp", Email = "acme@test.com" };
131+
var original = new Customer { Key = 1, Name = "Acme Corp", Email = "acme@test.com" };
132132
var bytes = serializer.Serialize(original, 1);
133133

134134
var currentSchema = serializer.BuildSchema(typeof(Customer));
@@ -145,7 +145,7 @@ public void Deserialize_WithSchemaHashMismatch_BestEffortMode_ReturnsObject()
145145
// Arrange - simulates schema evolution: entity was modified after records were saved.
146146
// The stored schema's hash differs from the current type's hash, but the data is still readable.
147147
var serializer = new BinaryObjectSerializer();
148-
var original = new Customer { Id = "c1", Name = "Acme Corp", Email = "acme@test.com" };
148+
var original = new Customer { Key = 1, Name = "Acme Corp", Email = "acme@test.com" };
149149
var bytes = serializer.Serialize(original, 1);
150150

151151
var currentSchema = serializer.BuildSchema(typeof(Customer));
@@ -157,7 +157,7 @@ public void Deserialize_WithSchemaHashMismatch_BestEffortMode_ReturnsObject()
157157

158158
// Assert
159159
Assert.NotNull(result);
160-
Assert.Equal("c1", result.Id);
160+
Assert.Equal(1u, result.Key);
161161
Assert.Equal("Acme Corp", result.Name);
162162
Assert.Equal("acme@test.com", result.Email);
163163
}

BareMetalWeb.Data.Tests/BooleanRenderingTests.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void BuildViewRowsHtml_WithBooleanTrue_RendersGreenCheckbox()
4040
// Arrange
4141
var customer = new Customer
4242
{
43-
Id = "cust-1",
43+
Key = 1,
4444
Name = "Test Customer",
4545
Email = "test@example.com",
4646
IsActive = true
@@ -66,7 +66,7 @@ public void BuildViewRowsHtml_WithBooleanFalse_RendersRedCheckbox()
6666
// Arrange
6767
var customer = new Customer
6868
{
69-
Id = "cust-2",
69+
Key = 2,
7070
Name = "Inactive Customer",
7171
Email = "inactive@example.com",
7272
IsActive = false
@@ -94,7 +94,7 @@ public void BuildListRows_WithBooleanTrue_RendersGreenCheckbox()
9494
{
9595
new Product
9696
{
97-
Id = "prod-1",
97+
Key = 1,
9898
Name = "Active Product",
9999
Sku = "SKU-001",
100100
IsActive = true,
@@ -132,7 +132,7 @@ public void BuildListRows_WithBooleanFalse_RendersRedCheckbox()
132132
{
133133
new Product
134134
{
135-
Id = "prod-2",
135+
Key = 2,
136136
Name = "Inactive Product",
137137
Sku = "SKU-002",
138138
IsActive = false,
@@ -168,8 +168,8 @@ public void BuildListRows_WithMixedBooleans_RendersAppropriateCheckboxes()
168168
// Arrange
169169
var customers = new[]
170170
{
171-
new Customer { Id = "c1", Name = "Active", Email = "a@test.com", IsActive = true },
172-
new Customer { Id = "c2", Name = "Inactive", Email = "b@test.com", IsActive = false }
171+
new Customer { Key = 1, Name = "Active", Email = "a@test.com", IsActive = true },
172+
new Customer { Key = 2, Name = "Inactive", Email = "b@test.com", IsActive = false }
173173
};
174174

175175
var meta = DataScaffold.GetEntityByType(typeof(Customer));
@@ -201,24 +201,24 @@ public void BuildListRows_WithMixedBooleans_RendersAppropriateCheckboxes()
201201
/// </summary>
202202
private class InMemoryDataStore : IDataObjectStore
203203
{
204-
private readonly Dictionary<(Type, string), BaseDataObject> _store = new();
204+
private readonly Dictionary<(Type, uint), BaseDataObject> _store = new();
205205

206206
public IReadOnlyList<IDataProvider> Providers => Array.Empty<IDataProvider>();
207207
public void RegisterProvider(IDataProvider provider, bool prepend = false) { }
208208
public void RegisterFallbackProvider(IDataProvider provider) { }
209209
public void ClearProviders() { }
210210

211211
public void Save<T>(T obj) where T : BaseDataObject
212-
=> _store[(typeof(T), obj.Id)] = obj;
212+
=> _store[(typeof(T), obj.Key)] = obj;
213213

214214
public ValueTask SaveAsync<T>(T obj, CancellationToken cancellationToken = default) where T : BaseDataObject
215215
{ Save(obj); return ValueTask.CompletedTask; }
216216

217-
public T? Load<T>(string id) where T : BaseDataObject
218-
=> _store.TryGetValue((typeof(T), id), out var obj) ? obj as T : null;
217+
public T? Load<T>(uint key) where T : BaseDataObject
218+
=> _store.TryGetValue((typeof(T), key), out var obj) ? obj as T : null;
219219

220-
public ValueTask<T?> LoadAsync<T>(string id, CancellationToken cancellationToken = default) where T : BaseDataObject
221-
=> ValueTask.FromResult(Load<T>(id));
220+
public ValueTask<T?> LoadAsync<T>(uint key, CancellationToken cancellationToken = default) where T : BaseDataObject
221+
=> ValueTask.FromResult(Load<T>(key));
222222

223223
public IEnumerable<T> Query<T>(QueryDefinition? query = null) where T : BaseDataObject
224224
=> _store.Values.OfType<T>();
@@ -229,10 +229,10 @@ public ValueTask<IEnumerable<T>> QueryAsync<T>(QueryDefinition? query = null, Ca
229229
public ValueTask<int> CountAsync<T>(QueryDefinition? query = null, CancellationToken cancellationToken = default) where T : BaseDataObject
230230
=> ValueTask.FromResult(Query<T>(query).Count());
231231

232-
public void Delete<T>(string id) where T : BaseDataObject
233-
=> _store.Remove((typeof(T), id));
232+
public void Delete<T>(uint key) where T : BaseDataObject
233+
=> _store.Remove((typeof(T), key));
234234

235-
public ValueTask DeleteAsync<T>(string id, CancellationToken cancellationToken = default) where T : BaseDataObject
236-
{ Delete<T>(id); return ValueTask.CompletedTask; }
235+
public ValueTask DeleteAsync<T>(uint key, CancellationToken cancellationToken = default) where T : BaseDataObject
236+
{ Delete<T>(key); return ValueTask.CompletedTask; }
237237
}
238238
}

0 commit comments

Comments
 (0)