UNPKG

pixiedb

Version:

A tiny in-memory javascript database with indexing and SQL like filters.

348 lines (343 loc) 13.8 kB
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 };