From e7b77ef14cfc05c14394c8530c2f99a5c8ab79f7 Mon Sep 17 00:00:00 2001 From: Kirlovon Date: Tue, 4 Aug 2020 00:36:31 +0300 Subject: [PATCH] Stable prototype --- lib/cursor.ts | 4 +- lib/declarations.ts | 85 ++++-------- lib/error.ts | 4 +- lib/main.ts | 122 ++++++++++------- lib/operators.ts | 80 +++++------ lib/search.ts | 30 +++-- lib/storage.ts | 1 + lib/types.ts | 66 ++++----- lib/utils.ts | 90 +++---------- mod.ts | 3 +- tests/benchmark.ts | 141 -------------------- tests/benchmarks/findOne_benchmark.ts | 49 +++++++ tests/benchmarks/insertOne_benchmark.ts | 40 ++++++ tests/benchmarks/updateOne_benchmark.ts | 51 +++++++ tests/benchmarks/utils.ts | 16 +++ tests/unit/{types.test.ts => types_test.ts} | 0 tests/unit/{utils.test.ts => utils_test.ts} | 90 ++----------- 17 files changed, 386 insertions(+), 486 deletions(-) delete mode 100644 tests/benchmark.ts create mode 100644 tests/benchmarks/findOne_benchmark.ts create mode 100644 tests/benchmarks/insertOne_benchmark.ts create mode 100644 tests/benchmarks/updateOne_benchmark.ts create mode 100644 tests/benchmarks/utils.ts rename tests/unit/{types.test.ts => types_test.ts} (100%) rename tests/unit/{utils.test.ts => utils_test.ts} (70%) diff --git a/lib/cursor.ts b/lib/cursor.ts index 8ada5d7..9156c5d 100644 --- a/lib/cursor.ts +++ b/lib/cursor.ts @@ -4,7 +4,7 @@ import { SearchQuery, CursorMethod, DatabaseConfig, Acceptable } from './declara // TODO export class Cursor> { /** Main search query. */ - private query: SearchQuery = {}; + private query: SearchQuery; /** Database configuration. */ private config: DatabaseConfig; @@ -68,7 +68,7 @@ export class Cursor> { for (let i = 0; i < methods.length; i++) { const method: CursorMethod = methods[i]; - const type = method.type; + const type: string = method.type; if (type === 'reverse') { found.reverse(); diff --git a/lib/declarations.ts b/lib/declarations.ts index d9c76ec..80e8aba 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -1,15 +1,9 @@ -/** - * Database initialization config - */ +/** Database initialization config */ export interface DatabaseConfig { - /** - * Path to the database file. - */ + /** Path to the database file. */ filePath?: string; - /** - * Save data in easy-to-read format. - */ + /** Save data in easy-to-read format. */ pretty: boolean; /** @@ -32,9 +26,7 @@ export interface DatabaseConfig { schemaValidator?: SchemaValidator; } -/** - * Database file structure. - */ +/** Database file structure. */ export interface DatabaseFile { /** Timestamp of the last data writing. */ timestamp: number; @@ -43,68 +35,49 @@ export interface DatabaseFile { documents: Document[]; } -/** - * Any document-like object. - */ +/** Any document-like object. */ export interface Document { [key: string]: DocumentValue; } -/** - * Any object without specified structure. - */ +/** Any object without specified structure. */ export interface UnknownObject { [key: string]: any; } -/** - * - */ +/** Checking the object for suitability for storage. */ export type Acceptable = { [K in keyof T]: T[K] & DocumentValue }; -/** - * Search query. - */ -export type SearchQuery = { [K in keyof T]?: T[K] | SearchFunction | RegExp } & { [key: string]: DocumentValue | SearchFunction | RegExp }; +/** Search query. */ +export type SearchQuery = Document> = { [K in keyof T]?: SearchQueryValue } | SearchFunction | undefined; -/** - * Update query. - */ -export type UpdateQuery = ({ [K in keyof T]?: T[K] & DocumentValue } & { [key: string]: DocumentValue }) | ((document: T) => void); +/** Update query. */ +export type UpdateQuery = Document> = { [K in keyof T]?: UpdateQueryValue } | UpdateFunction | undefined; -/** - * Sorting function. - */ -export type SortFunction = (a: Readonly, b: Readonly) => number; +/** Supported primitives. */ +export type DocumentPrimitive = string | number | boolean | null; -/** - * Search function for search queries. - */ -export type SearchFunction = (DocumentValue: DocumentValue) => boolean; +/** Supported documents values. */ +export type DocumentValue = DocumentPrimitive | DocumentPrimitive[] | Document | Document[]; -/** - * Manual schema validation. - */ -export type SchemaValidator = (document: Readonly) => void; +/** Search function for search queries. */ +export type SearchFunction = (value: Readonly) => boolean; -/** - * Supported primitives. - */ -export type DocumentPrimitive = string | number | boolean | null; +export type SearchQueryValue = T | SearchFieldFunction | RegExp | undefined; + +export type UpdateQueryValue = T | UpdateFieldFunction | undefined; -/** - * Supported documents DocumentValues. - */ -export type DocumentValue = DocumentPrimitive | DocumentPrimitive[] | Document | Document[] | undefined; +export type UpdateFunction = (value: T) => T; +export type SearchFieldFunction = (value: Readonly) => boolean; +export type UpdateFieldFunction = (value: T) => T; -/** - * Search query value. - */ -export type SearchQueryValue = DocumentValue | SearchFunction | RegExp | undefined; +/** Manual schema validation. */ +export type SchemaValidator = (document: Readonly) => void; + +/** Sorting function */ +export type SortFunction = (a: Readonly, b: Readonly) => number; -/** - * Cursor methods. - */ +/** Cursor methods. */ export type CursorMethod = | { type: 'limit'; number: number } | { type: 'skip'; number: number } diff --git a/lib/error.ts b/lib/error.ts index 1a5f505..a0aa3de 100644 --- a/lib/error.ts +++ b/lib/error.ts @@ -1,8 +1,6 @@ import { isError, isString } from './types.ts'; -/** - * Custom database error. - */ +/** Custom database error. */ export class DatabaseError extends Error { /** Error name. */ public name: string = 'DatabaseError'; diff --git a/lib/main.ts b/lib/main.ts index 2aaa656..3be3670 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -3,15 +3,19 @@ import { Cursor } from './cursor.ts'; import { Storage } from './storage.ts'; import { DatabaseError } from './error.ts'; import { isUndefined, isString, isBoolean, isObject, isArray, isFunction } from './types.ts'; -import { deepClone, cleanArray, isObjectEmpty, updateDocument, prepareObject } from './utils.ts'; -import { Document, Acceptable, DatabaseConfig, SearchQuery, UpdateQuery } from './declarations.ts'; +import { deepClone, cleanArray, isObjectEmpty, updateObject, prepareObject } from './utils.ts'; +import { Document, Acceptable, DatabaseConfig, SearchQuery, UpdateQuery, DocumentValue, UpdateQueryValue } from './declarations.ts'; /** * # AloeDB 🌿 * Light, Embeddable, NoSQL database for Deno */ class AloeDB = Document> { - /** In-Memory documents storage. */ + /** + * In-Memory documents storage. + * + * ___WARNING: It is better not to modify these documents manually, as the changes will not pass the necessary checks.___ + */ public documents: Schema[] = []; /** File storage manager. */ @@ -59,7 +63,7 @@ class AloeDB = Document> { } /** - * Insert new document. + * Insert a document. * @param document Document to insert. * @returns Inserted document. */ @@ -69,44 +73,41 @@ class AloeDB = Document> { if (!isObject(document)) throw new TypeError('Input must be an object'); prepareObject(document); - document = deepClone(document); if (schemaValidator) schemaValidator(document); + + const documentClone: Schema = deepClone(document); + this.documents.push(documentClone); - this.documents.push(document); await this.save(); - - return deepClone(document); + return document; } catch (error) { throw new DatabaseError('Error inserting document', error); } } /** - * Insert many documents at once. + * Inserts multiple documents. * @param documents Array of documents to insert. * @returns Array of inserted documents. */ public async insertMany(documents: Schema[]): Promise { try { const { schemaValidator } = this.config; - const inserted: Schema[] = []; - if (!isArray(documents)) throw new TypeError('Input must be an array'); for (let i = 0; i < documents.length; i++) { - const document: Schema = deepClone(documents[i]); + const document: Schema = documents[i]; if (!isObject(document)) throw new TypeError('Values must be an objects'); prepareObject(document); if (schemaValidator) schemaValidator(document); - - inserted.push(document); } - this.documents = [...this.documents, ...inserted]; - await this.save(); + const documentsClone: Schema[] = deepClone(documents); + this.documents = [...this.documents, ...documentsClone]; - return deepClone(documents); + await this.save(); + return documents; } catch (error) { throw new DatabaseError('Error inserting documents', error); } @@ -114,12 +115,12 @@ class AloeDB = Document> { /** * Find document by search query. - * @param query Document search query. + * @param query Document selection criteria. * @returns Found document. */ - public async findOne(query?: SearchQuery): Promise | null> { + public async findOne(query?: SearchQuery): Promise { try { - if (!isUndefined(query) && !isObject(query)) throw new TypeError('Search query must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object'); if ((isUndefined(query) || isObjectEmpty(query)) && this.documents.length > 0) return deepClone(this.documents[0]); const found: number[] = Search(query, this.documents); @@ -128,7 +129,7 @@ class AloeDB = Document> { const position: number = found[0]; const document: Schema = this.documents[position]; - return document; + return deepClone(document); } catch (error) { throw new DatabaseError('Error searching document', error); } @@ -136,18 +137,18 @@ class AloeDB = Document> { /** * Find multiple documents by search query. - * @param query Documents search query. + * @param query Documents selection criteria. * @returns Found documents. */ public async findMany(query?: SearchQuery): Promise { try { - if (!isUndefined(query) && !isObject(query)) throw new TypeError('Search query must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object'); if (isUndefined(query) || isObjectEmpty(query)) return deepClone(this.documents); const found: number[] = Search(query, this.documents); if (found.length === 0) return []; - const documents: Schema[] = found.map(position => this.documents[position]); + return deepClone(documents); } catch (error) { throw new DatabaseError('Error searching document', error); @@ -156,12 +157,12 @@ class AloeDB = Document> { /** * Count found documents. - * @param query Documents search query. + * @param query Documents selection criteria. * @returns Documents count. */ public async count(query?: SearchQuery): Promise { try { - if (!isUndefined(query) && !isObject(query)) throw new TypeError('Search query must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object'); if (isUndefined(query) || isObjectEmpty(query)) return this.documents.length; const found: number[] = Search(query, this.documents); @@ -171,18 +172,17 @@ class AloeDB = Document> { } } - /** - * Find and update document. - * @param query Documents search query. - * @param update Update query. - * @returns Updated document. + * Modifies an existing document. + * @param query Document selection criteria. + * @param update The modifications to apply. + * @returns Original document that has been modified. */ public async updateOne(query: SearchQuery, update: UpdateQuery): Promise { try { const { schemaValidator } = this.config; - if (!isUndefined(query) && !isObject(query)) throw new TypeError('Search query must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object'); if (!isObject(update) && !isFunction(update)) throw new TypeError('Update query must be an object or function'); const found: number[] = Search(query, this.documents); @@ -190,53 +190,67 @@ class AloeDB = Document> { const position: number = found[0]; const document: Schema = this.documents[position]; + const documentClone: Schema = deepClone(document); - updateDocument(deepClone(update), document); - prepareObject(document); - if (schemaValidator) schemaValidator(document); + prepareObject(update); + const updatedDocument: Schema = updateObject(update, documentClone) as Schema; + if (schemaValidator) schemaValidator(updatedDocument); - this.documents[position] = document; + this.documents[position] = deepClone(updatedDocument); await this.save(); - return deepClone(document); + return document; } catch (error) { throw new DatabaseError('Error updating document', error); } } + /** + * Modifies all documents that match search query. + * @param query Documents selection criteria. + * @param update The modifications to apply. + * @returns Original documents that has been modified. + */ public async updateMany(query: SearchQuery, update: UpdateQuery): Promise { try { const { schemaValidator } = this.config; const updated: Schema[] = []; - if (!isUndefined(query) && !isObject(query)) throw new TypeError('Search query must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object'); if (!isObject(update) && !isFunction(update)) throw new TypeError('Update query must be an object'); const found: number[] = Search(query, this.documents); if (found.length === 0) return []; + prepareObject(update); + for (let i = 0; i < found.length; i++) { const position: number = found[i]; - const document: Schema = deepClone(this.documents[position]); + const document: Schema = this.documents[position]; + const documentClone: Schema = deepClone(document); - updateDocument(update, document); - prepareObject(document); - if (schemaValidator) schemaValidator(document); + const updatedDocument: Schema = updateObject(update, documentClone) as Schema; + if (schemaValidator) schemaValidator(updatedDocument); - this.documents[position] = document; + this.documents[position] = deepClone(document); updated.push(document); - await this.save(); } - - return deepClone(updated); + + await this.save(); + return updated; } catch (error) { throw new DatabaseError('Error updating documents', error); } } + /** + * Delete one document. + * @param query Document selection criteria. + * @returns Deleted document. + */ public async deleteOne(query?: SearchQuery): Promise { try { - if (!isUndefined(query) && !isObject(query)) throw new TypeError('Search query must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object'); const found: number[] = Search(query, this.documents); if (found.length === 0) return null; @@ -253,9 +267,14 @@ class AloeDB = Document> { } } + /** + * Delete many documents. + * @param query Document selection criteria. + * @returns Array of deleted documents. + */ public async deleteMany(query?: SearchQuery): Promise { try { - if (!isUndefined(query) && !isObject(query)) throw new TypeError('Search query must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object'); const deleted: Schema[] = []; const found: number[] = Search(query, this.documents); @@ -278,9 +297,10 @@ class AloeDB = Document> { } } - public select(query: SearchQuery): Cursor { - return new Cursor(query, this.documents, this.config); - } + // TODO + // public select(query?: SearchQuery): Cursor { + // return new Cursor(query, this.documents, this.config); + // } /** * Delete all documents. diff --git a/lib/operators.ts b/lib/operators.ts index 3fdb098..7cc7b68 100644 --- a/lib/operators.ts +++ b/lib/operators.ts @@ -1,53 +1,55 @@ import { deepCompare, matchValues } from './utils.ts'; import { isArray, isUndefined, isString, isNumber, isBoolean, isNull, isObject } from './types.ts'; -import { DocumentValue, DocumentPrimitive, SearchFunction, SearchQueryValue } from './declarations.ts'; +import { DocumentValue, DocumentPrimitive, SearchFunction, SearchFieldFunction, SearchQueryValue } from './declarations.ts'; -export function equal(value: DocumentValue): SearchFunction { - return (target: DocumentValue) => deepCompare(target, value); +// TODO: Comments + +export function equal(value: DocumentValue): SearchFieldFunction { + return target => deepCompare(target, value); } -export function notEqual(value: DocumentValue): SearchFunction { - return (target: DocumentValue) => !deepCompare(target, value); +export function notEqual(value: DocumentValue): SearchFieldFunction { + return target => !deepCompare(target, value); } -export function inside(values: DocumentPrimitive[]): SearchFunction { - return (target: DocumentValue) => values.includes(target as any); +export function inside(values: DocumentPrimitive[]): SearchFieldFunction { + return target => values.includes(target as any); } -export function notInside(values: DocumentPrimitive[]): SearchFunction { - return (target: DocumentValue) => !values.includes(target as any); +export function notInside(values: DocumentPrimitive[]): SearchFieldFunction { + return target => !values.includes(target as any); } -export function moreThan(value: number): SearchFunction { - return (target: DocumentValue) => (target as number) > value; +export function moreThan(value: number): SearchFieldFunction { + return target => (target as number) > value; } -export function moreThanOrEqual(value: number): SearchFunction { - return (target: DocumentValue) => (target as number) >= value; +export function moreThanOrEqual(value: number): SearchFieldFunction { + return target => (target as number) >= value; } -export function lessThan(value: number): SearchFunction { - return (target: DocumentValue) => (target as number) < value; +export function lessThan(value: number): SearchFieldFunction { + return target => (target as number) < value; } -export function lessThanOrEqual(value: number): SearchFunction { - return (target: DocumentValue) => (target as number) <= value; +export function lessThanOrEqual(value: number): SearchFieldFunction { + return target => (target as number) <= value; } -export function between(min: number, max: number): SearchFunction { - return (target: DocumentValue) => (target as number) > min && (target as number) < max; +export function between(min: number, max: number): SearchFieldFunction { + return target => (target as number) > min && (target as number) < max; } -export function betweenOrEqual(min: number, max: number): SearchFunction { - return (target: DocumentValue) => (target as number) >= min && (target as number) <= max; +export function betweenOrEqual(min: number, max: number): SearchFieldFunction { + return target => (target as number) >= min && (target as number) <= max; } -export function exists(): SearchFunction { - return (target: DocumentValue) => !isUndefined(target); +export function exists(): SearchFieldFunction { + return target => !isUndefined(target); } -export function type(value: 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'): SearchFunction { - return (target: DocumentValue) => { +export function type(value: 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'): SearchFieldFunction { + return target => { switch (value) { case 'string': return isString(target); @@ -67,30 +69,30 @@ export function type(value: 'string' | 'number' | 'boolean' | 'null' | 'array' | }; } -export function includes(value: DocumentPrimitive): SearchFunction { - return (target: DocumentValue) => isArray(target) && target.includes(value as any); +export function includes(value: DocumentPrimitive): SearchFieldFunction { + return target => isArray(target) && target.includes(value as any); } -export function length(value: number): SearchFunction { - return (target: DocumentValue) => isArray(target) && target.length === value; +export function length(value: number): SearchFieldFunction { + return target => isArray(target) && target.length === value; } -export function elementMatch(...values: SearchQueryValue[]): SearchFunction { - return (target: DocumentValue) => isArray(target) && target.some((targetValue: DocumentValue) => values.every((value: SearchQueryValue) => matchValues(value, targetValue))); +export function elementMatch(...values: SearchQueryValue[]): SearchFieldFunction { + return target => isArray(target) && target.some((targetValue: DocumentValue) => values.every((value: SearchQueryValue) => matchValues(value, targetValue))); } -export function not(value: SearchQueryValue): SearchFunction { - return (target: DocumentValue) => matchValues(value, target) === false; +export function not(value: SearchQueryValue): SearchFieldFunction { + return target => matchValues(value, target as DocumentValue) === false; } -export function and(...values: SearchQueryValue[]): SearchFunction { - return (target: DocumentValue) => values.every(value => matchValues(value, target)); +export function and(...values: SearchQueryValue[]): SearchFieldFunction { + return target => values.every(value => matchValues(value, target as DocumentValue)); } -export function or(...values: SearchQueryValue[]): SearchFunction { - return (target: DocumentValue) => values.some(value => matchValues(value, target)); +export function or(...values: SearchQueryValue[]): SearchFieldFunction { + return target => values.some(value => matchValues(value, target as DocumentValue)); } -export function nor(...values: SearchQueryValue[]): SearchFunction { - return (target: DocumentValue) => !values.some(value => matchValues(value, target)); +export function nor(...values: SearchQueryValue[]): SearchFieldFunction { + return target => !values.some(value => matchValues(value, target as DocumentValue)); } diff --git a/lib/search.ts b/lib/search.ts index 8358edc..918e66d 100644 --- a/lib/search.ts +++ b/lib/search.ts @@ -1,6 +1,6 @@ -import { isUndefined } from './types.ts'; -import { cleanArray, getNestedValue, isObjectEmpty, matchValues } from './utils.ts'; -import { SearchQuery, SearchQueryValue, DocumentValue, Acceptable } from './declarations.ts'; +import { isUndefined, isFunction } from './types.ts'; +import { cleanArray, isObjectEmpty, matchValues, numbersList } from './utils.ts'; +import { Document, SearchQuery, SearchQueryValue, DocumentValue, Acceptable } from './declarations.ts'; /** * Find documents positions. @@ -8,23 +8,33 @@ import { SearchQuery, SearchQueryValue, DocumentValue, Acceptable } from './decl * @param documents An array of positions of suitable documents. * @returns Found positions. */ -export function Search>(query: SearchQuery | undefined, documents: T[]): number[] { +export function Search = Document>(query: SearchQuery | undefined, documents: T[]): number[] { let found: number[] = []; let firstSearch: boolean = true; if (documents.length === 0) return []; - if (isUndefined(query) || isObjectEmpty(query)) return [...Array(documents.length).keys()]; + if (isUndefined(query) || isObjectEmpty(query)) return numbersList(documents.length); + + if (isFunction(query)) { + for (let i = 0; i < documents.length; i++) { + const document: T = documents[i]; + + const isMatched: boolean = query(document); + if (isMatched) found.push(i); + } + + return found; + } for (const key in query) { const queryValue: SearchQueryValue = query[key] as SearchQueryValue; - const isNested: boolean = key.includes('.'); if (firstSearch) { firstSearch = false; for (let i = 0; i < documents.length; i++) { - const document: T = documents[i]; - const documentValue: DocumentValue = isNested ? getNestedValue(key, document) : document[key as keyof T]; + const document: Acceptable = documents[i] as Acceptable; + const documentValue: DocumentValue = document[key] as DocumentValue; const isMatched: boolean = matchValues(queryValue, documentValue); if (isMatched) found.push(i); @@ -38,8 +48,8 @@ export function Search>(query: SearchQuery | undefine if (isUndefined(found[i])) continue; const position: number = found[i]; - const document: T = documents[position]; - const documentValue: DocumentValue = isNested ? getNestedValue(key, document) : document[key as keyof T]; + const document: Acceptable = documents[position] as Acceptable; + const documentValue: DocumentValue = document[key] as DocumentValue; const isMatched: boolean = matchValues(queryValue, documentValue); if (isMatched) continue; diff --git a/lib/storage.ts b/lib/storage.ts index ce522e7..c38e01d 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -24,6 +24,7 @@ export class Storage { /** * Write data to the database file. + * @param documents Documents to write. */ public async write(documents: Document[]): Promise { try { diff --git a/lib/types.ts b/lib/types.ts index 0a80b4b..929aef2 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,99 +1,99 @@ import { UnknownObject } from './declarations.ts'; /** - * Checks whether the value is a string - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a string. + * @param target Target to check. + * @returns Result of checking. */ export function isString(target: unknown): target is string { return typeof target === 'string'; } /** - * Checks whether the value is a number - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a number. + * @param target Target to check. + * @returns Result of checking. */ export function isNumber(target: unknown): target is number { return typeof target === 'number' && !Number.isNaN(target); } /** - * Checks whether the value is a boolean - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a boolean. + * @param target Target to check. + * @returns Result of checking. */ export function isBoolean(target: unknown): target is boolean { return typeof target === 'boolean'; } /** - * Checks whether the value is undefined - * @param target Target to check - * @returns Result of checking + * Checks whether the value is undefined. + * @param target Target to check. + * @returns Result of checking. */ export function isUndefined(target: unknown): target is undefined { return typeof target === 'undefined'; } /** - * Checks whether the value is a null - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a null. + * @param target Target to check. + * @returns Result of checking. */ export function isNull(target: unknown): target is null { return target === null; } /** - * Checks whether the value is a function - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a function. + * @param target Target to check. + * @returns Result of checking. */ export function isFunction(target: unknown): target is (...args: any) => any { return typeof target === 'function'; } /** - * Checks whether the value is an array - * @param target Target to check - * @returns Result of checking + * Checks whether the value is an array. + * @param target Target to check. + * @returns Result of checking. */ export function isArray(target: unknown): target is any[] { return target instanceof Array; } /** - * Checks whether the value is a object - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a object. + * @param target Target to check. + * @returns Result of checking. */ export function isObject(target: unknown): target is UnknownObject { return target !== null && typeof target === 'object' && target?.constructor === Object; } /** - * Checks whether the value is a regular expression - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a regular expression. + * @param target Target to check. + * @returns Result of checking. */ export function isRegExp(target: unknown): target is RegExp { return target instanceof RegExp; } /** - * Checks whether the value is a date - * @param target Target to check - * @returns Result of checking + * Checks whether the value is a date. + * @param target Target to check. + * @returns Result of checking. */ export function isDate(target: unknown): target is Date { return target instanceof Date; } /** - * Checks whether the value is an error - * @param target Target to check - * @returns Result of checking + * Checks whether the value is an error. + * @param target Target to check. + * @returns Result of checking. */ export function isError(target: unknown): target is Error { return target instanceof Error; diff --git a/lib/utils.ts b/lib/utils.ts index 65691aa..1c5952b 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,5 @@ import DatabaseError from './error.ts'; -import { Acceptable, UnknownObject, DocumentValue, SearchQueryValue, UpdateQuery } from './declarations.ts'; +import { Acceptable, Document, UnknownObject, DocumentValue, SearchQueryValue, UpdateQuery, UpdateQueryValue } from './declarations.ts'; import { isFunction, isArray, isObject, isString, isNumber, isRegExp, isBoolean, isNull, isUndefined } from './types.ts'; /** @@ -21,13 +21,24 @@ export function isObjectEmpty(target: UnknownObject): boolean { return true; } +/** + * Generate array of numbers from 0 to Nth. + * @param number Nth value. + * @returns Generated array. + */ +export function numbersList(number: number): number[] { + const array: number[] = []; + for (let i = -1; i < number; i++) array.push(i); + return array; +} + /** * Get number of keys in object. * @param target An object for key counting. * @returns Number of keys. */ export function getObjectLength(target: UnknownObject): number { - let length = 0; + let length: number = 0; for (let key in target) length++; return length; } @@ -95,67 +106,6 @@ export function deepCompare(targetA: unknown, targetB: unknown): boolean { return targetA === targetB; } -/** - * Get nested value from object using dot notation. - * @param query Path to the value. - * @param object Object to get value from. - * @return Found value. - */ -export function getNestedValue(query: string, object: UnknownObject): any { - if (!query.includes('.')) return object[query]; - - const parts = query.split('.'); - const length = parts.length; - let property = object; - - for (let i = 0; i < length; i++) { - const part = parts[i]; - const nested = property[part]; - - if (isArray(nested) || isObject(nested)) { - property = nested; - } else { - return i === length - 1 ? nested : undefined; - } - } - - return property; -} - -/** - * Set property of nested object . - * @param query Path to the value. - * @param value Value to set. - * @param object Object to set value in. - */ -export function setNestedValue(query: string, value: any, object: UnknownObject): void { - if (!query.includes('.')) { - object[query] = value; - return; - } - - const parts = query.split('.'); - const length = parts.length - 1; - let property = object; - - for (let i = 0; i < length; i++) { - const part = parts[i]; - const nested = property[part]; - const nextPart = parts[i + 1] as any; - - if (isArray(nested)) { - if (!isUndefined(nextPart) && isNaN(nextPart)) property[part] = {}; - } else { - if (!isObject(nested)) property[part] = {}; - } - - property = property[part]; - } - - const lastPart = parts[length]; - property[lastPart] = value; -} - /** * Compares the value from the query and from the document. * @param queryValue Value from query. @@ -187,17 +137,19 @@ export function matchValues(queryValue: SearchQueryValue, documentValue: Documen } /** - * Update document. + * Update object. * @param query Update query. * @param document Document to update. */ -export function updateDocument>(query: UpdateQuery, document: T): void { +export function updateObject(query: UnknownObject, document: Document): Document { if (isFunction(query)) return query(document); for (const key in query) { - const queryValue: DocumentValue = deepClone(query[key]); - setNestedValue(key, queryValue, document); + const value: UpdateQueryValue = query[key] as UpdateQueryValue; + document[key] = isFunction(value) ? value(document[key]) : value as DocumentValue; } + + return document; } /** @@ -208,10 +160,6 @@ export function prepareObject(target: UnknownObject): void { for (const key in target) { const value = target[key]; - if (key.includes('.')) { - throw new DatabaseError('Fields in documents cannot contain a "." character'); - } - if (isArray(value)) { prepareArray(value); continue; diff --git a/mod.ts b/mod.ts index aa8d93d..c130519 100644 --- a/mod.ts +++ b/mod.ts @@ -1,2 +1,3 @@ export { default as AloeDB } from './lib/main.ts'; -export * as Operators from './lib/operators.ts'; \ No newline at end of file +export * as Operators from './lib/operators.ts'; +export * as Declarations from './lib/declarations.ts'; diff --git a/tests/benchmark.ts b/tests/benchmark.ts deleted file mode 100644 index 07719d3..0000000 --- a/tests/benchmark.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { AloeDB, Operators } from '../mod.ts'; -import { type } from '../lib/operators.ts'; - -/** - * At this moment, this benchmark should not be taken seriously, the data may not be accurate. - */ - -const DocumentsCount = 1000; -const Iterations = 10; - -interface Results { - insert: number[]; - find: number[]; - count: number[]; - update: number[]; - delete: number[]; -} - -interface TestDocument { - number: number; - string: string; - deleted: undefined; - array: number[]; - object: { boolean: boolean }; -} - -const results: Results = { - insert: [], - find: [], - count: [], - update: [], - delete: [], -}; - -const db = new AloeDB({ filePath: 'text.txt' }); - -for (let i = 0; i < Iterations; i++) { - console.log('iteration', i); - - await insertBenchmark(); - await updateBenchmark(); - await countBenchmark(); - await findBenchmark(); - await deleteBenchmark(); -} - -async function insertBenchmark(): Promise { - const start = performance.now(); - - for (let j = 0; j < DocumentsCount; j++) { - await db.insertOne({ - number: j, - string: `test-${j}`, - deleted: undefined, - array: [j, j, j], - object: { boolean: true }, - }); - } - - const end = performance.now(); - results.insert.push(end - start); -} - -async function findBenchmark(): Promise { - const start = performance.now(); - - for (let j = 0; j < DocumentsCount; j++) { - const result = await db.findOne({ - number: j, - array: [j, j, j], - object: { boolean: false }, - }); - } - - const end = performance.now(); - results.find.push(end - start); -} - -async function countBenchmark(): Promise { - const start = performance.now(); - - for (let j = 0; j < DocumentsCount; j++) { - const result = await db.count({ - number: j, - array: [j, j, j], - object: { boolean: false }, - }); - } - - const end = performance.now(); - results.count.push(end - start); -} - -async function updateBenchmark(): Promise { - const start = performance.now(); - - for (let j = 0; j < DocumentsCount; j++) { - const result = await db.updateOne( - { number: j }, - { - number: j, - string: `x-${j}`, - array: [1, 1, 1], - object: { boolean: false }, - } - ); - } - - - const end = performance.now(); - results.update.push(end - start); -} - -async function deleteBenchmark(): Promise { - const start = performance.now(); - - await db.deleteMany({ - string: /x/, - array: [1, 1, 1], - object: { boolean: false }, - }); - - const end = performance.now(); - results.delete.push(end - start); -} - -function calculate(nums: number[]) { - const average = nums.reduce((a, b) => a + b) / nums.length; - return numberWithCommas(Math.round(DocumentsCount / (average / 1000))); -} - -function numberWithCommas(x: number) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -} - -console.log(results); -console.log('insert:', calculate(results.insert), 'ops/sec'); -console.log('find:', calculate(results.find), 'ops/sec'); -console.log('count:', calculate(results.count), 'ops/sec'); -console.log('update:', calculate(results.update), 'ops/sec'); -console.log('delete:', calculate(results.delete), 'ops/sec'); diff --git a/tests/benchmarks/findOne_benchmark.ts b/tests/benchmarks/findOne_benchmark.ts new file mode 100644 index 0000000..16e9951 --- /dev/null +++ b/tests/benchmarks/findOne_benchmark.ts @@ -0,0 +1,49 @@ +import { AloeDB, Operators } from '../../mod.ts'; +import { BenchmarkDocument, CalculateResult } from './utils.ts'; + +/** + * At this moment, this benchmark should not be taken seriously, the data may not be accurate. + */ + +const DocumentsCount = 1000; +const Iterations = 10; +const Results = []; + +const db = new AloeDB({ onlyInMemory: true }); + +for (let i = -1; i < Iterations; i++) { + const first: boolean = i === -1; + + db.documents = []; + for (let i = 0; i < DocumentsCount; i++) { + db.documents.push({ + number: i, + string: `test-${i}`, + array: [i, i, i], + object: { boolean: true }, + }); + } + + const time = await FindOneBenchmark(); + if (first) continue; + + Results.push(time); +} + +async function FindOneBenchmark(): Promise { + const start = performance.now(); + + for (let i = 0; i < DocumentsCount; i++) { + await db.findOne({ + number: i, + string: `test-${i}`, + array: [i, i, i], + object: { boolean: true }, + }); + } + + const end = performance.now(); + return end - start; +} + +console.log('FindOne:', CalculateResult(Results, DocumentsCount), 'ops/sec'); \ No newline at end of file diff --git a/tests/benchmarks/insertOne_benchmark.ts b/tests/benchmarks/insertOne_benchmark.ts new file mode 100644 index 0000000..739924c --- /dev/null +++ b/tests/benchmarks/insertOne_benchmark.ts @@ -0,0 +1,40 @@ +import { AloeDB, Operators } from '../../mod.ts'; +import { BenchmarkDocument, CalculateResult } from './utils.ts'; + +/** + * At this moment, this benchmark should not be taken seriously, the data may not be accurate. + */ + +const DocumentsCount = 1000; +const Iterations = 10; +const Results = []; + +const db = new AloeDB({ onlyInMemory: true }); + +for (let i = -1; i < Iterations; i++) { + const first: boolean = i === -1; + db.documents = []; + + const time = await InsertOneBenchmark(); + if (first) continue; + + Results.push(time); +} + +async function InsertOneBenchmark(): Promise { + const start = performance.now(); + + for (let i = 0; i < DocumentsCount; i++) { + await db.insertOne({ + number: i, + string: `test-${i}`, + array: [i, i, i], + object: { boolean: true }, + }); + } + + const end = performance.now(); + return end - start; +} + +console.log('InsertOne:', CalculateResult(Results, DocumentsCount), 'ops/sec'); \ No newline at end of file diff --git a/tests/benchmarks/updateOne_benchmark.ts b/tests/benchmarks/updateOne_benchmark.ts new file mode 100644 index 0000000..d67d45d --- /dev/null +++ b/tests/benchmarks/updateOne_benchmark.ts @@ -0,0 +1,51 @@ +import { AloeDB, Operators } from '../../mod.ts'; +import { BenchmarkDocument, CalculateResult } from './utils.ts'; + +/** + * At this moment, this benchmark should not be taken seriously, the data may not be accurate. + */ + +const DocumentsCount = 1000; +const Iterations = 10; +const Results = []; + +const db = new AloeDB({ onlyInMemory: true }); + +for (let i = -1; i < Iterations; i++) { + const first: boolean = i === -1; + + db.documents = []; + for (let i = 0; i < DocumentsCount; i++) { + db.documents.push({ + number: i, + string: `test-${i}`, + array: [i, i, i], + object: { boolean: true }, + }); + } + + const time = await UpdateOneBenchmark(); + if (first) continue; + + Results.push(time); +} + +async function UpdateOneBenchmark(): Promise { + const start = performance.now(); + + for (let i = 0; i < DocumentsCount; i++) { + await db.updateOne({ + number: i, + string: `test-${i}`, + array: [i, i, i], + object: { boolean: true }, + }, { + string: 'x' + }); + } + + const end = performance.now(); + return end - start; +} + +console.log('FindOne:', CalculateResult(Results, DocumentsCount), 'ops/sec'); \ No newline at end of file diff --git a/tests/benchmarks/utils.ts b/tests/benchmarks/utils.ts new file mode 100644 index 0000000..a9d5b78 --- /dev/null +++ b/tests/benchmarks/utils.ts @@ -0,0 +1,16 @@ +export interface BenchmarkDocument { + number: number; + string: string; + array: number[]; + object: { boolean: boolean }; +} + +export function CalculateResult(results: number[], ops: number): string { + if (results.length === 0) return 'invalid'; + + const average = results.reduce((a, b) => a + b) / results.length; + const modified = Math.round(ops / (average / 1000)); + const formated = modified.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + + return formated; +} \ No newline at end of file diff --git a/tests/unit/types.test.ts b/tests/unit/types_test.ts similarity index 100% rename from tests/unit/types.test.ts rename to tests/unit/types_test.ts diff --git a/tests/unit/utils.test.ts b/tests/unit/utils_test.ts similarity index 70% rename from tests/unit/utils.test.ts rename to tests/unit/utils_test.ts index cea1f9a..c476365 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils_test.ts @@ -7,11 +7,9 @@ import { getObjectLength, deepClone, deepCompare, - getNestedValue, - setNestedValue, prepareArray, prepareObject, - updateDocument, + updateObject, matchValues, } from '../../lib/utils.ts'; @@ -149,76 +147,6 @@ Deno.test(`${green('[utils.ts]')} deepCompare ( Objects & Arrays )`, () => { ); }); -Deno.test(`${green('[utils.ts]')} getNestedValue`, () => { - const object: any = { - a: 1, - b: 'text', - c: true, - d: undefined, - e: null, - f: { test: null, value: undefined, x: [1, 2, 3], z: { test: 'text' } }, - g: [1, true, 'text', null, undefined, { test: 1 }, [1, 2, 3]], - }; - - const a = getNestedValue('d', object); - const b = getNestedValue('f.test', object); - const c = getNestedValue('f.z.test', object); - const d = getNestedValue('g.0', object); - const e = getNestedValue('g.6.2', object); - - const f = getNestedValue('x', object); - const g = getNestedValue('f.0', object); - const h = getNestedValue('g.6.99', object); - - assertEquals(a, undefined); - assertEquals(b, null); - assertEquals(c, 'text'); - assertEquals(d, 1); - assertEquals(e, 3); - - assertEquals(f, undefined); - assertEquals(g, undefined); - assertEquals(h, undefined); -}); - -Deno.test(`${green('[utils.ts]')} setNestedValue`, () => { - const object: any = { - a: 1, - b: 'text', - c: true, - d: undefined, - e: null, - f: { test: null, value: undefined, x: [1, 2, 3], z: { test: 'text' } }, - g: [1, true, 'text', null, undefined, { test: 1 }, [1, 2, 3]], - hh: [1, 2, 3], - }; - - setNestedValue('d', 300, object); - setNestedValue('f.test', true, object); - setNestedValue('f.z.test', 'other', object); - setNestedValue('g.0', 666, object); - setNestedValue('g.6.2', { test: true }, object); - - assertEquals(object.d, 300); - assertEquals(object.f.test, true); - assertEquals(object.f.z.test, 'other'); - assertEquals(object.g[0], 666); - assertEquals(object.g[6][2], { test: true }); - - setNestedValue('d.test.0.0', 0, object); - setNestedValue('a.b.c', { test: false }, object); - setNestedValue('x.y.0', [1, 2, 3], object); - setNestedValue('g.z.0.2', { x: [0, 0, 0] }, object); - setNestedValue('g.0.0.2', 'test', object); - setNestedValue('hh.10.1', [1, 2, 3], object); - - assertEquals(object.d.test[0][0], 0); - assertEquals(object.a.b.c, { test: false }); - assertEquals(object.x.y[0], [1, 2, 3]); - assertEquals(object.g.z[0][2], { x: [0, 0, 0] }); - assertEquals(object.g[0][0][2], 'test'); -}); - Deno.test(`${green('[utils.ts]')} prepareArray`, () => { const a: any = [1, 'text', true, undefined, null, { test: null, test2: undefined, test3: [1, 2, 3] }, [null, undefined, { test: undefined }]]; @@ -289,23 +217,27 @@ Deno.test(`${green('[utils.ts]')} matchValues`, () => { assertEquals(matchValues({ test: new Map() as any }, { test: [] }), false); }); -Deno.test(`${green('[utils.ts]')} updateDocument`, () => { +Deno.test(`${green('[utils.ts]')} updateObject`, () => { const object: any = { test: '123' }; - updateDocument({ value: true }, object); + updateObject({ value: true }, object); assertEquals(object, { test: '123', value: true }); - updateDocument({ 'value.test': [] }, object); + updateObject({ value: { test: [] } }, object); assertEquals(object, { test: '123', value: { test: [] } }); - updateDocument({ 'value.test.0': true }, object); + updateObject({ value: { test: [true] } }, object); assertEquals(object, { test: '123', value: { test: [true] } }); - updateDocument({ value: undefined }, object); + updateObject({ value: undefined }, object); assertEquals(object, { test: '123', value: undefined }); - updateDocument(document => { + updateObject(document => { document.value = 3; + return document; }, object); assertEquals(object, { test: '123', value: 3 }); + + updateObject({ value: x => { return [1, 2, 3, x ]} }, object); + assertEquals(object, { test: '123', value: 3 }); });