From b6db166ac9ab87c4cd52690296ad98ec7f170271 Mon Sep 17 00:00:00 2001 From: Scott Yabiku Date: Sun, 26 Sep 2021 12:23:42 -0400 Subject: [PATCH 1/3] Add dynamic databases and store schema --- example/BlazorDB.Example/Pages/Index.razor | 39 ++++++++++ src/BlazorDB/BlazorDbFactory.cs | 51 ++++++++++++- src/BlazorDB/DbStore.cs | 1 + src/BlazorDB/IndexDbManager.cs | 60 ++++++++++++++- src/BlazorDB/IndexedDbFunctions.cs | 3 + src/BlazorDB/wwwroot/blazorDB.js | 85 +++++++++++++++++++++- 6 files changed, 232 insertions(+), 7 deletions(-) diff --git a/example/BlazorDB.Example/Pages/Index.razor b/example/BlazorDB.Example/Pages/Index.razor index 924c170..997539e 100644 --- a/example/BlazorDB.Example/Pages/Index.razor +++ b/example/BlazorDB.Example/Pages/Index.razor @@ -47,6 +47,28 @@ +

Employee (Dynamic)

+ + + + + + + + + + + + + + + + + + + + + @code { IndexedDbManager manager; @@ -76,6 +98,23 @@ manager = await _dbFactory.GetDbManager(dbName); } + protected async void SwitchToEmployee() + { + dbName = "Test3"; + storeName = "Employee"; + manager = await _dbFactory.GetDbManager(dbName); + manager.ActionCompleted += (_, __) => + { + Console.WriteLine(__.Message); + }; + } + + protected async void AddSchema() + { + var schema = new StoreSchema() { Name = storeName, PrimaryKey = "id", PrimaryKeyAuto = true, Indexes = { "name"}, UniqueIndexes = { "guid" } }; + await manager.AddSchema(schema); + } + protected async Task Create() { Console.WriteLine("Create"); diff --git a/src/BlazorDB/BlazorDbFactory.cs b/src/BlazorDB/BlazorDbFactory.cs index c3f6b14..14cad3e 100644 --- a/src/BlazorDB/BlazorDbFactory.cs +++ b/src/BlazorDB/BlazorDbFactory.cs @@ -12,6 +12,7 @@ public class BlazorDbFactory : IBlazorDbFactory readonly IJSRuntime _jsRuntime; readonly IServiceProvider _serviceProvider; readonly IDictionary _dbs = new Dictionary(); + const string InteropPrefix = "window.blazorDB"; public BlazorDbFactory(IServiceProvider serviceProvider, IJSRuntime jSRuntime) { @@ -25,8 +26,12 @@ public async Task GetDbManager(string dbName) await BuildFromServices(); if(_dbs.ContainsKey(dbName)) return _dbs[dbName]; - - return null; + if ((await GetDbNames()).Contains(dbName)) + await BuildExistingDynamic(dbName); + else + await BuildNewDynamic(dbName); + + return _dbs[dbName]; } public Task GetDbManager(DbStore dbStore) @@ -40,11 +45,51 @@ async Task BuildFromServices() foreach(var dbStore in dbStores) { Console.WriteLine($"{dbStore.Name}{dbStore.Version}{dbStore.StoreSchemas.Count}"); - var db = new IndexedDbManager(dbStore, _jsRuntime); + var db = new IndexedDbManager(dbStore, _jsRuntime, _dbs); await db.OpenDb(); _dbs.Add(dbStore.Name, db); } } } + + async Task BuildExistingDynamic(string dbName) + { + var dbStore = await GetDbStoreDynamic(dbName); + var db = new IndexedDbManager(dbStore, _jsRuntime, _dbs); + _dbs.Add(dbStore.Name, db); + } + + async Task BuildNewDynamic(string dbName) + { + var newDbStore = new DbStore() { Name = dbName, StoreSchemas = new(), Version = 0, Dynamic = true }; + var newDb = new IndexedDbManager(newDbStore, _jsRuntime, _dbs); + await newDb.OpenDb(); + newDbStore.Version = 1; + _dbs.Add(newDbStore.Name, newDb); + } + + public async Task GetDbNames() + { + try + { + return await _jsRuntime.InvokeAsync($"{InteropPrefix}.{IndexedDbFunctions.GET_DB_NAMES}"); + } + catch (JSException jse) + { + throw new Exception($"Could not get database names. JavaScript exception: {jse.Message}"); + } + } + + async Task GetDbStoreDynamic(string dbName) + { + try + { + return await _jsRuntime.InvokeAsync($"{InteropPrefix}.{IndexedDbFunctions.GET_DB_STORE_DYNAMIC}", new object[] { dbName }); + } + catch (JSException jse) + { + throw new Exception($"Could not get dynamic database store configuration. JavaScript exception: {jse.Message}"); + } + } } } \ No newline at end of file diff --git a/src/BlazorDB/DbStore.cs b/src/BlazorDB/DbStore.cs index 59a06ca..881bfa7 100644 --- a/src/BlazorDB/DbStore.cs +++ b/src/BlazorDB/DbStore.cs @@ -7,5 +7,6 @@ public class DbStore public string Name { get; set; } public int Version { get; set; } public List StoreSchemas { get; set; } + public bool Dynamic { get; set; } = false; } } \ No newline at end of file diff --git a/src/BlazorDB/IndexDbManager.cs b/src/BlazorDB/IndexDbManager.cs index c7bb10f..28e186c 100644 --- a/src/BlazorDB/IndexDbManager.cs +++ b/src/BlazorDB/IndexDbManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.JSInterop; @@ -12,6 +13,7 @@ public class IndexedDbManager { readonly DbStore _dbStore; readonly IJSRuntime _jsRuntime; + readonly IDictionary _dbs; const string InteropPrefix = "window.blazorDB"; DotNetObjectReference _objReference; IDictionary>> _transactions = new Dictionary>>(); @@ -27,11 +29,12 @@ public class IndexedDbManager /// /// /// - internal IndexedDbManager(DbStore dbStore, IJSRuntime jsRuntime) + internal IndexedDbManager(DbStore dbStore, IJSRuntime jsRuntime, IDictionary dbs) { _objReference = DotNetObjectReference.Create(this); _dbStore = dbStore; _jsRuntime = jsRuntime; + _dbs = dbs; } public List Stores => _dbStore.StoreSchemas; @@ -63,6 +66,10 @@ public async Task DeleteDb(string dbName, Action action = n } var trans = GenerateTransaction(action); await CallJavascriptVoid(IndexedDbFunctions.DELETE_DB, trans, dbName); + if (_dbStore.Dynamic) + { + _dbs.Remove(dbName); + } return trans; } @@ -80,6 +87,57 @@ public async Task DeleteDbAsync(string dbName) } var trans = GenerateTransaction(); await CallJavascriptVoid(IndexedDbFunctions.DELETE_DB, trans.trans, dbName); + if (_dbStore.Dynamic) + { + _dbs.Remove(dbName); + } + return await trans.task; + } + + /// + /// Adds a new store schema to a dynamic database + /// + /// New schema to add + /// + public async Task AddSchema(StoreSchema storeSchema, Action action = null) + { + if (!_dbStore.Dynamic) + { + throw new ArgumentException($"Database is not dynamic. Cannot add schema."); + } + + var trans = GenerateTransaction(action); + await CallJavascriptVoid(IndexedDbFunctions.ADD_SCHEMA, trans, _dbStore, storeSchema); + _dbStore.StoreSchemas.Add(storeSchema); + _dbStore.Version = _dbStore.Version + 1; + + return trans; + } + + /// + /// Adds a new store schema to a dynamic database + /// Waits for response + /// + /// New schema to add + /// + public async Task AddSchemaAsync(StoreSchema storeSchema) + { + if (!_dbStore.Dynamic) + { + throw new ArgumentException($"Database is not dynamic. Cannot add schema."); + } + + var trans = GenerateTransaction(); + try + { + await CallJavascriptVoid(IndexedDbFunctions.ADD_SCHEMA, trans.trans, _dbStore, storeSchema); + _dbStore.StoreSchemas.Add(storeSchema); + _dbStore.Version = _dbStore.Version + 1; + } + catch (JSException jse) + { + RaiseEvent(trans.trans, true, jse.Message); + } return await trans.task; } diff --git a/src/BlazorDB/IndexedDbFunctions.cs b/src/BlazorDB/IndexedDbFunctions.cs index 84bc96a..86ff701 100644 --- a/src/BlazorDB/IndexedDbFunctions.cs +++ b/src/BlazorDB/IndexedDbFunctions.cs @@ -4,6 +4,9 @@ internal struct IndexedDbFunctions { public const string CREATE_DB = "createDb"; public const string DELETE_DB = "deleteDb"; + public const string GET_DB_NAMES = "getDbNames"; + public const string GET_DB_STORE_DYNAMIC = "getDbStoreDynamic"; + public const string ADD_SCHEMA = "addSchema"; public const string ADD_ITEM = "addItem"; public const string BULKADD_ITEM = "bulkAddItem"; public const string PUT_ITEM = "putItem"; diff --git a/src/BlazorDB/wwwroot/blazorDB.js b/src/BlazorDB/wwwroot/blazorDB.js index c382c21..8794072 100644 --- a/src/BlazorDB/wwwroot/blazorDB.js +++ b/src/BlazorDB/wwwroot/blazorDB.js @@ -1,7 +1,81 @@ window.blazorDB = { databases: [], - createDb: function(dotnetReference, transaction, dbStore) { - if(window.blazorDB.databases.find(d => d.name == dbStore.name) !== undefined) + getDbNames: function () { + return new Promise(resolve => Dexie.getDatabaseNames().then(resolve)); + }, + getDbStoreDynamic: function (dbName) { + return new Promise((resolve, reject) => { + Dexie.getDatabaseNames().then(dbNames => { + if (dbNames.find(n => n === dbName) === undefined) { + console.error("Database not found"); + reject("No database"); + } else { + var db = new Dexie(dbName); + + db.open().then(db => { + const dbStore = { + name: db.name, + version: db.verno, + storeSchemas: db.tables.map(table => { + return { + name: table.name, + primaryKey: table.schema.primKey.name, + primaryKeyAuto: table.schema.primKey.auto, + uniqueIndexes: table.schema.indexes + .filter(index => index.unique) + .map(index => index.name), + indexes: table.schema.indexes + .filter(index => !index.unique) + .map(index => index.name) + }; + }), + dynamic: true + }; + // Problem: when we create a dynamic db, we make it version 1 in createDb() below. + // However, if we close/exit our session without adding any store schemas, + // the next time we call db.open() on this dynamic db, the reported verno is 0, + // rather than 1. Yet browser devtools DOES report the correct version. + // Also, calling indexedDB directly will also report correct version. + // (note, for unrelated reasons, Dexie version is the indexedDB version / 10, + // see https://github.com/dfahlander/Dexie.js/issues/59) + // To fix: when we come across verno = 0, change the version number in dbStore to 1. + // dbStore needs correct version so we can increment it when a store schema is added. + // This is not ideal; need to understand why Dexie reports verno 0 instead of 1. + if (db.verno === 0) { + dbStore.version = 1; + indexedDB.databases().then(databases => { + console.warn(`Dexie reports version of ${db.name} is ${db.verno}, but indexedDB reports version is ${databases.find(db1 => db1.name === db.name).version / 10}. BlazorDB will use indexedDB version number.`); + }); + } + window.blazorDB.databases.push({ + name: dbName, + db: db + }); + resolve(dbStore); + }) + } + }) + }); + }, + addSchema: function (dotnetReference, transaction, dbStore, newStoreSchema) { + if (!dbStore.dynamic || window.blazorDB.databases.find(d => d.name === dbStore.name) === undefined) { + console.error(`Blazor.IndexedDB.Framework - Dynamic database ${dbStore.name} not found`); + dotnetReference.invokeMethodAsync('BlazorDBCallback', transaction, true, `Dynamic database ${dbStore.name} could not be found`); + return; + } + var db = window.blazorDB.databases.find(d => d.name === dbStore.name).db + if (db.tables.map(table => table.name).find(name => name === newStoreSchema.name) !== undefined) { + console.error(`Blazor.IndexedDB.Framework - schema for store ${newStoreSchema.name} already exists`); + dotnetReference.invokeMethodAsync('BlazorDBCallback', transaction, true, 'Schema already exists'); + return; + } + db.close(); + + dbStore.storeSchemas.push(newStoreSchema); + this.createDb(dotnetReference, transaction, dbStore); + }, + createDb: function (dotnetReference, transaction, dbStore) { + if(!dbStore.dynamic && window.blazorDB.databases.find(d => d.name === dbStore.name) !== undefined) console.warn("Blazor.IndexedDB.Framework - Database already exists"); var db = new Dexie(dbStore.name); @@ -32,6 +106,9 @@ window.blazorDB = { stores[schema.name] = def; } + if (dbStore.dynamic) { + dbStore.version = dbStore.version + 1; + } db.version(dbStore.version).stores(stores); if(window.blazorDB.databases.find(d => d.name == dbStore.name) !== undefined) { window.blazorDB.databases.find(d => d.name == dbStore.name).db = db; @@ -41,7 +118,9 @@ window.blazorDB = { db: db }); } - db.open().then(_ => { + db.open().then(db => { + if (dbStore.dynamic) + window.blazorDB.databases.find(d => d.name === dbStore.name).db = db; dotnetReference.invokeMethodAsync('BlazorDBCallback', transaction, false, 'Database opened'); }).catch(e => { console.error(e); From 9fd0a13e5fa90387b87133de12d9c57bb92ca7d2 Mon Sep 17 00:00:00 2001 From: Scott Yabiku Date: Sun, 26 Sep 2021 12:23:56 -0400 Subject: [PATCH 2/3] Update documentation --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 72265c0..36191fb 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ An easy, fast way to use IndexedDB in a Blazor application. Name = "Person", PrimaryKey = "id", PrimaryKeyAuto = true, - UniqueIndexes = new List { "name" } + UniqueIndexes = new List { "guid" }, + Indexes = new List { "name" } } }; }); @@ -40,20 +41,25 @@ An easy, fast way to use IndexedDB in a Blazor application. await manager.AddRecord(new StoreRecord() { StoreName = storeName, - Record = new { Name = "MyName", Age = 20 } + Record = new { Name = "MyName", Age = 20, Guid = System.Guid.NewGuid() } }); ``` ### How it works -You defined your databases and store schemes in the `ServicesCollection`. We add a helper called `AddBlazorDB` where you can build ` DbStore`. From there, we ensure that an instance of `IBlazorDbFactory` is available in your service provider and will automatically add all `DbStores` and `IndexedDbManagers` based on what is defined with `AddBlazorDb`. This allows you to have multiple databases if needed. +You defined your databases and store schemes in the `ServicesCollection`. We add a helper called `AddBlazorDB` where you can build ` DbStore`. From there, we ensure that an instance of `IBlazorDbFactory` is available in your service provider and will automatically add all `DbStores` and `IndexedDbManagers` based on what is defined with `AddBlazorDb`. This allows you to have multiple databases if needed. The databases and store schemes in the `ServicesCollection` are "static" because they cannot be created outside of the `WebAssemblyHostBuilder`. To access any database/store, all you need to do is inject the `IBlazorDbFactory` in your page and call `GetDbManager(string dbName)`. We will pull your `IndexedDbManager` from the factory and make sure it's created and ready. +If you call `GetDbManager(string dbName)` with a database name that does not exist within the `ServicesCollection`, a "dynamic" database will be created. The database will not have any schemas until you call the manager's `AddSchemaAsync(StoreSchema storeSchema)` method to define schemas. + Most calls will either be true `Async` or let you pass an `Action` in. If it lets you pass in an `Action`, it is optional. This `Action` will get called when the method is complete. This way, it isn't blocking on `WASMs` single thread. All of these calls return a `Guid` which is the "transaction" identifier to IndexeDB (it's not a true database transaction, just the call between C# and javascript that gets tracked). If the call is flagged as `Async`, we will wait for the JS callback that it is complete and then return the data. The library takes care of this connection between javascript and C# for you. ### Available Calls + - `Task GetDbNames()` - Get an array of all existing databases, both static and dynamic + - `Task AddSchema(StoreSchema storeSchema, Action action)` - Add a new store schema to a dynamic database, with an optional callback when complete + - `Task AddSchemaAsync(StoreSchema storeSchema)` - Add a new store schema to a dynamic database, and wait for it to complete - `Task OpenDb(Action action)` - Open the IndexedDb and make sure it is created, with an optional callback when complete - `Task DeleteDb(string dbName, Action action)` - Delete the database, with an optional callback when complete - `Task DeleteDbAsync(string dbName)` - Delete the database and wait for it to complete From 88e89ab130e1b2e04677b8c7a789934ca6177865 Mon Sep 17 00:00:00 2001 From: Scott Yabiku Date: Sun, 26 Sep 2021 12:43:01 -0400 Subject: [PATCH 3/3] Don't use C# 9 new() expression --- src/BlazorDB/BlazorDbFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BlazorDB/BlazorDbFactory.cs b/src/BlazorDB/BlazorDbFactory.cs index 14cad3e..93910e2 100644 --- a/src/BlazorDB/BlazorDbFactory.cs +++ b/src/BlazorDB/BlazorDbFactory.cs @@ -61,7 +61,7 @@ async Task BuildExistingDynamic(string dbName) async Task BuildNewDynamic(string dbName) { - var newDbStore = new DbStore() { Name = dbName, StoreSchemas = new(), Version = 0, Dynamic = true }; + var newDbStore = new DbStore() { Name = dbName, StoreSchemas = new List(), Version = 0, Dynamic = true }; var newDb = new IndexedDbManager(newDbStore, _jsRuntime, _dbs); await newDb.OpenDb(); newDbStore.Version = 1;