@stexcore/indexed-db
Version:
Manage the IndexedDB Web API, through a simple and easy interface.
599 lines (506 loc) • 27.5 kB
text/typescript
/***************************************************************
Authors : Steven Aray - Sunday, December 12, 2024
Website : https://stexcore.com
Lib: indexed-db
Copyright 2024
***************************************************************/
import { IFieldType } from "./types/field.type";
import { IFieldTypeToValueType } from "./types/field.type.value.type";
import { ISearchOptionsFind } from "./types/search.options";
import { IStructField } from "./types/struct.field";
import { IStructTable } from "./types/struct.table";
import { ITable } from "./types/table";
/**
* Make a structure of tables database to manage indexeddb
* @param struct Structure table
* @returns Structure table
*/
export function createStructTables<T extends {
[key: string]: IStructTable<{
[key: string]: IStructField<IFieldType, boolean | undefined, boolean | undefined>
}>
}>(struct: T) {
return struct;
}
/**
* Database to manage tables based in IndexedDB
*/
export class IndexedDB<T extends { [key: string]: IStructTable<{ [key: string]: IStructField<IFieldType, boolean | undefined, boolean | undefined> }> }> {
/**
* Structure of all tables
*/
public readonly structs: {
/**
* Tables
*/
[key in keyof T]: {
/**
* Name field key with primary key
*/
primaryKey: keyof T[key],
/**
* Fields of table
*/
fields: T[key]
}
};
/**
* Promise async getting database
*/
private gettingDatabase: {
/**
* Resolve promise
* @param db database
*/
resolve: (db: IDBDatabase) => void,
/**
* Reject promise
* @param err Error instance
*/
reject: (err: unknown) => void
}[] = [];
/**
* Database instance memory
*/
private db: IDBDatabase | undefined;
/**
* Inicialize the structure of database
* @param database Database name
* @param table_structs Structure of tables
*/
constructor(
/**
* database name
*/
public readonly db_name: string,
readonly table_structs: T
) {
const struct: any = {};
// Earch all tables to index the structure fields
for(const table_name in table_structs) {
const table = table_structs[table_name];
let fieldPrimary: string | undefined;
// Earch fields to validate initial structure
for(const field_name in table) {
const field = table[field_name];
// Validate if exitst athoner primary key
if(field.primarykey) {
if(fieldPrimary) throw new Error("The table '" + table_name + "' has duplicates primary keys: '" + fieldPrimary + "' and '" + field_name + "'")
fieldPrimary = field_name;
}
}
// validate if this table does'nt has a primary key
if(!fieldPrimary) throw new Error("The table '" + table_name + "' does'nt has a field with primary key")
// set to memory the structure table and her primary key
struct[table_name] = {
primaryKey: fieldPrimary,
fields: table_structs[table_name]
};
}
// Save to local memory the structure and hers primary key
this.structs = struct;
}
/**
* asynchronously obtains the database
* @returns Indexed Database
*/
private GetDataBase() {
return new Promise<IDBDatabase>((_resolve, _reject) => {
try {
// If exist in memory, resolve the database
if(this.db) _resolve(this.db);
// Append to getting
this.gettingDatabase.push({
resolve: _resolve,
reject: _reject
});
if(this.gettingDatabase.length > 1) return;
/**
* Resolve all gettings
* @param db Database
*/
const resolve = (db: IDBDatabase) => {
const temp = this.gettingDatabase;
this.gettingDatabase = [];
temp.forEach((item) => {
item.resolve(db);
});
};
/**
* Reject all gettings
* @param err Error
*/
const reject = (err: unknown) => {
const temp = this.gettingDatabase;
this.gettingDatabase = [];
temp.forEach((item) => {
item.reject(err);
});
};
// Get the last version of database
indexedDB.databases()
.then((databases) => {
// extract version and open the database
const databaseInfo = databases.find(databaseItem => databaseItem.name == this.db_name);
const requestOpen = indexedDB.open(this.db_name, (databaseInfo?.version || 0) + 1);
requestOpen.onsuccess = () => {
this.db = requestOpen.result;
resolve(requestOpen.result);
};
requestOpen.onerror = () => {
reject(requestOpen.error);
};
requestOpen.onupgradeneeded = (ev) => {
// earch the structure to create tables in database
for(const table_name in this.structs) {
const table = this.structs[table_name];
const primaryKey = table.fields[table.primaryKey];
let objectStore: IDBObjectStore;
// if not exists the table, create a new, and set the structure index
if(!requestOpen.result.objectStoreNames.contains(table_name)) {
objectStore = requestOpen.result.createObjectStore(table_name, {
autoIncrement: primaryKey.autoincrement!!,
keyPath: table.primaryKey as string
});
}
else {
objectStore = requestOpen.transaction!.objectStore(table_name);
}
// earch all fields
for(const fieldname in table.fields) {
const field = table.fields[fieldname];
// if not exist the field index, create a new
if(!objectStore.indexNames.contains(fieldname)) {
objectStore.createIndex(fieldname, fieldname, {
unique: !!field.unique
});
}
}
// search fields to check the index structure that should not exist
for(const fieldname of objectStore.indexNames) {
if(!table.fields[fieldname]) {
objectStore.deleteIndex(fieldname);
}
}
}
};
})
.catch((err) => {
reject(err);
});
}
catch(err) {
_reject(err);
}
});
}
/**
* Get a interface to manage a table of indexedDB
* @param name Name of table
* @returns Inteface to manage the table
*/
getTable<T2 extends (keyof T & string)>(name: T2): Promise<ITable<{ [key in keyof T[T2]]: (
T[T2][key]["allow_null"] extends true ?
(IFieldTypeToValueType<T[T2][key]["type"]> | null) :
(IFieldTypeToValueType<T[T2][key]["type"]>)
)}, {
[key in keyof T[T2] as T[T2][key]["autoincrement"] extends true ? never : key]: (
T[T2][key]["allow_null"] extends true ?
(IFieldTypeToValueType<T[T2][key]["type"]> | null) :
(IFieldTypeToValueType<T[T2][key]["type"]>)
)}>> {
type IValue = { [key in keyof T[T2]]: (
T[T2][key]["allow_null"] extends true ?
(IFieldTypeToValueType<T[T2][key]["type"]> | null) :
(IFieldTypeToValueType<T[T2][key]["type"]>)
)};
type IValueInsert = {
[key in keyof T[T2] as T[T2][key]["autoincrement"] extends true ? never : key]: (
T[T2][key]["allow_null"] extends true ?
(IFieldTypeToValueType<T[T2][key]["type"]> | null) :
(IFieldTypeToValueType<T[T2][key]["type"]>)
)};
return new Promise((resolve, reject) => {
try {
// If do'nt exist the table
if(!this.structs[name]) {
throw new Error("Does'nt exist the table '" + String(name) + "'");
}
// get the database
this.GetDataBase()
.then((db) => {
const structure = this.structs[name];
const fields = structure.fields;
/**
* Validate types and remove fields unused
* @param v value incomming
* @returns Value with a correct structure
*/
const format = (v: {[key: string]: any}) => {
const data: any = {};
for(const fieldname in fields) {
const fieldItem = fields[fieldname];
if((v[fieldname] === undefined || v[fieldname] === null) && fieldItem.allow_null) {
data[fieldname] = null;
}
else {
let isValid: boolean = true;
switch(fields[fieldname].type) {
case "string":
isValid = typeof v[fieldname] === "string";
break;
case "array":
isValid = v[fieldname] instanceof Array;
break;
case "object":
isValid = v[fieldname]instanceof Object;
break;
case "boolean":
isValid = typeof v[fieldname] === "boolean";
break;
case "number":
isValid = typeof v[fieldname] === "number";
break;
default:
// TODO: manage objects
}
if(!(fieldItem.autoincrement && fieldItem.primarykey)) {
if(!isValid) throw new Error("Unexpected type in field '" + fieldname + "'. Type required '" + fieldItem.type + "', type received '" + v[fieldname] + "'");
data[fieldname] = v[fieldname];
}
}
}
return data;
}
/**
* Remove undefineds in a object
* @param v value incoming
* @returns value without fields undefineds
*/
const removeUndefineds = (v: {[key: string]: any}) => {
const data: {[key: string]: any} = {};
for(const key in v) {
if(v[key] !== undefined) data[key] = v[key];
}
return data;
}
/**
* Instance table
*/
const table: ITable<IValue, IValueInsert> = {
table_name: String(name),
// find all records
findAll(searchOptions) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(name, "readonly");
const storage = transaction.objectStore(name);
// prepare where object
const where: any = searchOptions?.where || {};
const keys = Object.keys(where).sort((a, b) => (
a > b ? 1 : -b
));
// get all recods
const requestGetAll = storage.getAll();
requestGetAll.onsuccess = () => {
// filter based in where
const filteredRecords = requestGetAll.result.filter(record => {
return keys.every((key) => {
const value = where[key];
const recordValue = record[key];
// If the search value is an array, checks if the record value is in the array
if (Array.isArray(value)) {
return value.includes(recordValue);
}
// If not an array, compare directly
return recordValue === value;
});
});
// trim records based on limit
const slicedRecords = filteredRecords.slice(
searchOptions?.offset || 0,
(searchOptions?.offset || 0) + (searchOptions?.limit ?? filteredRecords.length)
);
// select attributes of result
const records = (searchOptions?.attributes && searchOptions.attributes.length) ? (
slicedRecords.map((record) => {
const item: any = { };
for(const attrItem of searchOptions.attributes!) {
if(attrItem) {
item[attrItem] = record[attrItem];
}
}
return item;
})
) : slicedRecords;
resolve(records);
}
requestGetAll.onerror = () => {
reject(requestGetAll.error);
}
});
},
// find one record
findOne<T2 extends IValue>(searchOptions: ISearchOptionsFind<IValue>) {
return (
table.findAll<T2>({
...searchOptions,
limit: 1,
offset: 0
})
.then((record) => ((record[0] || null) as any))
);
},
// insert recods
insert: (value) => {
return new Promise((resolve, reject) => {
try {
// get objetStorage/table
const objectstorage = db.transaction(name, "readwrite").objectStore(name);
// analize inputs to insert
const dataItem = (value instanceof Array ? value : [value]).map(d => format(d));
Promise.all(dataItem.map(data => {
return new Promise<any>((resolve, reject) => {
try {
// insert recods
const requestAdd = objectstorage.add(data);
requestAdd.onsuccess = () => {
// resolve data with primary key generated
resolve({
...data,
[structure.primaryKey]: requestAdd.result
});
}
requestAdd.onerror = () => {
reject(requestAdd.error);
}
}
catch(err) {
reject(err);
}
});
}))
.then((insertedRegs) => {
// resolve records inserted
resolve(value instanceof Array ? insertedRegs : insertedRegs[0]);
})
}
catch(err) {
reject(err);
}
});
},
delete: (searchOptions) => {
return new Promise((resolve, reject) => {
try {
// search recods
table.findAll(searchOptions)
.then((regs) => {
// get objectStorage/table
const objectstorage = db.transaction(name, "readwrite").objectStore(name);
// earch records
Promise.all(regs.map(item => {
return new Promise<boolean>((resolve, reject) => {
try {
// delete recods
const requestDelete = objectstorage.delete(item[structure.primaryKey] as string | number);
requestDelete.onsuccess = () => {
resolve(true)
};
requestDelete.onerror = () => {
console.error(requestDelete.error);
resolve(false);
};
}
catch(err) {
reject(err);
}
})
}))
.then((result) => {
// resolve counds records deleted
resolve({
n_affected: result.reduce((t, v) => t + Number(v), 0)
});
})
.catch(reject);
})
.catch(reject);
}
catch(err) {
reject(err);
}
});
},
update: (update, searchOptions) => {
return new Promise((resolve, reject) => {
try {
// search records
table.findAll(searchOptions)
.then((regs) => {
// get objectStorage/table
const objectstorage = db.transaction(name, "readwrite").objectStore(name);
// earch records
Promise.all(regs.map(item => {
return new Promise<boolean>((resolve, reject) => {
try {
// update record
const requestDelete = objectstorage.put({
...item,
...removeUndefineds(update),
[structure.primaryKey]: item[structure.primaryKey]
});
requestDelete.onsuccess = () => {
resolve(true)
};
requestDelete.onerror = () => {
console.error(requestDelete.error);
resolve(false);
};
}
catch(err) {
reject(err);
}
})
}))
.then((result) => {
// resolve records resolved
resolve({
n_affected: result.reduce((t, v) => t + Number(v), 0)
});
})
.catch(reject);
})
.catch(reject);
}
catch(err) {
reject(err);
}
});
},
count: (searchOptions) => {
return new Promise((resolve, reject) => {
try {
// search recods
table.findAll(searchOptions)
.then(regs => {
// resolve length
resolve(regs.length);
})
.catch(reject);
}
catch(err) {
reject(err);
}
});
}
}
resolve(table as any);
})
.catch(reject);
}
catch(err) {
reject(err);
}
});
}
}