diff --git a/README.md b/README.md index c28c62c..3832c02 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ AloeDB Logo

-

AloeDB

-

Light, Embeddable, NoSQL database for Deno

- +

+

AloeDB

+

Light, Embeddable, NoSQL database for Deno

+


@@ -60,7 +61,7 @@ await db.deleteOne({ title: 'Drive' });
-## 🏃‍♂️ Benchmarks +## 🏃‍ Benchmarks This database is not aimed at a heavily loaded backend, but its speed should be good enough for small APIs working with less than a million documents. To give you an example, here is the speed of a database operations with *1000* documents: @@ -69,27 +70,438 @@ To give you an example, here is the speed of a database operations with *1000* d | ------------- | ------------- | ------------- | ------------- | | 15k _ops/sec_ | 65k _ops/sec_ | 8k _ops/sec_ | 10k _ops/sec_ | - +🟠 **[AlroeDB](https://github.com/wkirk01/AlroeDB)** - database for Rust, also made by [wkirk01](https://github.com/wkirk01)! + +🟢 **[AloeDB-Node](https://github.com/ElectroGamez/AloeDB-Node)** - port to the Node.js, made by [ElectroGamez](https://github.com/ElectroGamez)! _(With awesome Active Records example)_ + +
+## 📃 License +MIT _(see [LICENSE](https://github.com/Kirlovon/AloeDB/blob/master/LICENSE) file)_ diff --git a/lib/core.ts b/lib/core.ts index 94abc44..373cd84 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -1,4 +1,15 @@ -import { Document, DocumentValue, Query, QueryFunction, QueryValue, Update, UpdateFunction, UpdateValue } from './types.ts'; +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + +import { + Document, + DocumentValue, + Query, + QueryValue, + QueryFunction, + Update, + UpdateValue, + UpdateFunction, +} from './types.ts'; import { cleanArray, @@ -75,33 +86,34 @@ export function searchDocuments(query: Query | QueryFunction | undefined, docume * Create new document applying modifications to specified document. * @param document Document to update. * @param update The modifications to apply. - * @returns New document with applyed updates. + * @returns New document with applyed updates or null. */ -export function updateDocument(document: Document, update: Update | UpdateFunction): Document { - const documentClone: Document = deepClone(document); +export function updateDocument(document: Document, update: Update | UpdateFunction): Document | null { + let newDocument: Document | null = deepClone(document); if (isFunction(update)) { - const newDocument: Document = update(documentClone); + newDocument = update(newDocument); + if (!newDocument) return null; if (!isObject(newDocument)) throw new TypeError('Document must be an object'); - prepareObject(newDocument); - return newDocument; - } + } else { + for (const key in update) { + const value: UpdateValue = update[key]; + const result: DocumentValue | undefined = isFunction(value) ? value(newDocument[key]) : value; - for (const key in update) { - const value: UpdateValue = update[key]; - const result: DocumentValue | undefined = isFunction(value) ? value(documentClone[key]) : value; + if (isUndefined(result)) { + delete newDocument[key]; + continue; + } - if (isUndefined(result)) { - delete documentClone[key]; - continue; + newDocument[key] = result; } - - documentClone[key] = result; } - prepareObject(documentClone); - return documentClone; + prepareObject(newDocument); + if (isObjectEmpty(newDocument)) return null; + + return deepClone(newDocument); } /** @@ -140,16 +152,18 @@ export function matchValues(queryValue: QueryValue, documentValue: DocumentValue * @returns Array of documents. */ export function parseDatabaseStorage(content: string): Document[] { - const documents: any = JSON.parse(content); + const trimmed: string = content.trim(); + if (trimmed === '') return []; + const documents: any = JSON.parse(trimmed); if (!isArray(documents)) throw new TypeError('Database storage should be an array of objects'); for (let i = 0; i < documents.length; i++) { const document: Document = documents[i]; - if (!isObject(document)) throw new TypeError('Database storage should contain only objects'); prepareObject(document); + if (isObjectEmpty(document)) delete documents[i]; } - return documents; + return cleanArray(documents); } diff --git a/lib/database.ts b/lib/database.ts index 791f462..fbc0e9b 100644 --- a/lib/database.ts +++ b/lib/database.ts @@ -1,6 +1,7 @@ +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + import { Writer } from './writer.ts'; import { Reader } from './reader.ts'; -import { DatabaseError } from './error.ts'; import { searchDocuments, updateDocument, parseDatabaseStorage } from './core.ts'; import { Document, DatabaseConfig, Query, QueryFunction, Update, UpdateFunction, Acceptable } from './types.ts'; import { cleanArray, deepClone, isObjectEmpty, prepareObject, isArray, isFunction, isObject, isString, isUndefined } from './utils.ts'; @@ -16,7 +17,7 @@ export class Database = Document> { * In-Memory documents storage. * * ***WARNING:*** It is better not to modify these documents manually, as the changes will not pass the necessary checks. - * ***However, if you modify storage manualy, call the method `db.save()` to save your changes.*** + * ***However, if you modify storage manualy, call the method `await db.save()` to save your changes.*** */ public documents: Schema[] = []; @@ -28,24 +29,25 @@ export class Database = Document> { path: undefined, pretty: true, autoload: true, + autosave: true, + optimize: true, immutable: true, - onlyInMemory: true, - schemaValidator: undefined, + validator: undefined }; /** * Create database collection to store documents. - * @param config Database configuration. + * @param config Database configuration or path to the database file. */ constructor(config?: Partial | string) { - if (isUndefined(config)) config = { onlyInMemory: true }; - if (isString(config)) config = { path: config, onlyInMemory: false }; - if (!isObject(config)) throw new DatabaseError('Database initialization error', 'Config must be an object'); + if (isUndefined(config)) config = { autoload: false, autosave: false }; + if (isString(config)) config = { path: config, autoload: true, autosave: true }; + if (!isObject(config)) throw new TypeError('Config must be an object or a string'); - if (isUndefined(config?.path) && isUndefined(config?.onlyInMemory)) config.onlyInMemory = true; - if (isString(config?.path) && isUndefined(config?.onlyInMemory)) config.onlyInMemory = false; - if (isUndefined(config?.path) && config?.onlyInMemory === false) throw new DatabaseError('Database initialization error', 'It is impossible to disable "onlyInMemory" mode if the "path" is not specified'); + // Disable autosave if path is not specified + if (isUndefined(config?.path)) config.autosave = false; + // Merge default config with users config this.config = { ...this.config, ...config }; // Writer initialization @@ -61,21 +63,18 @@ export class Database = Document> { * @returns Inserted document. */ public async insertOne(document: Schema): Promise { - try { - const { immutable, schemaValidator, onlyInMemory } = this.config; - if (!isObject(document)) throw new TypeError('Document must be an object'); + const { immutable, validator, autosave } = this.config; + if (!isObject(document)) throw new TypeError('Document must be an object'); - prepareObject(document); - if (schemaValidator) schemaValidator(document); + prepareObject(document); + if (isObjectEmpty(document)) return {} as Schema; + if (validator) validator(document); - const internal: Schema = deepClone(document); - this.documents.push(internal); - if (!onlyInMemory) this.save(); + const internal: Schema = deepClone(document); + this.documents.push(internal); + if (autosave) await this.save(); - return immutable ? deepClone(internal) : internal; - } catch (error) { - throw new DatabaseError('Error inserting document', error); - } + return immutable ? deepClone(internal) : internal; } /** @@ -84,32 +83,27 @@ export class Database = Document> { * @returns Array of inserted documents. */ public async insertMany(documents: Schema[]): Promise { - try { - const { immutable, schemaValidator, onlyInMemory } = this.config; - if (!isArray(documents)) throw new TypeError('Input must be an array'); + const { immutable, validator, autosave } = this.config; + if (!isArray(documents)) throw new TypeError('Input must be an array'); - const inserted: Schema[] = []; + const inserted: Schema[] = []; - for (let i = 0; i < documents.length; i++) { - const document: Schema = documents[i]; - if (!isObject(document)) { - throw new TypeError('Documents must be an objects'); - } + for (let i = 0; i < documents.length; i++) { + const document: Schema = documents[i]; + if (!isObject(document)) throw new TypeError('Documents must be an objects'); - prepareObject(document); - if (schemaValidator) schemaValidator(document); + prepareObject(document); + if (isObjectEmpty(document)) continue; + if (validator) validator(document); - const internal: Schema = deepClone(document); - inserted.push(internal); - } + const internal: Schema = deepClone(document); + inserted.push(internal); + } - this.documents = [...this.documents, ...inserted]; - if (!onlyInMemory) this.save(); + this.documents = [...this.documents, ...inserted]; + if (autosave) await this.save(); - return immutable ? deepClone(inserted) : inserted; - } catch (error) { - throw new DatabaseError('Error inserting documents', error); - } + return immutable ? deepClone(inserted) : inserted; } /** @@ -118,27 +112,23 @@ export class Database = Document> { * @returns Found document. */ public async findOne(query?: Query | QueryFunction): Promise { - try { - const { immutable } = this.config; - if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object or function'); - - // Optimization for empty queries - if (!isFunction(query) && (isUndefined(query) || isObjectEmpty(query))) { - if (this.documents.length === 0) return null; - const document: Schema = this.documents[0]; - return immutable ? deepClone(document) : document; - } - - const found: number[] = searchDocuments(query as Query, this.documents); - if (found.length === 0) return null; - - const position: number = found[0]; - const document: Schema = this.documents[position]; + const { immutable } = this.config; + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); + // Optimization for empty queries + if (!isFunction(query) && (isUndefined(query) || isObjectEmpty(query))) { + if (this.documents.length === 0) return null; + const document: Schema = this.documents[0]; return immutable ? deepClone(document) : document; - } catch (error) { - throw new DatabaseError('Error searching document', error); } + + const found: number[] = searchDocuments(query as Query, this.documents); + if (found.length === 0) return null; + + const position: number = found[0]; + const document: Schema = this.documents[position]; + + return immutable ? deepClone(document) : document; } /** @@ -147,161 +137,152 @@ export class Database = Document> { * @returns Found documents. */ public async findMany(query?: Query | QueryFunction): Promise { - try { - const { immutable } = this.config; - if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object or function'); + const { immutable } = this.config; + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); - // Optimization for empty queries - if (isUndefined(query) || (isObject(query) && isObjectEmpty(query))) { - return immutable ? deepClone(this.documents) : [...this.documents]; - } - - const found: number[] = searchDocuments(query as Query, this.documents); - if (found.length === 0) return []; + // Optimization for empty queries + if (isUndefined(query) || (isObject(query) && isObjectEmpty(query))) { + return immutable ? deepClone(this.documents) : [...this.documents]; + } - const documents: Schema[] = []; + const found: number[] = searchDocuments(query as Query, this.documents); + if (found.length === 0) return []; - for (let i = 0; i < found.length; i++) { - const position: number = found[i]; - const document: Schema = this.documents[position]; - documents.push(document); - } + const documents: Schema[] = []; - return immutable ? deepClone(documents) : documents; - } catch (error) { - throw new DatabaseError('Error searching document', error); + for (let i = 0; i < found.length; i++) { + const position: number = found[i]; + const document: Schema = this.documents[position]; + documents.push(document); } + + return immutable ? deepClone(documents) : documents; } /** - * Modifies an existing document. + * Modifies an existing document that match search query. * @param query Document selection criteria. * @param update The modifications to apply. - * @returns Original document that has been modified. + * @returns Found document with applied modifications. */ public async updateOne(query: Query | QueryFunction, update: Update | UpdateFunction): Promise { - try { - const { schemaValidator, onlyInMemory } = this.config; + const { validator, autosave, immutable } = this.config; - if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object or function'); - if (!isObject(update) && !isFunction(update)) throw new TypeError('Update must be an object or function'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); + if (!isObject(update) && !isFunction(update)) throw new TypeError('Update must be an object or function'); - const found: number[] = searchDocuments(query as Query, this.documents); - if (found.length === 0) return null; + const found: number[] = searchDocuments(query as Query, this.documents); + if (found.length === 0) return null; - const position: number = found[0]; - const document: Schema = this.documents[position]; + const position: number = found[0]; + const document: Schema = this.documents[position]; + const updated: Schema | null = updateDocument(document, update as Update) as Schema | null; - const updated: Schema = updateDocument(document, update as Update) as Schema; - if (schemaValidator) schemaValidator(updated); + if (!updated) { + this.documents.splice(position, 1); + return {} as Schema; + } - this.documents[position] = updated; - if (!onlyInMemory) this.save(); + if (validator) validator(updated); - return document; - } catch (error) { - throw new DatabaseError('Error updating document', error); - } + this.documents[position] = updated; + if (autosave) await this.save(); + + return immutable ? deepClone(updated) : updated; } /** * 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. + * @returns Found documents with applied modifications. */ public async updateMany(query: Query | QueryFunction, update: Update | UpdateFunction): Promise { - try { - const { schemaValidator, onlyInMemory } = this.config; + const { validator, autosave, immutable } = this.config; - if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object or function'); - if (!isObject(update) && !isFunction(update)) throw new TypeError('Update must be an object'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); + if (!isObject(update) && !isFunction(update)) throw new TypeError('Update must be an object or function'); - const found: number[] = searchDocuments(query as Query, this.documents); - if (found.length === 0) return []; + const found: number[] = searchDocuments(query as Query, this.documents); + if (found.length === 0) return []; - let temporary: Schema[] = [...this.documents]; - const originals: Schema[] = []; + let temporary: Schema[] = [...this.documents]; + let deleted: boolean = false; + const updatedDocuments: Schema[] = []; - for (let i = 0; i < found.length; i++) { - const position: number = found[i]; - const document: Schema = temporary[position]; - const updated: Schema = updateDocument(document, update as Update | UpdateFunction) as Schema; - if (schemaValidator) schemaValidator(updated); + for (let i = 0; i < found.length; i++) { + const position: number = found[i]; + const document: Schema = temporary[position]; + const updated: Schema | null = updateDocument(document, update as Update | UpdateFunction) as Schema | null; - temporary[position] = updated; - originals.push(document); + if (!updated) { + deleted = true; + delete temporary[position]; + continue; } - this.documents = temporary; - if (!onlyInMemory) this.save(); + if (validator) validator(updated); - return originals; - } catch (error) { - throw new DatabaseError('Error updating documents', error); + temporary[position] = updated; + updatedDocuments.push(updated); } + + this.documents = deleted ? cleanArray(temporary) : temporary; + if (autosave) await this.save(); + + return immutable ? deepClone(updatedDocuments) : updatedDocuments; } /** - * Delete one document. + * Deletes first found document that matches the search query. * @param query Document selection criteria. * @returns Deleted document. */ public async deleteOne(query?: Query | QueryFunction): Promise { - try { - const { onlyInMemory } = this.config; + const { autosave } = this.config; - if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object or function'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); - const found: number[] = searchDocuments(query as Query, this.documents); - if (found.length === 0) return null; + const found: number[] = searchDocuments(query as Query, this.documents); + if (found.length === 0) return null; - const position: number = found[0]; - const deleted: Schema = this.documents[position]; + const position: number = found[0]; + const deleted: Schema = this.documents[position]; - this.documents.splice(position, 1); - if (!onlyInMemory) this.save(); + this.documents.splice(position, 1); + if (autosave) await this.save(); - return deleted; - } catch (error) { - throw new DatabaseError('Error deleting documents', error); - } + return deleted; } /** - * Delete many documents. + * Deletes all documents that matches the search query. * @param query Document selection criteria. * @returns Array of deleted documents. */ public async deleteMany(query?: Query | QueryFunction): Promise { - try { - const { onlyInMemory } = this.config; - - if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object or function'); + const { autosave } = this.config; - const found: number[] = searchDocuments(query as Query, this.documents); - if (found.length === 0) return []; + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); - let temporary: Schema[] = [...this.documents]; - const deleted: Schema[] = []; + const found: number[] = searchDocuments(query as Query, this.documents); + if (found.length === 0) return []; - for (let i = 0; i < found.length; i++) { - const position: number = found[i]; - const document: Schema = temporary[position]; + let temporary: Schema[] = [...this.documents]; + const deleted: Schema[] = []; - deleted.push(document); - delete temporary[position]; - } + for (let i = 0; i < found.length; i++) { + const position: number = found[i]; + const document: Schema = temporary[position]; - temporary = cleanArray(temporary); + deleted.push(document); + delete temporary[position]; + } - this.documents = temporary; - if (!onlyInMemory) this.save(); + this.documents = cleanArray(temporary); + if (autosave) await this.save(); - return deleted; - } catch (error) { - throw new DatabaseError('Error deleting documents', error); - } + return deleted; } /** @@ -310,96 +291,74 @@ export class Database = Document> { * @returns Documents count. */ public async count(query?: Query | QueryFunction): Promise { - try { - if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Search query must be an object or function'); + if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); - // Optimization for empty queries - if (isUndefined(query) || (isObject(query) && isObjectEmpty(query))) return this.documents.length; + // Optimization for empty queries + if (isUndefined(query) || (isObject(query) && isObjectEmpty(query))) return this.documents.length; - const found: number[] = searchDocuments(query as Query, this.documents); - return found.length; - } catch (error) { - throw new DatabaseError('Error counting documents', error); - } + const found: number[] = searchDocuments(query as Query, this.documents); + return found.length; } /** * Delete all documents. */ public async drop(): Promise { - try { - const { onlyInMemory } = this.config; - - this.documents = []; - if (!onlyInMemory) this.save(); - } catch (error) { - throw new DatabaseError('Error dropping database', error); - } + this.documents = []; + if (this.config.autosave) await this.save(); } /** - * Load data from database storage file. + * Load data from storage file. */ public async load(): Promise { - try { - const { path, schemaValidator } = this.config; - if (!path) return; - - const content: string = await Reader.read(path); - const documents: Document[] = parseDatabaseStorage(content); - - // Schema validation - if (schemaValidator) { - for (let i = 0; i < documents.length; i++) schemaValidator(documents[i]) - } + const { path, validator } = this.config; + if (!path) return; - this.documents = documents as Schema[]; + const content: string = await Reader.read(path); + const documents: Document[] = parseDatabaseStorage(content); - } catch (error) { - throw new DatabaseError('Error loading documents', error); + // Schema validation + if (validator) { + for (let i = 0; i < documents.length; i++) validator(documents[i]) } + + this.documents = documents as Schema[]; } /** - * Load data from database storage file synchronously. + * Synchronously load data from storage file. */ public loadSync(): void { - try { - const { path, schemaValidator } = this.config; - if (!path) return; - - const content: string = Reader.readSync(path); - const documents: Document[] = parseDatabaseStorage(content); - - // Schema validation - if (schemaValidator) { - for (let i = 0; i < documents.length; i++) schemaValidator(documents[i]) - } + const { path, validator } = this.config; + if (!path) return; - this.documents = documents as Schema[]; + const content: string = Reader.readSync(path); + const documents: Document[] = parseDatabaseStorage(content); - } catch (error) { - throw new DatabaseError('Error loading documents', error); + // Schema validation + if (validator) { + for (let i = 0; i < documents.length; i++) validator(documents[i]) } + + this.documents = documents as Schema[]; } /** * Write documents to the database storage file. - * Called automatically after each insert, update or delete operation. _(Only if `onlyInMemory` mode disabled)_ + * Called automatically after each insert, update or delete operation. _(Only if `autosave` mode enabled)_ */ - public save(): void { - try { - if (!this.writer) return; - - const encoded: string = this.config.pretty - ? JSON.stringify(this.documents, null, '\t') - : JSON.stringify(this.documents); + public async save(): Promise { + if (!this.writer) return; - // No need for await - this.writer.write(encoded); + const encoded: string = this.config.pretty + ? JSON.stringify(this.documents, null, '\t') + : JSON.stringify(this.documents); - } catch (error) { - throw new DatabaseError('Error saving documents', error); + if (this.config.optimize) { + this.writer.add(encoded); // Should be without await + } else { + await this.writer.write(encoded); } } } diff --git a/lib/error.ts b/lib/error.ts deleted file mode 100644 index 4f8a53f..0000000 --- a/lib/error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { isString, isError } from './utils.ts'; - -/** - * Custom database error. - */ -export class DatabaseError extends Error { - /** Error name. */ - public name: string = 'DatabaseError'; - - /** Error message. */ - public message: string; - - /** Exectuion stack. */ - public stack: string | undefined; - - /** Cause of the error. */ - public cause?: string | Error; - - /** - * Error initialization. - * @param message Error message. - * @param cause Cause of the error. - */ - constructor(message: string, cause?: string | Error) { - super(message); - Error.captureStackTrace(this, DatabaseError); - - this.message = message; - if (cause) this.cause = cause; - - if (isString(cause)) this.message = `${message}: ${cause}`; - if (isError(cause) && isString(cause?.message)) { - this.message = `${message}: ${cause.message}`; - } - } -} diff --git a/lib/helpers.ts b/lib/helpers.ts index ab409b2..249ff6b 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,10 +1,12 @@ +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + import { matchValues } from './core.ts'; import { DocumentValue, DocumentPrimitive, QueryValue } from './types.ts'; import { isArray, isUndefined, isString, isNumber, isBoolean, isNull, isObject } from './utils.ts'; /** * Selects documents where the value of a field more than specified number. - * @param value + * @param value Comparison number. */ export function moreThan(value: number) { return (target: Readonly) => isNumber(target) && target > value; @@ -12,7 +14,7 @@ export function moreThan(value: number) { /** * Selects documents where the value of a field more than or equal to the specified number. - * @param value + * @param value Comparison number. */ export function moreThanOrEqual(value: number) { return (target: Readonly) => isNumber(target) && target >= value; @@ -20,7 +22,7 @@ export function moreThanOrEqual(value: number) { /** * Selects documents where the value of a field less than specified number. - * @param value + * @param value Comparison number. */ export function lessThan(value: number) { return (target: Readonly) => isNumber(target) && target < value; @@ -28,7 +30,7 @@ export function lessThan(value: number) { /** * Selects documents where the value of a field less than or equal to the specified number. - * @param value + * @param value Comparison number. */ export function lessThanOrEqual(value: number) { return (target: Readonly) => isNumber(target) && target <= value; @@ -86,7 +88,7 @@ export function type(type: 'string' | 'number' | 'boolean' | 'null' | 'array' | /** * Matches if array includes specified value. - * @param value + * @param value Primitive value to search in array. */ export function includes(value: DocumentPrimitive) { return (target: Readonly) => isArray(target) && target.includes(value); @@ -101,41 +103,41 @@ export function length(length: number) { } /** - * - * @param values + * Matches if at least one value in the array matches the given queries. + * @param queries Query values. */ -export function someElementMatch(...values: QueryValue[]) { - return (target: Readonly) => isArray(target) && target.some(targetValue => values.every(value => matchValues(value, targetValue))); +export function someElementMatch(...queries: QueryValue[]) { + return (target: Readonly) => isArray(target) && target.some(targetValue => queries.every(query => matchValues(query, targetValue))); } /** - * - * @param values + * Matches if all the values in the array match in the given queries. + * @param queries Query values. */ -export function everyElementMatch(...values: QueryValue[]) { - return (target: Readonly) => isArray(target) && target.every(targetValue => values.every(value => matchValues(value, targetValue))); +export function everyElementMatch(...queries: QueryValue[]) { + return (target: Readonly) => isArray(target) && target.every(targetValue => queries.every(query => matchValues(query, targetValue))); } /** * Logical AND operator. Selects documents where the value of a field equals to all specified values. - * @param values Query values. + * @param queries Query values. */ -export function and(...values: QueryValue[]) { - return (target: Readonly) => values.every(value => matchValues(value, target as DocumentValue)); +export function and(...queries: QueryValue[]) { + return (target: Readonly) => queries.every(query => matchValues(query, target as DocumentValue)); } /** * Logical OR operator. Selects documents where the value of a field equals at least one specified value. * @param values Query values. */ -export function or(...values: QueryValue[]) { - return (target: Readonly) => values.some(value => matchValues(value, target as DocumentValue)); +export function or(...queries: QueryValue[]) { + return (target: Readonly) => queries.some(query => matchValues(query, target as DocumentValue)); } /** * Logical NOT operator. Selects documents where the value of a field not equal to specified value. * @param value Query value. */ -export function not(value: QueryValue) { - return (target: Readonly) => matchValues(value, target as DocumentValue) === false; +export function not(query: QueryValue) { + return (target: Readonly) => matchValues(query, target as DocumentValue) === false; } diff --git a/lib/reader.ts b/lib/reader.ts index 1553ef5..8a7b355 100644 --- a/lib/reader.ts +++ b/lib/reader.ts @@ -1,3 +1,5 @@ +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + import { isUndefined, getPathDirname } from './utils.ts'; /** diff --git a/lib/types.ts b/lib/types.ts index c219318..fecaf09 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,8 +1,10 @@ +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + /** * Database initialization config */ export interface DatabaseConfig { - /** Path to the database file. */ + /** Path to the database file. If undefined, data will be stored only in-memory. _(Default: undefined)_ */ path?: string; /** Save data in easy-to-read format. _(Default: true)_ */ @@ -11,26 +13,27 @@ export interface DatabaseConfig { /** Automatically load the file synchronously when initializing the database. _(Default: true)_ */ autoload: boolean; + /** + * Automatically save data to the file after inserting, updating and deleting documents. + * If `path` specified, data will be read from the file, but new data will not be written. + */ + autosave: boolean; + /** Automatically deeply clone all returned objects. _(Default: true)_ */ immutable: boolean; /** - * Do not write data to the database file. - * If "path" specified, data will be read from the file, but new data will not be written. + * Optimize data writing. If enabled, the data will be written many times faster in case of a large number of operations. + * Disable it if you want the methods to be considered executed only when the data is written to a file. _(Default: true)_ */ - onlyInMemory: boolean; + optimize: boolean; /** - * Manual document validation function. + * Runtime documents validation function. * If the document does not pass the validation, just throw the error. * Works well with [Superstruct](https://github.com/ianstormtaylor/superstruct)! */ - schemaValidator?: SchemaValidator; -} - -/** Any object without specified structure. */ -export interface PlainObject { - [key: string]: unknown; + validator?: Validator; } /** Checking the object for suitability for storage. */ @@ -61,10 +64,10 @@ export type QueryValue = DocumentValue export type Update = { [K in keyof T]?: UpdateValue }; /** Manual modifications applying. */ -export type UpdateFunction = (document: T) => T; +export type UpdateFunction = (document: T) => T | null; /** Possible update values. */ export type UpdateValue = T | ((value: T) => T) | undefined; /** Schema validation. Throw error, if document unsuitable. */ -export type SchemaValidator = (document: Readonly) => void; +export type Validator = (document: Readonly) => void; diff --git a/lib/utils.ts b/lib/utils.ts index efdd91b..58ec512 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,4 +1,9 @@ -import { PlainObject } from './types.ts'; +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + +/** Any object without specified structure. */ +interface PlainObject { + [key: string]: unknown; +} /** * Remove all empty items from the array. @@ -261,12 +266,3 @@ export function isObject(target: unknown): target is PlainObject { export function isRegExp(target: unknown): target is RegExp { return target instanceof RegExp; } - -/** - * 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/writer.ts b/lib/writer.ts index 3907e35..a7d680d 100644 --- a/lib/writer.ts +++ b/lib/writer.ts @@ -1,3 +1,5 @@ +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + /** * Data writing manager. * Uses atomic writing and prevents race condition. @@ -16,43 +18,47 @@ export class Writer { /** Temporary file extension. */ private readonly extension: string = '.temp'; - /** - * Storage initialization. - * @param path Path to the database file. - * @param pretty Write data in easy-to-read format. - */ + /** + * Storage initialization. + * @param path Path to the database file. + */ constructor(path: string) { this.path = path; } - /** - * Write data to the database file. - * @param data Data to write. - */ - public async write(data: string): Promise { - - // Add writing to the queue if writing is locked + /** + * Add data to the writing queue. + * Do not call this method with `await`, otherwise the result of this method will be identical to the `write()` method. + * + * @param data Data to add to the queue. + */ + public async add(data: string): Promise { if (this.locked) { this.next = data; return; } - // Lock writing - this.locked = true; - - // Write data - const temp: string = this.path + this.extension; - await Deno.writeTextFile(temp, data); - await Deno.rename(temp, this.path); - - // Unlock writing - this.locked = false; + try { + this.locked = true; + await this.write(data); + } finally { + this.locked = false; + } - // Start next writing if there is data in the queue if (this.next) { const nextCopy: string = this.next; this.next = null; - this.write(nextCopy); + this.add(nextCopy); } } + + /** + * Write data to the database file. + * @param data Data to write. + */ + public async write(data: string): Promise { + const temp: string = this.path + this.extension; + await Deno.writeTextFile(temp, data); + await Deno.rename(temp, this.path); + } } diff --git a/mod.ts b/mod.ts index fa46bfc..bca5701 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,5 @@ +// Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. + export { Database } from './lib/database.ts'; export * from './lib/helpers.ts'; export * from './lib/types.ts'; diff --git a/other/head.png b/other/head.png index 47d955e..57fcbb8 100644 Binary files a/other/head.png and b/other/head.png differ diff --git a/other/logo.afdesign b/other/logo.afdesign index f78081b..7865fb4 100644 Binary files a/other/logo.afdesign and b/other/logo.afdesign differ diff --git a/tests/benchmark/benchmark.ts b/tests/benchmark/benchmark.ts index 30010d8..0c2673e 100644 --- a/tests/benchmark/benchmark.ts +++ b/tests/benchmark/benchmark.ts @@ -5,7 +5,7 @@ import { RunBenchmark } from './utils.ts'; const TEMP_FILE: string = './temp_benchmark_db.json'; // Initialization -const db = new Database({ onlyInMemory: false, immutable: true, path: TEMP_FILE, pretty: true }); +const db = new Database({ path: TEMP_FILE, autosave: true, immutable: true, pretty: false, optimize: true }); // Running insertion operations await RunBenchmark('Insertion', 1000, async (iteration) => { diff --git a/tests/benchmark/utils.ts b/tests/benchmark/utils.ts index 37dd52d..b1247c6 100644 --- a/tests/benchmark/utils.ts +++ b/tests/benchmark/utils.ts @@ -5,15 +5,15 @@ * @param test Test to run */ export async function RunBenchmark(name: string, iterations: number, test: (iteration: number) => Promise): Promise { - const testStart = Date.now(); + const testStart = performance.now(); for(let i = 0; i < iterations; i++) await test(i); - const testEnd = Date.now(); + const testEnd = performance.now(); const timeResult = testEnd - testStart; const operationsCount = 1000 / (timeResult / iterations); const formated = formatNumber(operationsCount); - console.log(`${name}: ${formated} ops/sec (${timeResult} ms)`); + console.log(`${name}: ${formated} ops/sec (${timeResult.toFixed(2)} ms)`); } /** diff --git a/tests/core_test.ts b/tests/core_test.ts index a456400..1a4a962 100644 --- a/tests/core_test.ts +++ b/tests/core_test.ts @@ -1,7 +1,12 @@ import { assertEquals, assertThrows } from 'https://deno.land/std/testing/asserts.ts'; import { yellow } from 'https://deno.land/std/fmt/colors.ts'; -import { searchDocuments, updateDocument, matchValues, parseDatabaseStorage } from '../lib/core.ts'; +import { + searchDocuments, + updateDocument, + matchValues, + parseDatabaseStorage +} from '../lib/core.ts'; Deno.test(`${yellow('[core.ts]')} searchDocuments (Single document)`, () => { const documents: any = [{ object: { foo: 'bar' } }, { array: [1, 2, 3] }, { nothing: null }, { boolean: true }, { number: 42 }, { text: 'foo' }]; @@ -13,7 +18,7 @@ Deno.test(`${yellow('[core.ts]')} searchDocuments (Single document)`, () => { const search5 = searchDocuments({ array: [1, 2, 3] }, documents); const search6 = searchDocuments({ object: { foo: 'bar' } }, documents); const search7 = searchDocuments({ text: /foo/ }, documents); - const search8 = searchDocuments({ number: (value) => value === 42 }, documents); + const search8 = searchDocuments({ number: (value: any) => value === 42 }, documents); assertEquals(search1, [5]); assertEquals(search2, [4]); @@ -138,6 +143,39 @@ Deno.test(`${yellow('[core.ts]')} updateDocument (Update field function)`, () => assertEquals(updated, { test: 'foobar', test2: { value: 0 }, test3: [1, 2, 3, 4] }); }); +Deno.test(`${yellow('[core.ts]')} updateDocument (Empty object)`, () => { + const updated = updateDocument( + { test: true }, + { test: undefined } + ); + assertEquals(updated, null); +}); + +Deno.test(`${yellow('[core.ts]')} updateDocument (Deletion)`, () => { + const updated = updateDocument( + { test: true }, + () => null + ); + assertEquals(updated, null); +}); + +Deno.test(`${yellow('[core.ts]')} updateDocument (Immutability)`, () => { + const array: any = [1, 2, 3, { field: 'value' }]; + const document = { test: [0], foo: 'bar' }; + + const updated = updateDocument( + document, + { test: array } + ); + + array[0] = 999; + array[3].field = 'changed'; + document.test[0] = 999; + document.foo = 'baz'; + + assertEquals(updated, { test: [1, 2, 3, { field: 'value' }], foo: 'bar' }); +}); + Deno.test(`${yellow('[core.ts]')} matchValues (Primitives)`, () => { assertEquals(matchValues('foo', 'foo'), true); assertEquals(matchValues(42, 42), true); @@ -148,7 +186,7 @@ Deno.test(`${yellow('[core.ts]')} matchValues (Primitives)`, () => { Deno.test(`${yellow('[core.ts]')} matchValues (Advanced Valid)`, () => { assertEquals( - matchValues(value => value === 'foo', 'foo'), + matchValues((value: any) => value === 'foo', 'foo'), true ); assertEquals(matchValues(/foo/, 'fooBar'), true); @@ -160,7 +198,7 @@ Deno.test(`${yellow('[core.ts]')} matchValues (Advanced Invalid)`, () => { assertEquals(matchValues('foo', 10), false); assertEquals(matchValues(/bar/, true), false); assertEquals( - matchValues(value => false, true), + matchValues((value: any) => false, true), false ); assertEquals(matchValues({ array: [1, 2, 3] }, { array: [1, 2, 3, 4] }), false); @@ -170,13 +208,16 @@ Deno.test(`${yellow('[core.ts]')} matchValues (Advanced Invalid)`, () => { }); - - Deno.test(`${yellow('[core.ts]')} parseDatabaseStorage`, () => { - const result = parseDatabaseStorage('[{"foo":"bar"}]'); + const result = parseDatabaseStorage('[{"foo":"bar"}, {}]'); assertEquals(result, [{foo: 'bar'}]); }); +Deno.test(`${yellow('[core.ts]')} parseDatabaseStorage (Empty file)`, () => { + const result = parseDatabaseStorage(''); + assertEquals(result, []); +}); + Deno.test(`${yellow('[core.ts]')} parseDatabaseStorage (Not an Array)`, () => { assertThrows(() => parseDatabaseStorage('true'), undefined, 'should be an array of objects') }); diff --git a/tests/database_test.ts b/tests/database_test.ts new file mode 100644 index 0000000..8329413 --- /dev/null +++ b/tests/database_test.ts @@ -0,0 +1,321 @@ +import { assertEquals, assertThrows } from 'https://deno.land/std/testing/asserts.ts'; +import { dirname, fromFileUrl } from 'https://deno.land/std/path/mod.ts'; +import { copy, ensureDir, emptyDir } from 'https://deno.land/std/fs/mod.ts'; +import { magenta } from 'https://deno.land/std/fmt/colors.ts'; +import { delay } from 'https://deno.land/std/async/mod.ts'; + +import { Database } from '../lib/database.ts'; + +// Prepare enviroment +const DIRNAME = dirname(fromFileUrl(import.meta.url)); +const TEMP_PATH = DIRNAME + '/temp_database_tests_enviroment'; +const ENVIROMENT_PATH = DIRNAME + '/enviroment'; + +await ensureDir(TEMP_PATH); +await emptyDir(TEMP_PATH) +await copy(ENVIROMENT_PATH, TEMP_PATH, { overwrite: true }); + +Deno.test({ + name: `${magenta('[database.ts]')} Initialization`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + new Database(); + new Database({}); + new Database({ + path: undefined, + pretty: false, + autoload: true, + autosave: false, + immutable: false, + validator: () => { }, + }); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} Pretty`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const pretty = new Database({ path: TEMP_PATH + '/pretty_test.json', pretty: true }); + const notPretty = new Database({ path: TEMP_PATH + '/not_pretty_test.json', pretty: false }); + + pretty.documents = [{ field: 0 }]; + notPretty.documents = [{ field: 0 }]; + + pretty.save(); + notPretty.save(); + + await delay(100); + + const prettyContent = await Deno.readTextFile(TEMP_PATH + '/pretty_test.json'); + assertEquals(prettyContent.includes('\t'), true); + + const notPrettyContent = await Deno.readTextFile(TEMP_PATH + '/not_pretty_test.json'); + assertEquals(notPrettyContent.includes('\t'), false); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} Return immutability`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const immutable = new Database({ immutable: true }); + + const immutable_insertOne = await immutable.insertOne({ field: 0 }); + immutable_insertOne.field = 1; + + const immutable_insertMany = await immutable.insertMany([{ field: 0 }]); + immutable_insertMany[0].field = 1; + + const immutable_findOne = await immutable.findOne({ field: 0 }) as any; + immutable_findOne.field = 1; + + const immutable_findMany = await immutable.findMany({ field: 0 }); + immutable_findMany[0].field = 1; + immutable_findMany[1].field = 1; + + const immutable_updateOne = await immutable.updateOne({ field: 0 }, { field: 0 }) as any; + immutable_updateOne.field = 1; + + const immutable_updateMany = await immutable.updateMany({ field: () => true }, {}); + immutable_updateMany[0].field = 1; + immutable_updateMany[1].field = 1; + immutable_updateMany.splice(0, 1); + + // Check if something changed + assertEquals(immutable.documents[0].field, 0); + assertEquals(immutable.documents[1].field, 0); + + ///////////////////////////////////////////////// + + const notImmutable = new Database({ immutable: false }); + + const notImmutable_insertOne = await notImmutable.insertOne({ field: 0 }); + notImmutable_insertOne.field = 1; + assertEquals(notImmutable.documents[0].field, 1); + + const notImmutable_insertMany = await notImmutable.insertMany([{ field: 0 }]); + notImmutable_insertMany[0].field = 1; + assertEquals(notImmutable.documents[1].field, 1); + + const notImmutable_findOne = await notImmutable.findOne({ field: 1 }) as any; + notImmutable_findOne.field = 2; + assertEquals(notImmutable.documents[0].field, 2); + + const notImmutable_findMany = await notImmutable.findMany({ field: 1 }); + notImmutable_findMany[0].field = 2; + assertEquals(notImmutable.documents[0].field, 2); + assertEquals(notImmutable.documents[1].field, 2); + + const notImmutable_updateOne = await notImmutable.updateOne({ field: 2 }, { field: 2 }) as any; + notImmutable_updateOne.field = 3; + assertEquals(notImmutable.documents[0].field, 3); + + const notImmutable_updateMany = await notImmutable.updateMany({ field: () => true }, {}); + notImmutable_updateMany[0].field = 4; + notImmutable_updateMany[1].field = 4; + assertEquals(notImmutable.documents[0].field, 4); + assertEquals(notImmutable.documents[1].field, 4); + + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} Storage Immutability`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const db = new Database({ immutable: false }); // Insert should always be immutable, without any exceptions + + const oneDocument: any = { field: [1, 2, { foo: ['bar'] }] }; + await db.insertOne(oneDocument); + oneDocument.field[2].foo[0] = 'baz'; + + const multipleDocuments: any = [{ field: [0] }, { field: [1] }]; + await db.insertMany(multipleDocuments); + multipleDocuments[0].field[0] = 999; + multipleDocuments[1].field[0] = 999; + multipleDocuments.push({ field: [999] }); + + const oneUpdate = [0]; + await db.updateOne({ field: [0] }, { field: oneUpdate }) as any; + oneUpdate[0] = 999; + + const multipleUpdate = [1]; + await db.updateMany({ field: [1] }, { field: multipleUpdate }) as any; + multipleUpdate[0] = 999; + multipleUpdate.push(999); + + assertEquals(db.documents, [ + { field: [1, 2, { foo: ['bar'] }] }, + { field: [0] }, + { field: [1] } + ]); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} insertOne`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const db = new Database({ path: TEMP_PATH + '/insertOne_test.json', pretty: false, optimize: false }); + + const inserted = await db.insertOne({ foo: 'bar' }); + assertEquals(db.documents, [{ foo: 'bar' }]); + assertEquals(inserted, { foo: 'bar' }); + + const content = await Deno.readTextFile(TEMP_PATH + '/insertOne_test.json'); + assertEquals(content, '[{"foo":"bar"}]'); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} insertOne (Empty)`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const db = new Database({ path: TEMP_PATH + '/insertOne_empty_test.json', pretty: false, optimize: false }); + + const inserted = await db.insertOne({}); + assertEquals(db.documents, []); + assertEquals(inserted, {}); + + const content = await Deno.readTextFile(TEMP_PATH + '/insertOne_empty_test.json'); + assertEquals(content, '[]'); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} insertMany`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const db = new Database({ path: TEMP_PATH + '/insertMany_test.json', pretty: false, optimize: false }); + + const inserted = await db.insertMany([{ foo: 'bar' }, { bar: 'foo' }, {}, ]); + assertEquals(db.documents, [{ foo: 'bar' }, { bar: 'foo' }]); + assertEquals(inserted, [{ foo: 'bar' }, { bar: 'foo' }]); + + const content = await Deno.readTextFile(TEMP_PATH + '/insertMany_test.json'); + assertEquals(content, '[{"foo":"bar"},{"bar":"foo"}]'); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} insertMany (Empty)`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const db = new Database({ path: TEMP_PATH + '/insertMany_empty_test.json', pretty: false, optimize: false }); + + const inserted = await db.insertMany([{}, {}, ]); + assertEquals(db.documents, []); + assertEquals(inserted, {}); + + const content = await Deno.readTextFile(TEMP_PATH + '/insertMany_empty_test.json'); + assertEquals(content, '[]'); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} findOne`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const db = new Database({ path: TEMP_PATH + '/findOne_test.json', pretty: false, optimize: false }); + + const initialData = [ + { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }, + { id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }, + { id: 3, text: 'three', boolean: true, empty: null, array: [3], object: {} }, + ]; + + await db.insertMany(initialData); + + const found1 = await db.findOne({ id: 1 }); + const found2 = await db.findOne({ id: 2, notDefined: undefined, object: {} }); + const found3 = await db.findOne({ id: 3, text: /three/, boolean: (value) => value === true, empty: null, array: [3], object: {} }); + const found4 = await db.findOne((value) => value.id === 1); + const found5 = await db.findOne({}); + const notFound1 = await db.findOne({ object: [] }); + const notFound2 = await db.findOne((value) => value.id === 4); + + assertEquals(found1, { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }); + assertEquals(found2, { id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }); + assertEquals(found3, { id: 3, text: 'three', boolean: true, empty: null, array: [3], object: {} }); + assertEquals(found4, { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }); + assertEquals(found5, { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }); + assertEquals(notFound1, null); + assertEquals(notFound2, null); + + (found1 as any).id = 999; + (found2 as any).id = 999; + (found3 as any).id = 999; + (found4 as any).array.push(999); + (found5 as any).array.push(999); + + assertEquals(db.documents, initialData); + } +}); + +Deno.test({ + name: `${magenta('[database.ts]')} findMany`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const db = new Database({ path: TEMP_PATH + '/findMany_test.json', pretty: false, optimize: false }); + + const initialData = [ + { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }, + { id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }, + { id: 3, text: 'three', boolean: true, empty: null, array: [3], object: {} }, + ]; + + await db.insertMany(initialData); + + const found1 = await db.findMany({ id: 1 }); + const found2 = await db.findMany({ id: 2, notDefined: undefined, object: {} }); + const found3 = await db.findMany({ boolean: (value) => value === true, object: {} }); + const found4 = await db.findMany((value) => value.id === 1); + const found5 = await db.findMany({}); + const notFound1 = await db.findMany({ object: [] }); + const notFound2 = await db.findMany((value) => value.id === 4); + + assertEquals(found1, [{ id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }]); + assertEquals(found2, [{ id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }]); + assertEquals(found3, initialData); + assertEquals(found4, [{ id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }]); + assertEquals(found5, initialData); + assertEquals(notFound1, []); + assertEquals(notFound2, []); + + (found1[0] as any).id = 999; + (found2[0] as any).id = 999; + (found3[0] as any).id = 999; + (found4[0] as any).array.push(999); + (found5[0] as any).array.push(999); + + assertEquals(db.documents, initialData); + } +}); + +// TODO: Finish testing + +// Remove temp files +window.addEventListener('unload', () => { + Deno.removeSync(TEMP_PATH, { recursive: true }); +}); diff --git a/tests/enviroments/reader/test_storage.json b/tests/enviroment/test_storage.json similarity index 95% rename from tests/enviroments/reader/test_storage.json rename to tests/enviroment/test_storage.json index 6ccfa27..0350bd5 100644 --- a/tests/enviroments/reader/test_storage.json +++ b/tests/enviroment/test_storage.json @@ -1,4 +1,5 @@ [ + {}, { "username": "foo", "password": "bar", @@ -27,5 +28,6 @@ "username": "foo", "password": "bar", "numbers": "invalid" - } + }, + {} ] diff --git a/tests/reader_test.ts b/tests/reader_test.ts index 4abe53e..2dd08ed 100644 --- a/tests/reader_test.ts +++ b/tests/reader_test.ts @@ -1,66 +1,100 @@ import { assertEquals, assertThrows, assertThrowsAsync } from 'https://deno.land/std/testing/asserts.ts'; import { copy, ensureDir, emptyDir, exists, existsSync } from 'https://deno.land/std/fs/mod.ts'; - +import { dirname, fromFileUrl } from 'https://deno.land/std/path/mod.ts'; import { cyan } from 'https://deno.land/std/fmt/colors.ts'; -import * as path from 'https://deno.land/std/path/mod.ts'; import { Reader } from '../lib/reader.ts'; // Prepare inviroment -const DIRNAME = path.dirname(path.fromFileUrl(import.meta.url)); -const TEMP_PATH = DIRNAME + '/temp_reader'; -const ENVIROMENT_PATH = DIRNAME + '/enviroments/reader'; +const DIRNAME = dirname(fromFileUrl(import.meta.url)); +const TEMP_PATH = DIRNAME + '/temp_reader_tests_enviroment'; +const ENVIROMENT_PATH = DIRNAME + '/enviroment'; await ensureDir(TEMP_PATH); await emptyDir(TEMP_PATH) await copy(ENVIROMENT_PATH, TEMP_PATH, { overwrite: true }); -Deno.test(`${cyan('[reader.ts]')} Read Sync (Basic)`, () => { - const content = Reader.readSync(TEMP_PATH + '/test_storage.json'); - const originalContent = Deno.readTextFileSync(TEMP_PATH + '/test_storage.json'); +Deno.test({ + name: `${cyan('[reader.ts]')} Read Sync (Basic)`, + sanitizeResources: false, + sanitizeOps: false, + + fn() { + const content = Reader.readSync(TEMP_PATH + '/test_storage.json'); + const originalContent = Deno.readTextFileSync(TEMP_PATH + '/test_storage.json'); - assertEquals(content, originalContent); + assertEquals(content, originalContent); + } }); -Deno.test(`${cyan('[reader.ts]')} Read Sync (Path doesn't exists)`, () => { - const content = Reader.readSync(TEMP_PATH + '/created/storage_sync.json'); - assertEquals(content, '[]'); +Deno.test({ + name: `${cyan('[reader.ts]')} Read Sync (Path doesn't exists)`, + sanitizeResources: false, + sanitizeOps: false, + + fn() { + const content = Reader.readSync(TEMP_PATH + '/created/storage_sync.json'); + assertEquals(content, '[]'); - const fileExists = existsSync(TEMP_PATH + '/created/storage_sync.json'); - assertEquals(fileExists, true); + const fileExists = existsSync(TEMP_PATH + '/created/storage_sync.json'); + assertEquals(fileExists, true); - const fileContent = Deno.readTextFileSync(TEMP_PATH + '/created/storage_sync.json'); - assertEquals(fileContent, '[]'); + const fileContent = Deno.readTextFileSync(TEMP_PATH + '/created/storage_sync.json'); + assertEquals(fileContent, '[]'); + } }); -Deno.test(`${cyan('[reader.ts]')} Read Sync (Invalid path)`, () => { - assertThrows(() => Reader.readSync(TEMP_PATH)); // Its directory, not a file +Deno.test({ + name: `${cyan('[reader.ts]')} Read Sync (Invalid path)`, + sanitizeResources: false, + sanitizeOps: false, + + fn() { + assertThrows(() => Reader.readSync(TEMP_PATH)); // Its directory, not a file + } }); +Deno.test({ + name: `${cyan('[reader.ts]')} Read (Basic)`, + sanitizeResources: false, + sanitizeOps: false, -Deno.test(`${cyan('[reader.ts]')} Read (Basic)`, async () => { - const content = await Reader.read(TEMP_PATH + '/test_storage.json'); - const originalContent = await Deno.readTextFile(TEMP_PATH + '/test_storage.json'); + async fn() { + const content = await Reader.read(TEMP_PATH + '/test_storage.json'); + const originalContent = await Deno.readTextFile(TEMP_PATH + '/test_storage.json'); - assertEquals(content, originalContent); + assertEquals(content, originalContent); + } }); -Deno.test(`${cyan('[reader.ts]')} Read (Path doesn't exists)`, async () => { - const content = await Reader.read(TEMP_PATH + '/created/storage_async.json'); - assertEquals(content, '[]'); +Deno.test({ + name: `${cyan('[reader.ts]')} Read (Path doesn't exists)`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + const content = await Reader.read(TEMP_PATH + '/created/storage_async.json'); + assertEquals(content, '[]'); - const fileExists = await exists(TEMP_PATH + '/created/storage_async.json'); - assertEquals(fileExists, true); + const fileExists = await exists(TEMP_PATH + '/created/storage_async.json'); + assertEquals(fileExists, true); - const fileContent = await Deno.readTextFile(TEMP_PATH + '/created/storage_async.json'); - assertEquals(fileContent, '[]'); + const fileContent = await Deno.readTextFile(TEMP_PATH + '/created/storage_async.json'); + assertEquals(fileContent, '[]'); + } }); -Deno.test(`${cyan('[reader.ts]')} Read (Invalid path)`, async () => { - await assertThrowsAsync(async () => await Reader.read(TEMP_PATH)); // Its directory, not a file +Deno.test({ + name: `${cyan('[reader.ts]')} Read (Invalid path)`, + sanitizeResources: false, + sanitizeOps: false, + + async fn() { + await assertThrowsAsync(async () => await Reader.read(TEMP_PATH)); // Its directory, not a file + } }); // Remove temp files -window.addEventListener('unload', async () => { - await Deno.remove(TEMP_PATH, { recursive: true }); +window.addEventListener('unload', () => { + Deno.removeSync(TEMP_PATH, { recursive: true }); }); diff --git a/tests/utils_test.ts b/tests/utils_test.ts index 80f2bab..cafa81b 100644 --- a/tests/utils_test.ts +++ b/tests/utils_test.ts @@ -20,8 +20,7 @@ import { isFunction, isArray, isObject, - isRegExp, - isError, + isRegExp } from '../lib/utils.ts'; Deno.test(`${green('[utils.ts]')} cleanArray`, () => { @@ -254,10 +253,3 @@ Deno.test(`${green('[utils.ts]')} isRegExp`, () => { assertEquals(isRegExp(new Date()), false); assertEquals(isRegExp({}), false); }); - -Deno.test(`${green('[utils.ts]')} isError`, () => { - assertEquals(isError(new TypeError('foo')), true); - assertEquals(isError(new Error('foo')), true); - assertEquals(isError(345345), false); - assertEquals(isError({}), false); -}); diff --git a/tests/writer_test.ts b/tests/writer_test.ts index d88e966..f9fcdb0 100644 --- a/tests/writer_test.ts +++ b/tests/writer_test.ts @@ -2,49 +2,52 @@ import { assertEquals } from 'https://deno.land/std/testing/asserts.ts'; import { ensureDir, emptyDir } from 'https://deno.land/std/fs/mod.ts'; import { delay } from 'https://deno.land/std/async/mod.ts'; -import { blue } from 'https://deno.land/std/fmt/colors.ts'; -import * as path from 'https://deno.land/std/path/mod.ts'; +import { red } from 'https://deno.land/std/fmt/colors.ts'; +import { dirname, fromFileUrl } from 'https://deno.land/std/path/mod.ts'; import { Writer } from '../lib/writer.ts'; -// Prepare inviroment -const DIRNAME = path.dirname(path.fromFileUrl(import.meta.url)); -const TEMP_PATH = DIRNAME + '/temp_writer'; +// Prepare enviroment +const DIRNAME = dirname(fromFileUrl(import.meta.url)); +const TEMP_PATH = DIRNAME + '/temp_writer_tests_enviroment'; await ensureDir(TEMP_PATH); -await emptyDir(TEMP_PATH) +await emptyDir(TEMP_PATH); -Deno.test(`${blue('[writer.ts]')} Write (Basic)`, async () => { - const writer = new Writer(TEMP_PATH + '/test_storage_1.json'); +Deno.test({ + name: `${red('[writer.ts]')} Write`, + sanitizeResources: false, + sanitizeOps: false, - await writer.write('[{"foo":"bar"}]'); + async fn() { + const writer = new Writer(TEMP_PATH + '/test_storage_1.json'); - const content = await Deno.readTextFile(TEMP_PATH + '/test_storage_1.json'); - assertEquals(content, '[{"foo":"bar"}]'); -}); - -Deno.test(`${blue('[writer.ts]')} Write (Internal logic validation)`, async () => { - const writer = new Writer(TEMP_PATH + '/test_storage_2.json'); + await writer.write('[{"foo":"bar"}]'); - writer.write('[{"foo":"old"}]'); - - assertEquals((writer as any).next, null); - assertEquals((writer as any).locked, true); + const content = await Deno.readTextFile(TEMP_PATH + '/test_storage_1.json'); + assertEquals(content, '[{"foo":"bar"}]'); + } +}); - writer.write('[{"foo":"new"}]'); +Deno.test({ + name: `${red('[writer.ts]')} Add`, + sanitizeResources: false, + sanitizeOps: false, - await delay(1); + async fn() { + const writer = new Writer(TEMP_PATH + '/test_storage_2.json'); - assertEquals((writer as any).next, '[{"foo":"new"}]'); - assertEquals((writer as any).locked, true); + writer.add('[{"foo":"old"}]'); + writer.add('[{"foo":"new"}]'); - await delay(10); + await delay(100); - const content = await Deno.readTextFile(TEMP_PATH + '/test_storage_2.json'); - assertEquals(content, '[{"foo":"new"}]'); + const content = await Deno.readTextFile(TEMP_PATH + '/test_storage_2.json'); + assertEquals(content, '[{"foo":"new"}]'); + } }); // Remove temp files -window.addEventListener('unload', async () => { - await Deno.remove(TEMP_PATH, { recursive: true }); +window.addEventListener('unload', () => { + Deno.removeSync(TEMP_PATH, { recursive: true }); });