diff --git a/README.md b/README.md
index c28c62c..3832c02 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,10 @@
-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 });
});