pixiedb
Version:
A tiny in-memory javascript database with indexing and SQL like filters.
348 lines (343 loc) • 13.8 kB
TypeScript
type OrderBy<T> = [key: keyof T, order: 'asc' | 'desc'];
type SortOptions<T> = ((keyof T) | OrderBy<T>)[];
type DBEvent<T> = {
/**
* 'L' alias of load. fires when data loaded
*/
type: 'L';
} | {
/**
* 'C' alias of change. fires on any changes happened like (insert/update/delete) after some time (debounced)
*/
type: 'C';
} | {
/**
* 'I' fires on row/record insert
*/
type: 'I';
payload: [T];
} | {
/**
* 'U' fires on row/record update
*/
type: 'U';
payload: [doc: T, oldDoc: T];
} | {
/**
* 'D' fires on row/record delete
*/
type: 'D';
payload: [T];
} | {
/**
* 'Q' alias of Quit. fires on database clone
*/
type: 'Q';
};
type Pretify<T> = {
[K in keyof T]: T[K];
} & unknown;
type MaybePartial<T> = T | Partial<T>;
interface BaseQueryBuilder<T extends Record<any, any>, SecondaryBuilder> {
/**
* return rows where field value is eqaul to given value
* @example
* pd.select().eq('name', 'Apple').data()
*/
eq: <K extends keyof T>(field: K, value: T[K]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* return rows where field value is in array of values
* @throws if values are not in array
* @example
* pd.select().nin('category', ['Fruit', 'Vegetable']).data()
*/
in: <K extends keyof T>(field: K, values: T[K][]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* return rows where field value is not in array of values
* @throws if values are not in array
* @example
* pd.select().nin('name', ['Apple', 'Banana']).data()
*/
nin: <K extends keyof T>(field: K, values: T[K][]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* field value is between of 2 value, within a given range
* @throws if values are not in array and the array is not length of 2
* @example
* pd.select().between('price', [140, 200]).data()
*/
between: <K extends keyof T>(field: K, values: [T[K], T[K]]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* field value is not between of 2 value, not within a given range
* @throws if values are not in array and the array is not length of 2
* @example
* pd.select().nbetween('price', [120, 400]).data()
*/
nbetween: <K extends keyof T>(field: K, values: [T[K], T[K]]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* return rows where field value is not eqaul to given value
* @example
* pd.select().neq('name', 'Apple').data()
*/
neq: <K extends keyof T>(field: K, value: T[K]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* return rows where field value is greater than to given value
* @example
* pd.select().gt('price', 150).data()
*/
gt: <K extends keyof T>(field: K, value: T[K]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* return rows where field value is greater than and eqaul to given value.
* rows are order by the given field in ascending order
* @example
* pd.select().gte('price', 100).data()
*/
gte: <K extends keyof T>(field: K, value: T[K]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* return rows where field value is lower than to given value
* @example
* pd.select().lt('price', 120).data()
*/
lt: <K extends keyof T>(field: K, value: T[K]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
/**
* return rows where field value is lower than and eqaul to given value.
* rows are order by the given field in ascending order
* @example
* pd.select().lte('price', 120).data()
*/
lte: <K extends keyof T>(field: K, value: T[K]) => SecondaryBuilder & BaseQueryBuilder<T, SecondaryBuilder>;
}
interface QueryBuilder<T extends Record<any, any>, SecondaryBuilder> {
/**
* used to tell the starting point and length to return rows from a result set
* @example
* pd.select().range(2, 10).data()
*/
range: (offset: number, count?: number) => SecondaryBuilder & Omit<QueryBuilder<T, SecondaryBuilder>, 'range'>;
/**
* used to sort data with single or multiple keys
*
* @example
* pd.select().orderBy('name', 'price').data()
* pd.select().orderBy('name', ['price', 'desc']).data()
*/
orderBy: (opt: SortOptions<T>) => SecondaryBuilder & Omit<QueryBuilder<T, SecondaryBuilder>, 'orderBy'>;
}
interface SelectBaseQueryBuilder<T extends Record<any, any>, Fields extends readonly (keyof T)[] | []> {
/**
* returns the first filtered row
* @example
* pd.select().eq('id', 2).single()
* pd.select('name', 'price').in('id', [2, 3, 8]).single()
*/
single: () => Pretify<Fields extends [] ? T : Pretify<Pick<T, Fields[number]>>>;
/**
* returns all the filtered rows
* @example
* pd.select('name', 'price').in('id', [2, 3, 8]).data()
*/
data: () => Pretify<Fields extends [] ? T[] : Pretify<Pick<T, Fields[number]>[]>>;
/**
* count of the filtered rows/docs/records
* @example
* pd.select().between('price', [8, 12]).count()
*/
count: () => number;
}
type SelectQueryBuilder<T extends Record<any, any>, Fields extends readonly (keyof T)[] | []> = Pretify<SelectBaseQueryBuilder<T, Fields> & BaseQueryBuilder<T, SelectBaseQueryBuilder<T, Fields> & QueryBuilder<T, SelectBaseQueryBuilder<T, Fields>>> & QueryBuilder<T, SelectBaseQueryBuilder<T, Fields>>>;
interface WhereBaseQueryBuilder<T extends Record<any, any>> {
/**
* update all the docs with the given doc keys
* @returns all updated docs if passed true as the param
* @example
* pd.where().in('id', [2, 3, 8]).update({ price: 113 })
*/
update: (data: MaybePartial<T>) => T[];
/**
* delete all the filtered docs/rows
* @returns all removed docs
* @example
* pd.where().between('id', [8, 10]).delete()
*/
delete: () => T[];
}
type WhereQueryBuilder<T extends Record<any, any>> = Pretify<WhereBaseQueryBuilder<T> & BaseQueryBuilder<T, WhereBaseQueryBuilder<T>>>;
declare class EvEmit<T> {
/**
* Events listners
*/
private events;
/**
* used to emit change event after a fixed time of last change event
*/
private cEv;
/**
* @param cTime Change emit fire throttle time
*/
constructor(cTime: number);
/**
* set change event listener fire throttle time
* @param cTime Change emit fire throttle time
*/
setCEVTime(cTime: number): void;
/**
* add event listner to the given event
* @param ev event name 'L': 'load', 'C': 'change', 'I': 'insert', 'U': 'insert', 'D': 'delete', 'Q': 'quit/close'.
* @param fn Listner/Handler
* ```
* 'L' alias of load. fires when data loaded
* 'C' alias of change. fires on any changes happened like (insert/update/delete) after some time (debounced)
* 'I' fires on row/record insert
* 'U' fires on row/record update
* 'D' fires on row/record delete
* 'Q' alias of Quit. fires on database clone
* ```
* -----
* @example
* pd.on('I', (ev, doc) => { console.log(ev, doc) }) // 'I', { id: 3, name: 'Orange', price: 20, category: 'Fruit' } // on insert event
* pd.on('C', (evs)=>{}) // on change event with list of changes
* pd.on('L', (ev)=>{ console.log('database loaded') }) // on load event
* pd.on('Q', (ev)=>{}) // on quit event
*/
on<Type extends DBEvent<T>['type'] | DBEvent<T>['type'][], Ev extends Type extends any[] ? Type[number] : Type>(event: Type, fn: ((...args: Extract<DBEvent<T>, {
type: Ev;
}> extends {
payload: infer Payload;
} ? [event: Ev, data: Payload extends any[] ? Payload : [Payload]] : [event: Ev]) => void)): typeof fn;
protected offAll(): void;
/**
* remove event listner from the given event
* @param ev event name 'L': 'load', 'C': 'change', 'I': 'insert', 'U': 'insert', 'D': 'delete', 'Q': 'quit/close'.
* @param fn Listner/Handler
* @example
* pd.off('Q', (ev)=>{})
*/
off<Type extends DBEvent<T>['type'] | DBEvent<T>['type'][], Ev extends Type extends any[] ? Type[number] : Type>(event: Type, fn: ((...args: Extract<DBEvent<T>, {
type: Ev;
}> extends {
payload: infer Payload;
} ? [event: Ev, data: Payload extends any[] ? Payload : [Payload]] : [event: Ev]) => void)): typeof fn;
/**
* emit events
* @param ev event name 'L': 'load', 'C': 'change', 'I': 'insert', 'U': 'update', 'D': 'delete', 'Q': 'quit/close'.
* @param data data payload to emit
* @example
* pd.emit('I', { id: 3, name: 'Orange', price: 20, category: 'Fruit' }) // emit insert event
* pd.emit('C', 'I', 'U', 'D') // emit change event with list of changes
* pd.emit('L') // emit load event
* pd.emit('Q') // emit quit event
*/
emit<Type extends DBEvent<T>['type']>(...args: Extract<DBEvent<T>, {
type: Type;
}> extends {
payload: infer Payload;
} ? [event: Type, ...data: Payload extends any[] ? Payload : [Payload]] : [event: Type]): void;
}
declare function deepClone<T>(o: T): T;
/**
* -----------------------------------------------------
* A tiny in-memory javascript database with indexing and filters.
*
* @author Praveen yadav
* @see https://github.com/pixiedevpraveen/pixiedb/tree/master/README.md
* ---
* @example
* const pd = new PixieDb('id', ['price', 'category'], products) // data is optional can be load after using the load method
* const byId = pd.select().eq('id', 2).single() // { id: 2, name: 'Banana', price: 10, category: 'Fruit' }
* const allByName = pd.select().eq('name', 'Apple').orderBy(['name', ['price', 'desc']]).data() // [{ id: 1, name: 'Apple', price: 10, category: 'Fruit' }, ...]
*/
declare class PixieDb<T extends Record<any, any>, Key extends keyof T> extends EvEmit<T> {
#private;
/**
* primary key or unique key for the database
*/
readonly key: Key;
/**
* method to clone data
* default clone method is JSON stringify and parse
*/
cloneMethod: typeof deepClone;
/**
* @param key primary key or unique key for the database (key should be primitive).
* @param indexes list of index names or for unique indexes { name: 'nameOfIndex', unique: true }
* @param data data list/array to load (with clone)
*/
constructor(key: Key, indexes?: Array<keyof T | {
name: keyof T;
unique: boolean;
}>, data?: T[]);
/**
* used to perform select after complex filtering
* @example
* pd.select(['name', 'price']).eq('category', 'Fruit').data() // [{ name: 'Apple', price: 10 }, { name: 'Banana', price: 10 }, ...]
* pd.select().eq('category', 'Fruit').orderBy('name', ['price', 'desc']).data() // [{ id: 1, name: 'Apple', price: 10, category: 'Fruit' }, { id: 2, name: 'Banana', price: 10, category: 'Fruit' }, ...]
*/
select<Fields extends readonly (keyof T)[]>(fields?: Fields): SelectQueryBuilder<T, Fields>;
/**
* used to perform delete/update with complex filtering
* @example
* pd.where().eq('category', 'Fruit').delete() // delete all fruit products
* pd.where().eq('category', 'Fruit').update({ price: 20 }) // update all fruit products price to 20
*/
where(): WhereQueryBuilder<T>;
/**
* Insert data or array of data into database
* @example
* pd.insert({ id: 3, name: 'Orange', price: 20, category: 'Fruit' }) // insert single record
* pd.insert([{ id: 3, name: 'Orange', price: 20, category: 'Fruit' }, { id: 4, name: 'Mango', price: 30, category: 'Fruit' }]) // insert multiple records
* @param docs data or array of data to insert
* @param upsert set true to update if found
* @param silent true to not emit events default false
* @param clone false to not clone data before insert
*/
insert<Doc extends T | T[]>(docs: Doc, { silent, clone, upsert }?: {
silent?: boolean;
clone?: boolean;
upsert?: boolean;
}): Doc extends any[] ? T[] : (T | undefined);
/**
* @param data data list/array to load (without clone)
* @param clear true to clear existing data
*/
load(data: T[], clear?: boolean): void;
/**
* get doc using key (primary key/unique id)
* @example
* const getByKey = pd.get(2) // { id: 2, name: 'Banana', price: 10, category: 'Fruit' }
*
* @param {T[Key]} key key to get data by
*/
get(key: T[Key]): T | undefined;
/**
* @param clone
* clone the returning rows (don't modify values if pass clone as false)
* default `true`
* @example
* const allData = pd.data() // [{ id: 1, name: 'Apple', price: 10, category: 'Fruit' }, ...]
*/
data(clone?: boolean): T[];
/**
* get all indexes names (including unique and key)
*/
get indexes(): (keyof T)[];
/**
* tell is the key is an unique index name
* @param k name of the index
*/
isUniqIdx(k: keyof T): boolean;
/**
* to close/quit/terminate the database
* @param silent true to not emit events default false
*/
close(silent?: boolean): void;
/**
* return JSON of all data without cloning, key and index names
* @example
* const json = pd.toJSON() // { key: 'id', indexes: ['price', 'category', {name: 'id', unique: true}], data: [{ id: 1, name: 'Apple', price: 10, category: 'Fruit' }, ...] }
*/
toJSON(): {
key: Key;
indexes: (keyof T)[];
data: T[];
};
}
export { PixieDb };