├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── README.md └── todo │ ├── README.md │ ├── main.ts │ ├── preview.png │ ├── static │ ├── code.js │ ├── index.html │ └── style.css │ └── tasks.json ├── lib ├── core.ts ├── database.ts ├── helpers.ts ├── reader.ts ├── types.ts ├── utils.ts └── writer.ts ├── mod.ts ├── other ├── head.png └── logo.afdesign └── tests ├── benchmark ├── benchmark.ts └── utils.ts ├── core_test.ts ├── database_test.ts ├── enviroment └── test_storage.json ├── helpers_test.ts ├── reader_test.ts ├── utils_test.ts └── writer_test.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = 4 9 | end_of_line = crlf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE configs 2 | .idea/ 3 | .vscode/ 4 | 5 | # Temporary enviroment files for testing 6 | temp/ 7 | 8 | # Scripts 9 | *.bat 10 | *.cmd 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kirill Reunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | AloeDB Logo 3 |

4 | 5 |

6 |

AloeDB

7 |

Light, Embeddable, NoSQL database for Deno

8 |

9 | 10 |
11 | 12 | ## ✨ Features 13 | * 🎉 Simple to use API, similar to [MongoDB](https://www.mongodb.com/)! 14 | * 🚀 Optimized for a large number of operations. 15 | * ⚖ No dependencies, even without [std](https://deno.land/std)! 16 | * 📁 Stores data in readable JSON file. 17 | 18 |
19 | 20 | ## 📦 Importing 21 | ```typescript 22 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts' 23 | ``` 24 | 25 |
26 | 27 | ## 📖 Example 28 | ```typescript 29 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 30 | 31 | // Structure of stored documents 32 | interface Film { 33 | title: string; 34 | year: number; 35 | film: boolean; 36 | genres: string[]; 37 | authors: { director: string }; 38 | } 39 | 40 | // Initialization 41 | const db = new Database('./path/to/the/file.json'); 42 | 43 | // Insert operations 44 | await db.insertOne({ 45 | title: 'Drive', 46 | year: 2012, 47 | film: true, 48 | genres: ['crime', 'drama', 'noir'], 49 | authors: { director: 'Nicolas Winding Refn' } 50 | }); 51 | 52 | // Search operations 53 | const found = await db.findOne({ title: 'Drive', film: true }); 54 | 55 | // Update operations 56 | await db.updateOne({ title: 'Drive' }, { year: 2011 }); 57 | 58 | // Delete operations 59 | await db.deleteOne({ title: 'Drive' }); 60 | ``` 61 | _P.S. You can find more examples [here](https://github.com/Kirlovon/AloeDB/tree/master/examples)!_ 62 | 63 | 64 |
65 | 66 | ## 🏃‍ Benchmarks 67 | 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. 68 | 69 | To give you an example, here is the speed of a database operations with *1000* documents: 70 | 71 | | Insertion | Searching | Updating | Deleting | 72 | | ------------- | ------------- | ------------- | ------------- | 73 | | 15k _ops/sec_ | 65k _ops/sec_ | 8k _ops/sec_ | 10k _ops/sec_ | 74 | 75 |
76 | 77 | ## 📚 Guide 78 | 79 | ### Initialization 80 | ```typescript 81 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 82 | 83 | interface Schema { 84 | username: string; 85 | password: string; 86 | } 87 | 88 | const db = new Database({ 89 | path: './data.json', 90 | pretty: true, 91 | autoload: true, 92 | autosave: true, 93 | optimize: true, 94 | immutable: true, 95 | validator: (document: any) => {} 96 | }); 97 | ``` 98 | The following fields are available for configuration: 99 | * `path` - Path to the database file. If undefined, data will be stored only in-memory. _(Default: undefined)_ 100 | * `pretty` - Save data in easy-to-read format. _(Default: true)_ 101 | * `autoload` - Automatically load the file synchronously when initializing the database. _(Default: true)_ 102 | * `autosave` - Automatically save data to the file after inserting, updating and deleting documents. _(Default: true)_ 103 | * `optimize` - Optimize data writing. If enabled, the data will be written many times faster in case of a large number of operations. _(Default: true)_ 104 | * `immutable` - Automatically deeply clone all returned objects. _(Default: true)_ 105 | * `validator` - Runtime documents validation function. If the document does not pass the validation, just throw the error. 106 | 107 | Also, you can initialize the database in the following ways: 108 | ```typescript 109 | // In-memory database 110 | const db = new Database(); 111 | ``` 112 | 113 | ```typescript 114 | // Short notation, specifying the file path only 115 | const db = new Database('./path/to/the/file.json'); 116 | ``` 117 | 118 |
119 | 120 | ### Typization 121 | AloeDB allows you to specify the schema of documents. 122 | By doing this, you will get auto-completion and types validation. This is a **completely optional** feature that can make it easier for you to work with the database. 123 | 124 | Just specify an interface that contains only the types supported by the database _(strings, numbers, booleans, nulls, array, objects)_, and everything will works like magic! 🧙‍ 125 | 126 | ```typescript 127 | // Your schema 128 | interface User { 129 | username: string; 130 | password: string; 131 | } 132 | 133 | // Initialize a database with a specific schema 134 | const db = new Database(); 135 | 136 | await db.insertOne({ username: 'bob', password: 'qwerty' }); // Ok 👌 137 | await db.insertOne({ username: 'greg' }); // Error: Property 'password' is missing 138 | ``` 139 | 140 |
141 | 142 | ### Inserting 143 | AloeDB is a document-oriented database, so you storing objects in it. The supported types are **Strings**, **Numbers**, **Booleans**, **Nulls**, **Arrays** & **Objects**. 144 | 145 | Keep in mind that data types such as **Date**, **Map**, **Set** and other complex types are not supported, and all fields with them will be deleted. Also, any blank documents will not be inserted. 146 | 147 | ```typescript 148 | const inserted = await db.insertOne({ text: 'Hey hey, im inserted!' }); 149 | console.log(inserted); // { text: 'Hey hey, im inserted!' } 150 | ``` 151 | 152 |
153 | 154 | ### Querying 155 | Search query can be an object or a search function. If query is an object, then the search will be done by deeply comparing the fields values in the query with the fields values in the documents. 156 | 157 | In search queries you can use **Primitives** _(strings, numbers, booleans, nulls)_, **Arrays**, **Objects**, **RegExps** and **Functions**. 158 | 159 | ```typescript 160 | await db.insertMany([ 161 | { key: 1, value: 'one' }, 162 | { key: 2, value: 'two' }, 163 | { key: 3, value: 'three' }, 164 | ]); 165 | 166 | // Simple query 167 | const found1 = await db.findOne({ key: 1 }); 168 | console.log(found1); // { key: 1, value: 'one' } 169 | 170 | // Advanced query with search function 171 | const found2 = await db.findOne((document: any) => document.key === 2); 172 | console.log(found2); // { key: 2, value: 'two' } 173 | ``` 174 | 175 | When specifying **Arrays** or **Objects**, a deep comparison will be performed. 176 | ```typescript 177 | await db.insertMany([ 178 | { key: 1, values: [1, 2] }, 179 | { key: 2, values: [1, 2, 3] }, 180 | { key: 3, values: [1, 2, 3, 4] }, 181 | ]); 182 | 183 | const found = await db.findOne({ values: [1, 2, 3] }); 184 | console.log(found); // { key: 2, values: [1, 2, 3] } 185 | ``` 186 | 187 |
188 | 189 | ### Updating 190 | As with search queries, update queries can be either a function or an object. If this is a function, then the function receives the document to update as a parameter, and you must return updated document from the function. _(or return `null` or `{}` to delete it)_ 191 | 192 | By the way, you can pass a function as a parameter value in an object. This can be useful if you want to update a specific field in your document. Also, you can return `undefined`, to remove this field. 193 | 194 | ```typescript 195 | await db.insertMany([ 196 | { key: 1, value: 'one' }, 197 | { key: 2, value: 'two' }, 198 | { key: 3, value: 'three' }, 199 | ]); 200 | 201 | // Simple update 202 | const updated1 = await db.updateOne({ key: 1 }, { key: 4, value: 'four' }); 203 | console.log(updated1); // { key: 4, value: 'four' } 204 | 205 | // Advanced update with update function 206 | const updated2 = await db.updateOne({ key: 2 }, (document: any) => { 207 | document.key = 5; 208 | document.value = 'five'; 209 | return document; 210 | }); 211 | console.log(updated2); // { key: 5, value: 'five' } 212 | 213 | // Advanced update with field update function 214 | const updated3 = await db.updateOne({ key: 3 }, { 215 | key: (value: any) => value === 6, 216 | value: (value: any) => value === 'six' 217 | }); 218 | console.log(updated3); // { key: 6, value: 'six' } 219 | ``` 220 | 221 |
222 | 223 | ## 🔧 Methods 224 | 225 | ### Documents 226 | ```typescript 227 | db.documents; 228 | ``` 229 | This property stores all your documents. It is better not to modify these property manually, as database methods do a bunch of checks for security and stability reasons. But, if you do this, be sure to call `await db.save()` method after your changes. 230 | 231 |
232 | 233 | ### InsertOne 234 | ```typescript 235 | await db.insertOne({ foo: 'bar' }); 236 | ``` 237 | Inserts a document into the database. After insertion, it returns the inserted document. 238 | 239 | All fields with `undefined` values will be deleted. Empty documents will not be inserted. 240 | 241 | By default, the document will be inserted as the last entry in the database. To insert it somewhere else, specify the optional `index` parameter: 242 | 243 | ```typescript 244 | await db.insertOne({ foo: 'bar' }, 9); 245 | ``` 246 | 247 | If the provided `index` is greater than the number of database entries, it will be inserted at the end. 248 | 249 |
250 | 251 | ### InsertMany 252 | ```typescript 253 | await db.insertMany([{ foo: 'bar' }, { foo: 'baz' }]); 254 | ``` 255 | Inserts multiple documents into the database. After insertion, it returns the array with inserted documents. 256 | 257 | This operation is **atomic**, so if something goes wrong, nothing will be inserted. 258 | 259 |
260 | 261 | ### FindOne 262 | ```typescript 263 | await db.findOne({ foo: 'bar' }); 264 | ``` 265 | Returns a document that matches the search query. Returns `null` if nothing found. 266 | 267 |
268 | 269 | ### FindMany 270 | ```typescript 271 | await db.findMany({ foo: 'bar' }); 272 | ``` 273 | Returns an array of documents matching the search query. 274 | 275 |
276 | 277 | ### UpdateOne 278 | ```typescript 279 | await db.updateOne({ foo: 'bar' }, { foo: 'baz' }); 280 | ``` 281 | Modifies an existing document that match search query. Returns the found document with applied modifications. If nothing is found, it will return `null`. 282 | 283 | The document will be deleted if all of its fields are `undefined`, or if you return `null` or `{}` using a update function. 284 | 285 | This operation is **atomic**, so if something goes wrong, nothing will be updated. 286 | 287 |
288 | 289 | ### UpdateMany 290 | ```typescript 291 | await db.updateMany({ foo: 'bar' }, { foo: 'baz' }); 292 | ``` 293 | Modifies all documents that match search query. Returns an array with updated documents. 294 | 295 | This operation is **atomic**, so if something goes wrong, nothing will be updated. 296 | 297 |
298 | 299 | ### DeleteOne 300 | ```typescript 301 | await db.deleteOne({ foo: 'bar' }); 302 | ``` 303 | Deletes first found document that matches the search query. After deletion, it will return deleted document. 304 | 305 |
306 | 307 | ### DeleteMany 308 | ```typescript 309 | await db.deleteMany({ foo: 'bar' }); 310 | ``` 311 | Deletes all documents that matches the search query. After deletion, it will return all deleted documents. 312 | 313 | This operation is **atomic**, so if something goes wrong, nothing will be deleted. 314 | 315 |
316 | 317 | ### Count 318 | ```typescript 319 | await db.count({ foo: 'bar' }); 320 | ``` 321 | Returns the number of documents found by the search query. If the query is not specified or empty, it will return total number of documents in the database. 322 | 323 |
324 | 325 | ### Drop 326 | ```typescript 327 | await db.drop(); 328 | ``` 329 | Removes all documents from the database. 330 | 331 |
332 | 333 | ### Load 334 | ```typescript 335 | await db.load(); 336 | ``` 337 | Loads, parses and validates documents from the specified database file. If the file is not specified, then nothing will be done. 338 | 339 |
340 | 341 | ### LoadSync 342 | ```typescript 343 | db.loadSync(); 344 | ``` 345 | Same as `db.load()` method, but synchronous. Will be called automatically if the `autoload` parameter is set to **true**. 346 | 347 |
348 | 349 | ### Save 350 | ```typescript 351 | await db.save(); 352 | ``` 353 | Saves documents from memory to a database file. If the `optimize` parameter is **false**, then the method execution will be completed when data writing is completely finished. Otherwise the data record will be added to the queue and executed later. 354 | 355 |
356 | 357 | ### Helpers 358 | This module contains helper functions that will make it easier to write and read search queries. 359 | 360 | ```typescript 361 | // Importing database & helpers 362 | import { Database, and, includes, length, not, exists } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 363 | 364 | const db = new Database(); 365 | await db.insertOne({ test: [1, 2, 3] }); 366 | 367 | // Helpers usage 368 | const found = await db.findOne({ 369 | test: and( 370 | length(3), 371 | includes(2) 372 | ), 373 | other: not(exists()) 374 | }); 375 | 376 | console.log(found); // { test: [1, 2, 3] } 377 | ``` 378 | 379 | #### List of all available helpers: 380 | * moreThan 381 | * moreThanOrEqual 382 | * lessThan 383 | * lessThanOrEqual 384 | * between 385 | * betweenOrEqual 386 | * exists 387 | * type 388 | * includes 389 | * length 390 | * someElementMatch 391 | * everyElementMatch 392 | * and 393 | * or 394 | * not 395 | 396 |
397 | 398 | ## 💡 Tips & Tricks 399 | 400 | ### Multiple Collections 401 | 402 | By default, one database instance has only one collection. However, since the database instances are quite lightweight, you can initialize multiple instances for each collection. 403 | 404 | Keep in mind that you **cannot specify the same file for multiple instances!** 405 | 406 | ```typescript 407 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 408 | 409 | // Initialize database instances 410 | const users = new Database({ path: './users.json' }); 411 | const posts = new Database({ path: './posts.json' }); 412 | const comments = new Database({ path: './comments.json' }); 413 | 414 | // For convenience, you can collect all instances into one object 415 | const db = { users, posts, comments }; 416 | 417 | // Looks nice 😎 418 | await db.users.insertOne({ username: 'john', password: 'qwerty123' }); 419 | ``` 420 | 421 |
422 | 423 | ### Runtime Validation 424 | 425 | You cannot always be sure about the data that comes to your server. TypeScript highlights a lot of errors at compilation time, but it doesn't help at runtime. 426 | 427 | Luckily, you can use a library such as [SuperStruct](https://github.com/ianstormtaylor/superstruct), which allows you to check your documents structure: 428 | 429 | ```typescript 430 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 431 | import { assert, object, string, Infer } from 'https://cdn.skypack.dev/superstruct?dts'; 432 | 433 | // Specify structure 434 | const User = object({ 435 | username: string(), 436 | password: string() 437 | }); 438 | 439 | // Create validation function 440 | const UserValidator = (document: any) => assert(document, User); 441 | 442 | // Convert structure to TypeScript type 443 | type UserSchema = Infer; 444 | 445 | // Initialize 446 | const db = new Database({ validator: UserValidator }); 447 | 448 | // Ok 👌 449 | await db.insertOne({ username: 'bob', password: 'dylan' }); 450 | 451 | // StructError: At path: password -- Expected a string, but received: null 452 | await db.insertOne({ username: 'bob', password: null as any }); 453 | ``` 454 | 455 |
456 | 457 | ### Manual Changes 458 | 459 | For performance reasons, a copy of the whole storage is kept in memory. Knowing this, you can modify the documents manually by modifying the `db.documents` parameter. 460 | 461 | Most of the time this is not necessary, as the built-in methods are sufficient, but if you want to have full control, you can do it! 462 | 463 | Keep in mind that after your changes, **you should always call the `await db.save()` method!** 464 | 465 | ```typescript 466 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 467 | 468 | // Initialize 469 | const db = new Database('./data.json'); 470 | 471 | try { 472 | 473 | // Your changes... 474 | db.documents.push({ foo: 'bar' }); 475 | 476 | } finally { 477 | await db.save(); // ALWAYS CALL SAVE! 478 | } 479 | ``` 480 | 481 | Also, if you set the parameter **immutable** to `false` when initializing the database, you will get back references to in-memory documents instead of their copies. This means that you cannot change the returned documents without calling the `await db.save()` method. 482 | 483 | ```typescript 484 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 485 | 486 | // Initialization with immutability disabled 487 | const db = new Database({ path: './data.json', immutable: false }); 488 | 489 | // Initial data 490 | await db.insertOne({ field: 'The Days' }); 491 | 492 | // Finding and modifying document 493 | const found = await db.findOne({ field: 'The Days' }) as { field: string }; 494 | found.field = 'The Nights'; 495 | 496 | // Saving 497 | await db.save(); 498 | 499 | console.log(db.documents); // [{ field: 'The Nights' }] 500 | ``` 501 | 502 |
503 | 504 | ## 🦄 Community Ports 505 | Surprisingly, this library was ported to other programming languages without my participation. **Much appreciation to this guys for their work!** ❤ 506 | 507 | 🔵 **[AlgoeDB](https://github.com/billykirk01/AlgoeDB)** - database for Go, made by [billykirk01](https://github.com/billykirk01)! 508 | 509 | 🟠 **[AlroeDB](https://github.com/billykirk01/AlroeDB)** - database for Rust, also made by [billykirk01](https://github.com/billykirk01)! 510 | 511 | 🟢 **[AloeDB-Node](https://github.com/wouterdebruijn/AloeDB-Node)** - port to the Node.js, made by [Wouter de Bruijn](https://github.com/wouterdebruijn)! _(With awesome Active Records example)_ 512 | 513 |
514 | 515 | ## 📃 License 516 | MIT _(see [LICENSE](https://github.com/Kirlovon/AloeDB/blob/master/LICENSE) file)_ 517 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 |

2 | AloeDB Logo 3 |

4 | 5 | # AloeDB Examples 6 | 7 | Examples of using [AloeDB](https://github.com/Kirlovon/AloeDB) on small projects. Can be handy for developers who are more comfortable reading code instead of documentation. 8 | 9 |
10 | 11 | ### Examples: 12 | 13 | * [ToDo List](https://github.com/Kirlovon/AloeDB/tree/master/examples/todo) - to do list made with AloeDB, Oak, NanoID & Superstruct! 14 | 15 |
16 | 17 | 18 | ## License 19 | MIT _(see [LICENSE](https://github.com/Kirlovon/AloeDB/blob/master/LICENSE) file)_ 20 | -------------------------------------------------------------------------------- /examples/todo/README.md: -------------------------------------------------------------------------------- 1 | ![ToDo List Preview](https://github.com/Kirlovon/AloeDB/raw/master/examples/todo/preview.png) 2 | 3 |
4 | 5 | # ToDo List 6 | Simple to do list application, written with [Deno](https://deno.land/). The client part is written in pure JavaScript, and uses fetch to communicate with the server. 7 | 8 | Powered by [AloeDB](https://github.com/Kirlovon/AloeDB), [Oak](https://github.com/oakserver/oak), [NanoID](https://github.com/ai/nanoid) & [Superstruct](https://github.com/ianstormtaylor/superstruct)! Also, uses path module from [std](https://deno.land/std@0.103.0/). 9 | 10 |
11 | 12 | ## Running 13 | To start the web server, run this command in the cloned repository: 14 | 15 | ```console 16 | deno run --allow-read --allow-write --allow-net main.ts 17 | ``` 18 | 19 | Application will be available at **localhost:3000**! 20 | 21 | Used Deno version: **1.11.5** _(Should work on any version higher than this one)_ 22 | 23 |
24 | 25 | ## License 26 | MIT _(see [LICENSE](https://github.com/Kirlovon/AloeDB/blob/master/LICENSE) file)_ 27 | -------------------------------------------------------------------------------- /examples/todo/main.ts: -------------------------------------------------------------------------------- 1 | import { Database } from 'https://deno.land/x/aloedb@0.9.0/mod.ts'; 2 | import { Application, Router, send } from 'https://deno.land/x/oak@v8.0.0/mod.ts'; 3 | import { nanoid } from 'https://deno.land/x/nanoid@v3.0.0/mod.ts'; 4 | import { assert, object, string, boolean, Infer } from 'https://cdn.skypack.dev/superstruct?dts'; 5 | import { dirname, fromFileUrl } from 'https://deno.land/std@0.103.0/path/mod.ts'; 6 | 7 | // Get parent directory of main.ts 8 | const DIRNAME = dirname(fromFileUrl(import.meta.url)); 9 | 10 | // Specify Superstruct structure 11 | const TaskStructure = object({ 12 | id: string(), 13 | text: string(), 14 | done: boolean() 15 | }); 16 | 17 | // Create validation function 18 | const TaskValidator = (document: any) => assert(document, TaskStructure); 19 | 20 | // Convert structure to TypeScript type 21 | type Task = Infer; 22 | 23 | // Initialize database 24 | const db = new Database({ 25 | path: `${DIRNAME}/tasks.json`, 26 | validator: TaskValidator, 27 | pretty: true 28 | }); 29 | 30 | // Router setup 31 | const router = new Router(); 32 | 33 | // Add simple logger 34 | router.use(async (context, next) => { 35 | console.log('[SERVER]', `${context.request.method} ${context.request.url}`); 36 | await next(); 37 | }); 38 | 39 | // Get all tasks 40 | router.get('/tasks', async (context) => { 41 | const tasks = await db.findMany(); 42 | context.response.type = 'json'; 43 | context.response.body = tasks; 44 | }); 45 | 46 | // Add new task 47 | router.post('/tasks', async (context) => { 48 | const body = await context.request.body().value; 49 | 50 | await db.insertOne({ 51 | id: nanoid(), 52 | text: body.text, 53 | done: false, 54 | }); 55 | 56 | const tasks = await db.findMany(); 57 | context.response.type = 'json'; 58 | context.response.body = tasks; 59 | }); 60 | 61 | // Toggle task 62 | router.put('/tasks/:id', async (context) => { 63 | 64 | await db.updateOne( 65 | { id: context.params.id }, 66 | { done: (value) => !value } 67 | ); 68 | 69 | const tasks = await db.findMany(); 70 | context.response.type = 'json'; 71 | context.response.body = tasks; 72 | }); 73 | 74 | // Delete task 75 | router.delete('/tasks/:id', async (context) => { 76 | 77 | await db.deleteOne({ id: context.params.id }); 78 | 79 | const tasks = await db.findMany(); 80 | context.response.type = 'json'; 81 | context.response.body = tasks; 82 | }); 83 | 84 | // Setup Oak and router 85 | const app = new Application(); 86 | app.use(router.routes()); 87 | app.use(router.allowedMethods()); 88 | 89 | // Static content 90 | app.use(async (context) => { 91 | await send(context, context.request.url.pathname, { 92 | root: `${DIRNAME}/static`, 93 | index: 'index.html', 94 | }); 95 | }); 96 | 97 | // Log about server start 98 | app.addEventListener('listen', ({ hostname, port, secure }) => { 99 | console.log('[SERVER]', `Server started on: ${secure ? 'https://' : 'http://'}${hostname ?? 'localhost'}:${port}`); 100 | }); 101 | 102 | // Start webserver 103 | await app.listen({ port: 3000 }); 104 | -------------------------------------------------------------------------------- /examples/todo/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirlovon/aloedb/849b3260bbca9d45d3bd971c26ef7bb9a5b7bf2a/examples/todo/preview.png -------------------------------------------------------------------------------- /examples/todo/static/code.js: -------------------------------------------------------------------------------- 1 | // Initializtion 2 | document.addEventListener('DOMContentLoaded', async () => { 3 | const response = await fetch('/tasks'); 4 | const data = await response.json(); 5 | render(data); 6 | }); 7 | 8 | /** 9 | * Render todo list tasks 10 | * @param {{ id: string, text: string, done: boolean }[]} data Array with tasks 11 | */ 12 | function render(data) { 13 | const list = document.querySelector('#list'); 14 | 15 | // Remove old DOM content 16 | list.innerHTML = ''; 17 | 18 | // If list is empty 19 | if (data.length === 0) { 20 | const nothingElement = document.createElement('h2'); 21 | nothingElement.innerText = 'There\'s no tasks here! 😎'; 22 | 23 | list.appendChild(nothingElement); 24 | return; 25 | } 26 | 27 | // Add each task to the list element 28 | data.forEach((task) => { 29 | const taskElement = document.createElement('li'); 30 | taskElement.className = task.done ? 'task done' : 'task'; 31 | taskElement.innerText = task.text; 32 | 33 | // Toggle task on click 34 | taskElement.addEventListener('click', async () => { 35 | const response = await fetch(`/tasks/${task.id}`, { method: 'PUT' }); 36 | const data = await response.json(); 37 | render(data); 38 | }); 39 | 40 | // Delete task 41 | taskElement.addEventListener('dblclick', async () => { 42 | const response = await fetch(`/tasks/${task.id}`, { method: 'DELETE' }); 43 | const data = await response.json(); 44 | render(data); 45 | }); 46 | 47 | list.appendChild(taskElement); 48 | }); 49 | 50 | } 51 | 52 | // Add new task 53 | document.querySelector('#add').addEventListener('click', async () => { 54 | const textElement = document.querySelector('#text'); 55 | const text = textElement.value.trim(); 56 | 57 | // Skip if input is empty 58 | if (text === '') return; 59 | 60 | // Remove old text 61 | textElement.value = ''; 62 | 63 | // Send task to the server 64 | const response = await fetch('/tasks', { 65 | method: 'POST', 66 | headers: { 67 | 'Accept': 'application/json', 68 | 'Content-Type': 'application/json' 69 | }, 70 | body: JSON.stringify({ text: text }) 71 | }); 72 | 73 | // Render response 74 | const data = await response.json(); 75 | render(data); 76 | }); 77 | -------------------------------------------------------------------------------- /examples/todo/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ToDo List 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

ToDo List

16 | Made with AloeDB, Oak, NanoID & Superstruct 17 | 18 |
    19 | Loading... 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 | Click on the task to mark it as done. Double-click to delete it. 28 |
29 | 30 |
Made by Kirlovon as an example of using AloeDB
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/todo/static/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | width: 100%; 9 | min-height: 100vh; 10 | overflow-x: hidden; 11 | font-family: monospace; 12 | background-color: #ff573a; 13 | } 14 | 15 | h1 { 16 | font-size: 24px; 17 | } 18 | 19 | main { 20 | position: absolute; 21 | top: 10vh; 22 | left: 50%; 23 | width: 100%; 24 | padding: 43px; 25 | height: auto; 26 | min-height: 128px; 27 | max-width: 400px; 28 | margin-bottom: 64px; 29 | background-color: white; 30 | transform: translateX(-50%); 31 | box-shadow: 12px 12px 0 black; 32 | } 33 | 34 | #list { 35 | width: 100%; 36 | display: flex; 37 | margin-top: 32px; 38 | margin-bottom: 32px; 39 | flex-direction: column; 40 | } 41 | 42 | .task { 43 | cursor: pointer; 44 | width: 100%; 45 | font-size: 14px; 46 | user-select: none; 47 | margin-bottom: 6px; 48 | padding-bottom: 6px; 49 | list-style-position: inside; 50 | border-bottom: 1px dashed black; 51 | } 52 | 53 | .task:last-child { 54 | border-bottom: none; 55 | } 56 | 57 | .task.done { 58 | color: #808080; 59 | text-decoration: line-through; 60 | } 61 | 62 | .inputs { 63 | width: 100%; 64 | display: flex; 65 | margin-bottom: 4px; 66 | flex-direction: row; 67 | } 68 | 69 | .info { 70 | width: 100%; 71 | font-size: 8px; 72 | text-align: center; 73 | display: block; 74 | } 75 | 76 | #add:focus, 77 | #text:focus { 78 | outline: none; 79 | } 80 | 81 | #text { 82 | display: block; 83 | width: 100%; 84 | font-size: 10px; 85 | border-radius: 0; 86 | padding: 4px 8px; 87 | margin-right: 4px; 88 | border: 1px dashed black; 89 | } 90 | 91 | #add { 92 | display: block; 93 | width: 92px; 94 | border: none; 95 | color: white; 96 | cursor: pointer; 97 | font-size: 10px; 98 | border-radius: 0; 99 | padding: 4px 8px; 100 | font-weight: bold; 101 | background-color: black; 102 | } 103 | 104 | #add:hover { 105 | background-color: #ff573a; 106 | } 107 | 108 | .credits { 109 | left: 16px; 110 | bottom: 16px; 111 | font-size: 11px; 112 | position: fixed; 113 | font-style: italic; 114 | } 115 | -------------------------------------------------------------------------------- /examples/todo/tasks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1eT30aviFuV3hIWgoekZW", 4 | "text": "Eat pizza 🍕", 5 | "done": false 6 | }, 7 | { 8 | "id": "WeJ69TBQLnblJLqT1kItc", 9 | "text": "Make coffie ☕", 10 | "done": false 11 | }, 12 | { 13 | "id": "_g_BBDNr3kmA7ucFBXDZx", 14 | "text": "Feed a cat 🐈", 15 | "done": false 16 | }, 17 | { 18 | "id": "0iCzpv60u5BO8W0_jAi9A", 19 | "text": "Take a rest 🛌", 20 | "done": true 21 | } 22 | ] -------------------------------------------------------------------------------- /lib/core.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | import { 4 | Document, 5 | DocumentValue, 6 | Query, 7 | QueryValue, 8 | QueryFunction, 9 | Update, 10 | UpdateValue, 11 | UpdateFunction, 12 | } from './types.ts'; 13 | 14 | import { 15 | cleanArray, 16 | deepClone, 17 | deepCompare, 18 | isPrimitive, 19 | isArray, 20 | isFunction, 21 | isObject, 22 | isObjectEmpty, 23 | isRegExp, 24 | isString, 25 | isUndefined, 26 | numbersList, 27 | prepareObject, 28 | } from './utils.ts'; 29 | 30 | /** 31 | * Find one document. 32 | * @param query Document selection criteria. 33 | * @param documents Array of documents to search. 34 | * @returns Found document index. 35 | */ 36 | export function findOneDocument(query: Query | QueryFunction | undefined, documents: T[]): number | null { 37 | if (isFunction(query)) { 38 | for (let i = 0; i < documents.length; i++) { 39 | const document = documents[i]; 40 | const isMatched = query(document); 41 | if (isMatched) return i; 42 | } 43 | 44 | return null; 45 | } 46 | 47 | if (isUndefined(query) || isObjectEmpty(query)) { 48 | return documents.length > 0 ? 0 : null; 49 | } 50 | 51 | for (let i = 0; i < documents.length; i++) { 52 | const document = documents[i]; 53 | let suitable = true; 54 | 55 | for (const key in query) { 56 | const documentValue = document[key]; 57 | const queryValue = query[key]; 58 | const isMatched = matchValues(queryValue as QueryValue, documentValue); 59 | if (isMatched) continue; 60 | 61 | suitable = false; 62 | break; 63 | } 64 | 65 | if (suitable) return i; 66 | } 67 | 68 | return null; 69 | } 70 | 71 | /** 72 | * Find multiple documents. 73 | * @param query Documents selection criteria. 74 | * @param documents Array of documents to search. 75 | * @returns Found documents indexes. 76 | */ 77 | export function findMultipleDocuments(query: Query | QueryFunction | undefined, documents: T[]): number[] { 78 | let found = []; 79 | let firstSearch = true; 80 | 81 | if (isFunction(query)) { 82 | for (let i = 0; i < documents.length; i++) { 83 | const document = documents[i]; 84 | const isMatched = query(document); 85 | if (isMatched) found.push(i); 86 | } 87 | 88 | return found; 89 | } 90 | 91 | if (isUndefined(query) || isObjectEmpty(query)) return numbersList(documents.length - 1); 92 | 93 | for (const key in query) { 94 | const queryValue = query[key]; 95 | 96 | if (firstSearch) { 97 | firstSearch = false; 98 | 99 | for (let i = 0; i < documents.length; i++) { 100 | const document = documents[i]; 101 | const documentValue = document[key]; 102 | const isMatched = matchValues(queryValue as QueryValue, documentValue); 103 | if (isMatched) found.push(i); 104 | } 105 | 106 | if (found.length === 0) return []; 107 | continue; 108 | } 109 | 110 | for (let i = 0; i < found.length; i++) { 111 | if (isUndefined(found[i])) continue; 112 | const position = found[i]; 113 | const document = documents[position]; 114 | const documentValue = document[key]; 115 | const isMatched = matchValues(queryValue as QueryValue, documentValue); 116 | if (isMatched) continue; 117 | delete found[i]; 118 | } 119 | } 120 | 121 | return cleanArray(found); 122 | } 123 | 124 | /** 125 | * Create new document with applyed modifications. 126 | * @param document Document to update. 127 | * @param update The modifications to apply. 128 | * @returns New document with applied updates or null if document should be deleted. 129 | */ 130 | export function updateDocument(document: T, update: Update | UpdateFunction): T { 131 | let newDocument: T | null = deepClone(document); 132 | 133 | if (isFunction(update)) { 134 | newDocument = update(newDocument); 135 | if (!newDocument) return {} as T; 136 | if (!isObject(newDocument)) throw new TypeError('Document must be an object'); 137 | 138 | } else { 139 | for (const key in update) { 140 | const value = update[key]; 141 | 142 | newDocument[key] = isFunction(value) 143 | ? value(newDocument[key]) 144 | : value as T[Extract]; 145 | } 146 | } 147 | 148 | prepareObject(newDocument); 149 | return deepClone(newDocument); 150 | } 151 | 152 | /** 153 | * Compares the value from the query and from the document. 154 | * @param queryValue Value from query. 155 | * @param documentValue Value from document. 156 | * @returns Are the values equal. 157 | */ 158 | export function matchValues(queryValue: QueryValue, documentValue: DocumentValue): boolean { 159 | if (isPrimitive(queryValue)) { 160 | return queryValue === documentValue; 161 | } 162 | 163 | if (isFunction(queryValue)) { 164 | return queryValue(documentValue) ? true : false; 165 | } 166 | 167 | if (isRegExp(queryValue)) { 168 | return isString(documentValue) && queryValue.test(documentValue); 169 | } 170 | 171 | if (isArray(queryValue) || isObject(queryValue)) { 172 | return deepCompare(queryValue, documentValue); 173 | } 174 | 175 | if (isUndefined(queryValue)) { 176 | return isUndefined(documentValue); 177 | } 178 | 179 | return false; 180 | } 181 | 182 | /** 183 | * Parse database storage file. 184 | * @param content Content of the file. 185 | * @returns Array of documents. 186 | */ 187 | export function parseDatabaseStorage(content: string): Document[] { 188 | const trimmed = content.trim(); 189 | if (trimmed === '') return []; 190 | 191 | const documents = JSON.parse(trimmed); 192 | if (!isArray(documents)) throw new TypeError('Database storage should be an array of objects'); 193 | 194 | for (let i = 0; i < documents.length; i++) { 195 | const document = documents[i]; 196 | if (!isObject(document)) throw new TypeError('Database storage should contain only objects'); 197 | prepareObject(document); 198 | if (isObjectEmpty(document)) delete documents[i]; 199 | } 200 | 201 | return cleanArray(documents); 202 | } 203 | -------------------------------------------------------------------------------- /lib/database.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | import { Writer } from './writer.ts'; 4 | import { Reader } from './reader.ts'; 5 | import { findOneDocument, findMultipleDocuments, updateDocument, parseDatabaseStorage } from './core.ts'; 6 | import { Document, DatabaseConfig, Query, QueryFunction, Update, UpdateFunction, Acceptable } from './types.ts'; 7 | import { cleanArray, deepClone, isObjectEmpty, prepareObject, isArray, isFunction, isObject, isString, isUndefined, isNull } from './utils.ts'; 8 | 9 | // TODO: Before Writing & After Reading configuration 10 | // TODO: Config with skip, limit, sort, immutable 11 | // TODO: Make documents storage read only (Optional) 12 | // TODO: Finish testing 13 | 14 | /** 15 | * # AloeDB 🌿 16 | * Light, Embeddable, NoSQL database for Deno 17 | * 18 | * [Deno](https://deno.land/x/aloedb) | [Github](https://github.com/Kirlovon/AloeDB) 19 | */ 20 | export class Database = Document> { 21 | /** 22 | * In-Memory documents storage. 23 | * 24 | * ***WARNING:*** It is better not to modify these documents manually, as the changes will not pass the necessary checks. 25 | * ***However, if you modify storage manualy, call the method `await db.save()` to save your changes.*** 26 | */ 27 | public documents: Schema[] = []; 28 | 29 | /** Data writing manager. */ 30 | private readonly writer?: Writer; 31 | 32 | /** Database configuration. */ 33 | private readonly config: DatabaseConfig = { 34 | path: undefined, 35 | pretty: true, 36 | autoload: true, 37 | autosave: true, 38 | batching: true, 39 | immutable: true, 40 | validator: undefined 41 | }; 42 | 43 | /** 44 | * Create database collection to store documents. 45 | * @param config Database configuration or path to the database file. 46 | */ 47 | constructor(config?: Partial | string) { 48 | if (isUndefined(config)) config = { autoload: false, autosave: false }; 49 | if (isString(config)) config = { path: config, autoload: true, autosave: true }; 50 | if (!isObject(config)) throw new TypeError('Config must be an object or a string'); 51 | 52 | // Disable autosave if path is not specified 53 | if (isUndefined(config?.path)) config.autosave = false; 54 | 55 | // Merge default config with users config 56 | this.config = { ...this.config, ...config }; 57 | 58 | // Writer initialization 59 | if (this.config.path) { 60 | this.writer = new Writer(this.config.path ); 61 | if (this.config.autoload) this.loadSync(); 62 | } 63 | } 64 | 65 | /** 66 | * Insert a document. 67 | * @param document Document to insert. 68 | * @param index Where to insert document. 69 | * @returns Inserted document. 70 | */ 71 | public async insertOne(document: Schema, index: Number = this.documents.length): Promise { 72 | const { immutable, validator, autosave } = this.config; 73 | if (!isObject(document)) throw new TypeError('Document must be an object'); 74 | 75 | prepareObject(document); 76 | if (validator) validator(document); 77 | if (isObjectEmpty(document)) return {} as Schema; 78 | 79 | const internal: Schema = deepClone(document); 80 | this.documents.splice(index, 0, internal); 81 | if (autosave) await this.save(); 82 | 83 | return immutable ? deepClone(internal) : internal; 84 | } 85 | 86 | /** 87 | * Inserts multiple documents. 88 | * @param documents Array of documents to insert. 89 | * @returns Array of inserted documents. 90 | */ 91 | public async insertMany(documents: Schema[]): Promise { 92 | const { immutable, validator, autosave } = this.config; 93 | if (!isArray(documents)) throw new TypeError('Input must be an array'); 94 | 95 | const inserted: Schema[] = []; 96 | 97 | for (let i = 0; i < documents.length; i++) { 98 | const document: Schema = documents[i]; 99 | if (!isObject(document)) throw new TypeError('Documents must be an objects'); 100 | 101 | prepareObject(document); 102 | if (validator) validator(document); 103 | if (isObjectEmpty(document)) continue; 104 | 105 | const internal: Schema = deepClone(document); 106 | inserted.push(internal); 107 | } 108 | 109 | this.documents = [...this.documents, ...inserted]; 110 | if (autosave) await this.save(); 111 | 112 | return immutable ? deepClone(inserted) : inserted; 113 | } 114 | 115 | /** 116 | * Find document by search query. 117 | * @param query Document selection criteria. 118 | * @returns Found document. 119 | */ 120 | public async findOne(query?: Query | QueryFunction): Promise { 121 | const { immutable } = this.config; 122 | if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); 123 | 124 | const found: number | null = findOneDocument(query, this.documents); 125 | if (isNull(found)) return null; 126 | 127 | const position: number = found; 128 | const document: Schema = this.documents[position]; 129 | 130 | return immutable ? deepClone(document) : document; 131 | } 132 | 133 | /** 134 | * Find multiple documents by search query. 135 | * @param query Documents selection criteria. 136 | * @returns Found documents. 137 | */ 138 | public async findMany(query?: Query | QueryFunction): Promise { 139 | const { immutable } = this.config; 140 | if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); 141 | 142 | // Optimization for empty queries 143 | if (isUndefined(query) || (isObject(query) && isObjectEmpty(query))) { 144 | return immutable ? deepClone(this.documents) : [...this.documents]; 145 | } 146 | 147 | const found: number[] = findMultipleDocuments(query, this.documents); 148 | if (found.length === 0) return []; 149 | 150 | const documents: Schema[] = []; 151 | 152 | for (let i = 0; i < found.length; i++) { 153 | const position: number = found[i]; 154 | const document: Schema = this.documents[position]; 155 | documents.push(document); 156 | } 157 | 158 | return immutable ? deepClone(documents) : documents; 159 | } 160 | 161 | /** 162 | * Modifies an existing document that match search query. 163 | * @param query Document selection criteria. 164 | * @param update The modifications to apply. 165 | * @returns Found document with applied modifications. 166 | */ 167 | public async updateOne(query: Query | QueryFunction, update: Update | UpdateFunction): Promise { 168 | const { validator, autosave, immutable } = this.config; 169 | 170 | if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); 171 | if (!isObject(update) && !isFunction(update)) throw new TypeError('Update must be an object or function'); 172 | 173 | const found: number | null = findOneDocument(query, this.documents); 174 | if (isNull(found)) return null; 175 | 176 | const position: number = found; 177 | const document: Schema = this.documents[position]; 178 | const updated: Schema = updateDocument(document, update); 179 | 180 | if (validator) validator(updated); 181 | 182 | if (isObjectEmpty(updated)) { 183 | this.documents.splice(position, 1); 184 | return {} as Schema; 185 | } 186 | 187 | this.documents[position] = updated; 188 | if (autosave) await this.save(); 189 | 190 | return immutable ? deepClone(updated) : updated; 191 | } 192 | 193 | /** 194 | * Modifies all documents that match search query. 195 | * @param query Documents selection criteria. 196 | * @param update The modifications to apply. 197 | * @returns Found documents with applied modifications. 198 | */ 199 | public async updateMany(query: Query | QueryFunction, update: Update | UpdateFunction): Promise { 200 | const { validator, autosave, immutable } = this.config; 201 | 202 | if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); 203 | if (!isObject(update) && !isFunction(update)) throw new TypeError('Update must be an object or function'); 204 | 205 | const found: number[] = findMultipleDocuments(query, this.documents); 206 | if (found.length === 0) return []; 207 | 208 | const temporary: Schema[] = [...this.documents]; 209 | const updatedDocuments: Schema[] = []; 210 | let deleted: boolean = false; 211 | 212 | for (let i = 0; i < found.length; i++) { 213 | const position: number = found[i]; 214 | const document: Schema = temporary[position]; 215 | const updated: Schema = updateDocument(document, update); 216 | 217 | if (validator) validator(updated); 218 | 219 | if (isObjectEmpty(updated)) { 220 | deleted = true; 221 | delete temporary[position]; 222 | continue; 223 | } 224 | 225 | temporary[position] = updated; 226 | updatedDocuments.push(updated); 227 | } 228 | 229 | this.documents = deleted ? cleanArray(temporary) : temporary; 230 | if (autosave) await this.save(); 231 | 232 | return immutable ? deepClone(updatedDocuments) : updatedDocuments; 233 | } 234 | 235 | /** 236 | * Deletes first found document that matches the search query. 237 | * @param query Document selection criteria. 238 | * @returns Deleted document. 239 | */ 240 | public async deleteOne(query?: Query | QueryFunction): Promise { 241 | const { autosave } = this.config; 242 | 243 | if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); 244 | 245 | const found: number | null = findOneDocument(query, this.documents); 246 | if (isNull(found)) return null; 247 | 248 | const position: number = found; 249 | const deleted: Schema = this.documents[position]; 250 | 251 | this.documents.splice(position, 1); 252 | if (autosave) await this.save(); 253 | 254 | return deleted; 255 | } 256 | 257 | /** 258 | * Deletes all documents that matches the search query. 259 | * @param query Document selection criteria. 260 | * @returns Array of deleted documents. 261 | */ 262 | public async deleteMany(query?: Query | QueryFunction): Promise { 263 | const { autosave } = this.config; 264 | 265 | if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); 266 | 267 | const found: number[] = findMultipleDocuments(query, this.documents); 268 | if (found.length === 0) return []; 269 | 270 | const temporary: Schema[] = [...this.documents]; 271 | const deleted: Schema[] = []; 272 | 273 | for (let i = 0; i < found.length; i++) { 274 | const position: number = found[i]; 275 | const document: Schema = temporary[position]; 276 | 277 | deleted.push(document); 278 | delete temporary[position]; 279 | } 280 | 281 | this.documents = cleanArray(temporary); 282 | if (autosave) await this.save(); 283 | 284 | return deleted; 285 | } 286 | 287 | /** 288 | * Count found documents. 289 | * @param query Documents selection criteria. 290 | * @returns Documents count. 291 | */ 292 | public async count(query?: Query | QueryFunction): Promise { 293 | if (!isUndefined(query) && !isObject(query) && !isFunction(query)) throw new TypeError('Query must be an object or function'); 294 | 295 | // Optimization for empty queries 296 | if (isUndefined(query) || (isObject(query) && isObjectEmpty(query))) return this.documents.length; 297 | 298 | const found: number[] = findMultipleDocuments(query, this.documents); 299 | return found.length; 300 | } 301 | 302 | /** 303 | * Delete all documents. 304 | */ 305 | public async drop(): Promise { 306 | this.documents = []; 307 | if (this.config.autosave) await this.save(); 308 | } 309 | 310 | /** 311 | * Load data from storage file. 312 | */ 313 | public async load(): Promise { 314 | const { path, validator } = this.config; 315 | if (!path) return; 316 | 317 | const content: string = await Reader.read(path); 318 | const documents: Document[] = parseDatabaseStorage(content); 319 | 320 | // Schema validation 321 | if (validator) { 322 | for (let i = 0; i < documents.length; i++) validator(documents[i]) 323 | } 324 | 325 | this.documents = documents as Schema[]; 326 | } 327 | 328 | /** 329 | * Synchronously load data from storage file. 330 | */ 331 | public loadSync(): void { 332 | const { path, validator } = this.config; 333 | if (!path) return; 334 | 335 | const content: string = Reader.readSync(path); 336 | const documents: Document[] = parseDatabaseStorage(content); 337 | 338 | // Schema validation 339 | if (validator) { 340 | for (let i = 0; i < documents.length; i++) validator(documents[i]) 341 | } 342 | 343 | this.documents = documents as Schema[]; 344 | } 345 | 346 | /** 347 | * Write documents to the database storage file. 348 | * Called automatically after each insert, update or delete operation. _(Only if `autosave` parameter is set to `true`)_ 349 | */ 350 | public async save(): Promise { 351 | if (!this.writer) return; 352 | 353 | if (this.config.batching) { 354 | this.writer.batchWrite(this.documents, this.config.pretty); // Should be without await 355 | } else { 356 | await this.writer.write(this.documents, this.config.pretty); 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /lib/helpers.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | import { matchValues } from './core.ts'; 4 | import { DocumentValue, DocumentPrimitive, QueryValue } from './types.ts'; 5 | import { isArray, isUndefined, isString, isNumber, isBoolean, isNull, isObject } from './utils.ts'; 6 | 7 | /** 8 | * Selects documents where the value of a field more than specified number. 9 | * @param value Comparison number. 10 | */ 11 | export function moreThan(value: number) { 12 | return (target: Readonly) => isNumber(target) && target > value; 13 | } 14 | 15 | /** 16 | * Selects documents where the value of a field more than or equal to the specified number. 17 | * @param value Comparison number. 18 | */ 19 | export function moreThanOrEqual(value: number) { 20 | return (target: Readonly) => isNumber(target) && target >= value; 21 | } 22 | 23 | /** 24 | * Selects documents where the value of a field less than specified number. 25 | * @param value Comparison number. 26 | */ 27 | export function lessThan(value: number) { 28 | return (target: Readonly) => isNumber(target) && target < value; 29 | } 30 | 31 | /** 32 | * Selects documents where the value of a field less than or equal to the specified number. 33 | * @param value Comparison number. 34 | */ 35 | export function lessThanOrEqual(value: number) { 36 | return (target: Readonly) => isNumber(target) && target <= value; 37 | } 38 | 39 | /** 40 | * Matches if number is between specified range values. 41 | * @param min Range start. 42 | * @param max Range end. 43 | */ 44 | export function between(min: number, max: number) { 45 | return (target: Readonly) => isNumber(target) && target > min && target < max; 46 | } 47 | 48 | /** 49 | * Matches if number is between or equal to the specified range values. 50 | * @param min Range start. 51 | * @param max Range end. 52 | */ 53 | export function betweenOrEqual(min: number, max: number) { 54 | return (target: Readonly) => isNumber(target) && target >= min && target <= max; 55 | } 56 | 57 | /** 58 | * Matches if field exists. 59 | */ 60 | export function exists() { 61 | return (target?: Readonly) => !isUndefined(target); 62 | } 63 | 64 | /** 65 | * Matches if value type equal to specified type. 66 | * @param type Type of the value. 67 | */ 68 | export function type(type: 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object') { 69 | return (target: Readonly) => { 70 | switch (type) { 71 | case 'string': 72 | return isString(target); 73 | case 'number': 74 | return isNumber(target); 75 | case 'boolean': 76 | return isBoolean(target); 77 | case 'null': 78 | return isNull(target); 79 | case 'array': 80 | return isArray(target); 81 | case 'object': 82 | return isObject(target); 83 | default: 84 | return false; 85 | } 86 | }; 87 | } 88 | 89 | /** 90 | * Matches if array includes specified value. 91 | * @param value Primitive value to search in array. 92 | */ 93 | export function includes(value: DocumentPrimitive) { 94 | return (target: Readonly) => isArray(target) && target.includes(value); 95 | } 96 | 97 | /** 98 | * Matches if array length equal to specified length. 99 | * @param length Length of the array. 100 | */ 101 | export function length(length: number) { 102 | return (target: Readonly) => isArray(target) && target.length === length; 103 | } 104 | 105 | /** 106 | * Matches if at least one value in the array matches the given queries. 107 | * @param queries Query values. 108 | */ 109 | export function someElementMatch(...queries: QueryValue[]) { 110 | return (target: Readonly) => isArray(target) && target.some(targetValue => queries.every(query => matchValues(query, targetValue))); 111 | } 112 | 113 | /** 114 | * Matches if all the values in the array match in the given queries. 115 | * @param queries Query values. 116 | */ 117 | export function everyElementMatch(...queries: QueryValue[]) { 118 | return (target: Readonly) => isArray(target) && target.every(targetValue => queries.every(query => matchValues(query, targetValue))); 119 | } 120 | 121 | /** 122 | * Logical AND operator. Selects documents where the value of a field equals to all specified values. 123 | * @param queries Query values. 124 | */ 125 | export function and(...queries: QueryValue[]) { 126 | return (target: Readonly) => queries.every(query => matchValues(query, target as DocumentValue)); 127 | } 128 | 129 | /** 130 | * Logical OR operator. Selects documents where the value of a field equals at least one specified value. 131 | * @param values Query values. 132 | */ 133 | export function or(...queries: QueryValue[]) { 134 | return (target: Readonly) => queries.some(query => matchValues(query, target as DocumentValue)); 135 | } 136 | 137 | /** 138 | * Logical NOT operator. Selects documents where the value of a field not equal to specified value. 139 | * @param value Query value. 140 | */ 141 | export function not(query: QueryValue) { 142 | return (target: Readonly) => matchValues(query, target as DocumentValue) === false; 143 | } 144 | -------------------------------------------------------------------------------- /lib/reader.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | import { isUndefined, getPathDirname } from './utils.ts'; 4 | 5 | /** 6 | * Database storage file reader. 7 | */ 8 | export class Reader { 9 | 10 | /** 11 | * Read database storage file. Creates a new file if its not exists. 12 | * @param path Path to the file. 13 | * @returns File content. 14 | */ 15 | public static async read(path: string): Promise { 16 | if (isUndefined(path)) return '[]'; 17 | 18 | if (await exists(path) === false) { 19 | await ensureFile(path, '[]'); 20 | return '[]'; 21 | } 22 | 23 | const content: string = await Deno.readTextFile(path); 24 | return content; 25 | } 26 | 27 | /** 28 | * Read database storage file synchronously. Creates a new file if its not exists. 29 | * @param path Path to the file. 30 | * @returns File content. 31 | */ 32 | public static readSync(path: string): string { 33 | if (isUndefined(path)) return '[]'; 34 | 35 | if (existsSync(path) === false) { 36 | ensureFileSync(path, '[]'); 37 | return '[]'; 38 | } 39 | 40 | const content: string = Deno.readTextFileSync(path); 41 | return content; 42 | } 43 | } 44 | 45 | /** 46 | * Test whether or not the given path exists. 47 | * @param path Path to the file. 48 | * @returns Is file exists. 49 | */ 50 | async function exists(path: string): Promise { 51 | try { 52 | await Deno.lstat(path); 53 | return true; 54 | } catch (error) { 55 | if (error instanceof Deno.errors.NotFound) return false; 56 | throw error; 57 | } 58 | } 59 | 60 | /** 61 | * Synchronously test whether or not the given path exists. 62 | * @param path Path to the file. 63 | * @returns Is file exists. 64 | */ 65 | function existsSync(path: string): boolean { 66 | try { 67 | Deno.lstatSync(path); 68 | return true; 69 | } catch (error) { 70 | if (error instanceof Deno.errors.NotFound) return false; 71 | throw error; 72 | } 73 | } 74 | 75 | /** 76 | * Ensures that the file exists. 77 | * @param path Path to the file. 78 | * @param data Data to write to if file not exists. 79 | * @returns Is file created. 80 | */ 81 | async function ensureFile(path: string, data: string = ''): Promise { 82 | try { 83 | const info = await Deno.lstat(path); 84 | if (!info.isFile) throw new Error('Invalid file specified'); 85 | } catch (error) { 86 | if (error instanceof Deno.errors.NotFound) { 87 | const dirname: string = getPathDirname(path); 88 | await ensureDir(dirname); 89 | await Deno.writeTextFile(path, data); 90 | return; 91 | } 92 | 93 | throw error; 94 | } 95 | } 96 | 97 | /** 98 | * Ensures that the file exists synchronously. 99 | * @param path Path to the file. 100 | * @param data Data to write to if file not exists. 101 | * @returns Is file created. 102 | */ 103 | function ensureFileSync(path: string, data: string = ''): void { 104 | try { 105 | const info = Deno.lstatSync(path); 106 | if (!info.isFile) throw new Error('Invalid file specified'); 107 | } catch (error) { 108 | if (error instanceof Deno.errors.NotFound) { 109 | const dirname: string = getPathDirname(path); 110 | ensureDirSync(dirname); 111 | Deno.writeTextFileSync(path, data); 112 | return; 113 | } 114 | 115 | throw error; 116 | } 117 | } 118 | 119 | /** 120 | * Ensures that the file directory. 121 | * @param path Path to the directory. 122 | * @returns Is directory created. 123 | */ 124 | async function ensureDir(path: string): Promise { 125 | try { 126 | const info: Deno.FileInfo = await Deno.lstat(path); 127 | if (!info.isDirectory) throw new Error('Invalid directory specified'); 128 | } catch (error) { 129 | if (error instanceof Deno.errors.NotFound) { 130 | await Deno.mkdir(path, { recursive: true }); 131 | return; 132 | } 133 | 134 | throw error; 135 | } 136 | } 137 | 138 | /** 139 | * Ensures that the file directory synchronously. 140 | * @param path Path to the directory. 141 | * @returns Is directory created. 142 | */ 143 | function ensureDirSync(path: string): void { 144 | try { 145 | const info: Deno.FileInfo = Deno.lstatSync(path); 146 | if (!info.isDirectory) throw new Error('Invalid directory specified'); 147 | } catch (error) { 148 | if (error instanceof Deno.errors.NotFound) { 149 | Deno.mkdirSync(path, { recursive: true }); 150 | return; 151 | } 152 | 153 | throw error; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | /** 4 | * Database initialization config 5 | */ 6 | export interface DatabaseConfig { 7 | 8 | /** Path to the database file. If undefined, data will be stored only in-memory. _(Default: undefined)_ */ 9 | path?: string; 10 | 11 | /** Save data in easy-to-read format. _(Default: true)_ */ 12 | pretty: boolean; 13 | 14 | /** Automatically load the file synchronously when initializing the database. _(Default: true)_ */ 15 | autoload: boolean; 16 | 17 | /** 18 | * Automatically save data to the file after inserting, updating and deleting documents. 19 | * If `path` specified, data will be read from the file, but new data will not be written. 20 | */ 21 | autosave: boolean; 22 | 23 | /** Automatically deeply clone all returned objects. _(Default: true)_ */ 24 | immutable: boolean; 25 | 26 | /** 27 | * Optimize writing using batching. If enabled, the data will be written many times faster in case of a large number of operations. 28 | * Disable it if you want the methods to be considered executed only when the data is written to a file. _(Default: true)_ 29 | */ 30 | batching: boolean | number; 31 | 32 | /** 33 | * Runtime documents validation function. 34 | * If the document does not pass the validation, just throw the error. 35 | * Works well with [Superstruct](https://github.com/ianstormtaylor/superstruct)! 36 | */ 37 | validator?: (document: any) => void; 38 | 39 | } 40 | 41 | /** Checking the object for storage suitability. */ 42 | export type Acceptable = { [K in keyof T]: T[K] & DocumentValue }; 43 | 44 | /** Any document-like object. */ 45 | export type Document = { [key: string]: DocumentValue }; 46 | 47 | /** Array of document values. */ 48 | export type DocumentArray = DocumentValue[]; 49 | 50 | /** Supported documents values. */ 51 | export type DocumentValue = DocumentPrimitive | Document | DocumentArray; 52 | 53 | /** Supported primitives. */ 54 | export type DocumentPrimitive = string | number | boolean | null; 55 | 56 | /** Documents selection criteria. */ 57 | export type Query = { [K in keyof T]?: QueryValue }; 58 | 59 | /** Possible search query values. */ 60 | export type QueryValue = DocumentValue | ((value: Readonly) => boolean) | RegExp | undefined; 61 | 62 | /** Manual вocuments selection function. */ 63 | export type QueryFunction = (document: Readonly) => boolean; 64 | 65 | /** The modifications to apply. */ 66 | export type Update = { [K in keyof T]?: UpdateValue }; 67 | 68 | /** Possible update values. */ 69 | export type UpdateValue = T | ((value: T) => T) | undefined; 70 | 71 | /** Manual modifications applying. */ 72 | export type UpdateFunction = (document: T) => T | null; 73 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | import { DocumentPrimitive } from './types.ts'; 4 | 5 | /** Any object without specified structure. */ 6 | interface PlainObject { 7 | [key: string]: unknown; 8 | } 9 | 10 | /** 11 | * Remove all empty items from the array. 12 | * @param target Array to clean. 13 | * @returns Cleaned array. 14 | */ 15 | export function cleanArray(target: T): T { 16 | return target.filter(() => true) as T; 17 | } 18 | 19 | /** 20 | * Generate array of numbers from 0 to Nth. 21 | * @param number Nth value. 22 | * @returns Generated array. 23 | */ 24 | export function numbersList(number: number): number[] { 25 | const array: number[] = []; 26 | for (let i = 0; i <= number; i++) array.push(i); 27 | return array; 28 | } 29 | 30 | /** 31 | * Checks if the object is empty. 32 | * @param target Object to check. 33 | * @returns Is object empty or not. 34 | */ 35 | export function isObjectEmpty(target: PlainObject): boolean { 36 | for (let key in target) return false; 37 | return true; 38 | } 39 | 40 | /** 41 | * Get number of keys in object. 42 | * @param target An object for key counting. 43 | * @returns Number of keys. 44 | */ 45 | export function getObjectLength(target: PlainObject): number { 46 | let length: number = 0; 47 | for (let key in target) length++; 48 | return length; 49 | } 50 | 51 | /** 52 | * Get filename from the path. 53 | * @param path Path to the file. 54 | * @returns Filename from the path. 55 | */ 56 | export function getPathFilename(path: string): string { 57 | const parsed: string[] = path.split(/[\\\/]/); 58 | const filename: string | undefined = parsed.pop(); 59 | 60 | return filename ? filename : ''; 61 | } 62 | 63 | /** 64 | * Get dirname from the path. 65 | * @param path Path to the file. 66 | * @returns Dirname from the path. 67 | */ 68 | export function getPathDirname(path: string): string { 69 | let parsed: string[] = path.split(/[\\\/]/); 70 | parsed = parsed.map(value => value.trim()); 71 | parsed = parsed.filter(value => value !== ''); 72 | parsed.pop(); 73 | 74 | const dirname: string = parsed.join('/'); 75 | return dirname; 76 | } 77 | 78 | /** 79 | * Deep clone for objects and arrays. Can also be used for primitives. 80 | * @param target Target to clone. 81 | * @return Clone of the target. 82 | */ 83 | export function deepClone(target: T): T { 84 | if (isNull(target)) return target; 85 | 86 | if (isArray(target)) { 87 | const clone: any = []; 88 | for (let i = 0; i < target.length; i++) clone[i] = deepClone(target[i]); 89 | return clone as T; 90 | } 91 | 92 | if (isObject(target)) { 93 | const clone: any = {}; 94 | for (const key in target) clone[key] = deepClone(target[key]); 95 | return clone as T; 96 | } 97 | 98 | return target; 99 | } 100 | 101 | /** 102 | * Deep targets comparison. 103 | * @param targetA First target for comparison. 104 | * @param targetB Second target for comparison. 105 | * @returns Targets equal or not. 106 | */ 107 | export function deepCompare(targetA: unknown, targetB: unknown): boolean { 108 | if (isNull(targetA)) return isNull(targetB); 109 | if (isNull(targetB)) return isNull(targetA); 110 | 111 | if (isArray(targetA) && isArray(targetB)) { 112 | if (targetA.length !== targetB.length) return false; 113 | 114 | for (let i = 0; i < targetA.length; i++) { 115 | if (!deepCompare(targetA[i], targetB[i])) return false; 116 | } 117 | 118 | return true; 119 | } 120 | 121 | if (isObject(targetA) && isObject(targetB)) { 122 | if (getObjectLength(targetA) !== getObjectLength(targetB)) return false; 123 | 124 | for (const key in targetA) { 125 | if (!deepCompare(targetA[key], targetB[key])) return false; 126 | } 127 | 128 | return true; 129 | } 130 | 131 | return targetA === targetB; 132 | } 133 | 134 | /** 135 | * Prepare object for database storage. 136 | * @param target Object to prepare. 137 | */ 138 | export function prepareObject(target: PlainObject): void { 139 | for (const key in target) { 140 | const value: unknown = target[key]; 141 | 142 | if (isPrimitive(value)) { 143 | continue; 144 | } 145 | 146 | if (isArray(value)) { 147 | prepareArray(value); 148 | continue; 149 | } 150 | 151 | if (isObject(value)) { 152 | prepareObject(value); 153 | continue; 154 | } 155 | 156 | delete target[key]; 157 | } 158 | } 159 | 160 | /** 161 | * Prepare array for database storage. 162 | * @param target Array to prepare. 163 | */ 164 | export function prepareArray(target: unknown[]): void { 165 | for (let i = 0; i < target.length; i++) { 166 | const value: unknown = target[i]; 167 | 168 | if (isPrimitive(value)) { 169 | continue; 170 | } 171 | 172 | if (isArray(value)) { 173 | prepareArray(value); 174 | continue; 175 | } 176 | 177 | if (isObject(value)) { 178 | prepareObject(value); 179 | continue; 180 | } 181 | 182 | if (isUndefined(value)) { 183 | target[i] = null; 184 | continue; 185 | } 186 | 187 | target[i] = null; 188 | } 189 | } 190 | 191 | /** 192 | * Checks whether the value is a primitive. 193 | * @param target Target to check. 194 | * @returns Result of checking. 195 | */ 196 | export function isPrimitive(target: unknown): target is DocumentPrimitive { 197 | const type = typeof target; 198 | return type === 'string' || type === 'number' || type === 'boolean' || target === null; 199 | } 200 | 201 | /** 202 | * Checks whether the value is a string. 203 | * @param target Target to check. 204 | * @returns Result of checking. 205 | */ 206 | export function isString(target: unknown): target is string { 207 | return typeof target === 'string'; 208 | } 209 | 210 | /** 211 | * Checks whether the value is a number. 212 | * @param target Target to check. 213 | * @returns Result of checking. 214 | */ 215 | export function isNumber(target: unknown): target is number { 216 | return typeof target === 'number' && !Number.isNaN(target); 217 | } 218 | 219 | /** 220 | * Checks whether the value is a boolean. 221 | * @param target Target to check. 222 | * @returns Result of checking. 223 | */ 224 | export function isBoolean(target: unknown): target is boolean { 225 | return typeof target === 'boolean'; 226 | } 227 | 228 | /** 229 | * Checks whether the value is undefined. 230 | * @param target Target to check. 231 | * @returns Result of checking. 232 | */ 233 | export function isUndefined(target: unknown): target is undefined { 234 | return typeof target === 'undefined'; 235 | } 236 | 237 | /** 238 | * Checks whether the value is a null. 239 | * @param target Target to check. 240 | * @returns Result of checking. 241 | */ 242 | export function isNull(target: unknown): target is null { 243 | return target === null; 244 | } 245 | 246 | /** 247 | * Checks whether the value is a function. 248 | * @param target Target to check. 249 | * @returns Result of checking. 250 | */ 251 | export function isFunction(target: unknown): target is (...args: any) => any { 252 | return typeof target === 'function'; 253 | } 254 | 255 | /** 256 | * Checks whether the value is an array. 257 | * @param target Target to check. 258 | * @returns Result of checking. 259 | */ 260 | export function isArray(target: unknown): target is any[] { 261 | return Object.prototype.toString.call(target) === '[object Array]'; 262 | } 263 | 264 | /** 265 | * Checks whether the value is a object. 266 | * @param target Target to check. 267 | * @returns Result of checking. 268 | */ 269 | export function isObject(target: unknown): target is PlainObject { 270 | return Object.prototype.toString.call(target) === '[object Object]'; 271 | } 272 | 273 | /** 274 | * Checks whether the value is a regular expression. 275 | * @param target Target to check. 276 | * @returns Result of checking. 277 | */ 278 | export function isRegExp(target: unknown): target is RegExp { 279 | return target instanceof RegExp; 280 | } 281 | -------------------------------------------------------------------------------- /lib/writer.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | import { Document } from './types.ts'; 4 | 5 | /** 6 | * Data writing manager. 7 | * Uses atomic writing and prevents race condition. 8 | */ 9 | export class Writer { 10 | 11 | /** Next data for writing. */ 12 | private next: Document[] | null = null; 13 | 14 | /** Lock writing. */ 15 | private locked: boolean = false; 16 | 17 | /** Path to the database file. */ 18 | private readonly path: string; 19 | 20 | /** Temporary file extension. */ 21 | private readonly extension: string = '.temp'; 22 | 23 | /** 24 | * Storage initialization. 25 | * @param path Path to the database file. 26 | */ 27 | constructor(path: string) { 28 | this.path = path; 29 | } 30 | 31 | /** 32 | * Batch data writing. 33 | * Do not call this method with `await`, otherwise the result of this method will be identical to the `write()` method. 34 | * 35 | * @param documents Array with documents to write. 36 | * @param pretty Write data in easy-to-read format. 37 | */ 38 | public async batchWrite(documents: Document[], pretty: boolean): Promise { 39 | if (this.locked) { 40 | this.next = documents; 41 | return; 42 | } 43 | 44 | try { 45 | this.locked = true; 46 | await this.write(documents, pretty); 47 | } finally { 48 | this.locked = false; 49 | } 50 | 51 | if (this.next) { 52 | const nextCopy = this.next; 53 | this.next = null; 54 | this.batchWrite(nextCopy, pretty); 55 | } 56 | } 57 | 58 | /** 59 | * Write data to the database file. 60 | * @param documents Array with documents to write. 61 | * @param pretty Write data in easy-to-read format. 62 | */ 63 | public async write(documents: Document[], pretty: boolean): Promise { 64 | const temp: string = this.path + this.extension; 65 | const encoded: string = pretty ? JSON.stringify(documents, null, '\t') : JSON.stringify(documents); 66 | 67 | await Deno.writeTextFile(temp, encoded); 68 | await Deno.rename(temp, this.path); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 the AloeDB authors. All rights reserved. MIT license. 2 | 3 | export { Database } from './lib/database.ts'; 4 | export * from './lib/helpers.ts'; 5 | export * from './lib/types.ts'; 6 | -------------------------------------------------------------------------------- /other/head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirlovon/aloedb/849b3260bbca9d45d3bd971c26ef7bb9a5b7bf2a/other/head.png -------------------------------------------------------------------------------- /other/logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirlovon/aloedb/849b3260bbca9d45d3bd971c26ef7bb9a5b7bf2a/other/logo.afdesign -------------------------------------------------------------------------------- /tests/benchmark/benchmark.ts: -------------------------------------------------------------------------------- 1 | import { Database } from '../../mod.ts'; 2 | import { RunBenchmark, randomID, delay } from './utils.ts'; 3 | 4 | const TEMP_FILE: string = './temp_benchmark_db.json'; 5 | const ITERATIONS = 1000; 6 | 7 | const IDS: string[] = []; 8 | for (let i = 0; i < ITERATIONS; i++) IDS.push(randomID()); 9 | 10 | // Initialization 11 | const db = new Database({ path: TEMP_FILE, autosave: true, immutable: true, pretty: true, batching: true }); 12 | 13 | // Running insertion operations 14 | await RunBenchmark('Insertion', ITERATIONS, async (iteration) => { 15 | await db.insertOne({ foo: IDS[iteration], i: iteration }); 16 | }); 17 | 18 | await delay(500); 19 | 20 | // Running searching operations 21 | await RunBenchmark('Searching', ITERATIONS, async (iteration) => { 22 | await db.findOne({ foo: IDS[iteration] }); 23 | }); 24 | 25 | await delay(500); 26 | 27 | // Running updating operations 28 | await RunBenchmark('Updating', ITERATIONS, async (iteration) => { 29 | await db.updateMany({ foo: IDS[iteration] }, { foo: IDS[iteration] }); 30 | }); 31 | 32 | await delay(500); 33 | 34 | // Running deleting operations 35 | await RunBenchmark('Deleting', ITERATIONS, async (iteration) => { 36 | await db.deleteMany({ foo: IDS[iteration] }); 37 | }); 38 | 39 | // Remove temp file 40 | setTimeout(() => { 41 | Deno.removeSync(TEMP_FILE); 42 | }, 1000); 43 | 44 | -------------------------------------------------------------------------------- /tests/benchmark/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run benchmark test 3 | * @param name Name of the benchmark 4 | * @param iterations Amount of iterations 5 | * @param test Test to run 6 | */ 7 | export async function RunBenchmark(name: string, iterations: number, test: (iteration: number) => Promise): Promise { 8 | const testStart = performance.now(); 9 | for (let i = 0; i < iterations; i++) await test(i); 10 | const testEnd = performance.now(); 11 | 12 | const timeResult = testEnd - testStart; 13 | const operationsCount = 1000 / (timeResult / iterations); 14 | const formated = formatNumber(operationsCount); 15 | 16 | console.log(`${name}: ${formated} ops/sec (${timeResult.toFixed(2)} ms)`); 17 | } 18 | 19 | /** 20 | * Format big numbers to more readable format (16000000 -> 16M) 21 | * @param number Number to format 22 | * @returns Formated number 23 | */ 24 | export function formatNumber(number: number): string { 25 | if (number >= 1000000) return (number / 1000000).toFixed(1) + 'M'; 26 | if (number >= 1000) return (number / 1000).toFixed(1) + 'K'; 27 | return number.toFixed(1); 28 | } 29 | 30 | /** 31 | * Generate random ID 32 | * @param length Length of the id 33 | * @returns Random id 34 | */ 35 | export function randomID(length: number = 32): string { 36 | const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; 37 | let result = ''; 38 | 39 | for (let i = 0; i < length; i++) { 40 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 41 | } 42 | 43 | return result; 44 | } 45 | 46 | /** 47 | * Delay function 48 | * @param ms Delay time 49 | */ 50 | export function delay(ms: number): Promise { 51 | return new Promise((resolve) => setTimeout(resolve, ms)); 52 | } 53 | -------------------------------------------------------------------------------- /tests/core_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from 'https://deno.land/std@0.102.0/testing/asserts.ts'; 2 | import { yellow } from 'https://deno.land/std@0.102.0/fmt/colors.ts'; 3 | 4 | import { 5 | findOneDocument, 6 | findMultipleDocuments, 7 | updateDocument, 8 | matchValues, 9 | parseDatabaseStorage 10 | } from '../lib/core.ts'; 11 | 12 | Deno.test(`${yellow('[core.ts]')} findOneDocument (Single document)`, () => { 13 | const documents: any = [{ object: { foo: 'bar' } }, { array: [1, 2, 3] }, { nothing: null }, { boolean: true }, { number: 42 }, { text: 'foo' }]; 14 | 15 | const search1 = findOneDocument({ text: 'foo' }, documents); 16 | const search2 = findOneDocument({ number: 42 }, documents); 17 | const search3 = findOneDocument({ boolean: true }, documents); 18 | const search4 = findOneDocument({ nothing: null }, documents); 19 | const search5 = findOneDocument({ array: [1, 2, 3] }, documents); 20 | const search6 = findOneDocument({ object: { foo: 'bar' } }, documents); 21 | const search7 = findOneDocument({ text: /foo/ }, documents); 22 | const search8 = findOneDocument({ number: (value: any) => value === 42 }, documents); 23 | 24 | assertEquals(search1, 5); 25 | assertEquals(search2, 4); 26 | assertEquals(search3, 3); 27 | assertEquals(search4, 2); 28 | assertEquals(search5, 1); 29 | assertEquals(search6, 0); 30 | assertEquals(search7, 5); 31 | assertEquals(search8, 4); 32 | }); 33 | 34 | Deno.test(`${yellow('[core.ts]')} findOneDocument (No criteria)`, () => { 35 | const documents: any = [ 36 | { text: 'foo' }, 37 | { text: 'bar' }, 38 | { text: 'baz' }, 39 | ]; 40 | 41 | const search1 = findOneDocument({}, documents); 42 | const search2 = findOneDocument(undefined, documents); 43 | const search3 = findOneDocument({}, []); 44 | const search4 = findOneDocument(undefined, []); 45 | 46 | assertEquals(search1, 0); 47 | assertEquals(search2, 0); 48 | assertEquals(search3, null); 49 | assertEquals(search4, null); 50 | }); 51 | 52 | Deno.test(`${yellow('[core.ts]')} findOneDocument (Search function)`, () => { 53 | const documents: any = [ 54 | { text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3], object: { foo: 'bar' } }, 55 | { text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3] }, 56 | { text: 'foo', number: 42, boolean: true, nothing: 0 }, 57 | ]; 58 | 59 | const search1 = findOneDocument(() => true, documents); 60 | const search2 = findOneDocument(() => false, documents); 61 | const search3 = findOneDocument((value: any) => value?.nothing === 0, documents); 62 | 63 | assertEquals(search1, 0); 64 | assertEquals(search2, null); 65 | assertEquals(search3, 2); 66 | }); 67 | 68 | Deno.test(`${yellow('[core.ts]')} findMultipleDocuments (Single document)`, () => { 69 | const documents: any = [{ object: { foo: 'bar' } }, { array: [1, 2, 3] }, { nothing: null }, { boolean: true }, { number: 42 }, { text: 'foo' }]; 70 | 71 | const search1 = findMultipleDocuments({ text: 'foo' }, documents); 72 | const search2 = findMultipleDocuments({ number: 42 }, documents); 73 | const search3 = findMultipleDocuments({ boolean: true }, documents); 74 | const search4 = findMultipleDocuments({ nothing: null }, documents); 75 | const search5 = findMultipleDocuments({ array: [1, 2, 3] }, documents); 76 | const search6 = findMultipleDocuments({ object: { foo: 'bar' } }, documents); 77 | const search7 = findMultipleDocuments({ text: /foo/ }, documents); 78 | const search8 = findMultipleDocuments({ number: (value: any) => value === 42 }, documents); 79 | 80 | assertEquals(search1, [5]); 81 | assertEquals(search2, [4]); 82 | assertEquals(search3, [3]); 83 | assertEquals(search4, [2]); 84 | assertEquals(search5, [1]); 85 | assertEquals(search6, [0]); 86 | assertEquals(search7, [5]); 87 | assertEquals(search8, [4]); 88 | }); 89 | 90 | Deno.test(`${yellow('[core.ts]')} findMultipleDocuments (Multiple documents)`, () => { 91 | const documents: any = [ 92 | { text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3], object: { foo: 'bar' } }, 93 | { text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3] }, 94 | { text: 'foo', number: 42, boolean: true, nothing: null }, 95 | { text: 'foo', number: 42, boolean: true }, 96 | { text: 'foo', number: 42 }, 97 | { text: 'foo' }, 98 | ]; 99 | 100 | const search1 = findMultipleDocuments({ text: 'foo' }, documents); 101 | const search2 = findMultipleDocuments({ text: 'foo', number: 42 }, documents); 102 | const search3 = findMultipleDocuments({ text: 'foo', number: 42, boolean: true }, documents); 103 | const search4 = findMultipleDocuments({ text: 'foo', number: 42, boolean: true, nothing: null }, documents); 104 | const search5 = findMultipleDocuments({ text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3] }, documents); 105 | const search6 = findMultipleDocuments({ text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3], object: { foo: 'bar' } }, documents); 106 | const search7 = findMultipleDocuments({ text: /foo/, array: (value: any) => value?.[0] === 1 }, documents); 107 | const search8 = findMultipleDocuments({ text: /foo/, object: (value: any) => value?.foo === 'bar' }, documents); 108 | 109 | assertEquals(search1, [0, 1, 2, 3, 4, 5]); 110 | assertEquals(search2, [0, 1, 2, 3, 4]); 111 | assertEquals(search3, [0, 1, 2, 3]); 112 | assertEquals(search4, [0, 1, 2]); 113 | assertEquals(search5, [0, 1]); 114 | assertEquals(search6, [0]); 115 | assertEquals(search7, [0, 1]); 116 | assertEquals(search8, [0]); 117 | }); 118 | 119 | Deno.test(`${yellow('[core.ts]')} findMultipleDocuments (No criteria)`, () => { 120 | const documents: any = [ 121 | { text: 'foo' }, 122 | { text: 'bar' }, 123 | { text: 'baz' }, 124 | ]; 125 | 126 | const search1 = findMultipleDocuments({}, documents); 127 | const search2 = findMultipleDocuments(undefined, documents); 128 | const search3 = findMultipleDocuments({}, []); 129 | const search4 = findMultipleDocuments(undefined, []); 130 | 131 | assertEquals(search1, [0, 1, 2]); 132 | assertEquals(search2, [0, 1, 2]); 133 | assertEquals(search3, []); 134 | assertEquals(search4, []); 135 | }); 136 | 137 | Deno.test(`${yellow('[core.ts]')} findMultipleDocuments (Search function)`, () => { 138 | const documents: any = [ 139 | { text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3], object: { foo: 'bar' } }, 140 | { text: 'foo', number: 42, boolean: true, nothing: null, array: [1, 2, 3] }, 141 | { text: 'foo', number: 42, boolean: true, nothing: null }, 142 | { text: 'foo', number: 42, boolean: true }, 143 | { text: 'foo', number: 42 }, 144 | { text: 'foo' }, 145 | ]; 146 | 147 | const search1 = findMultipleDocuments(() => true, documents); 148 | const search2 = findMultipleDocuments(() => false, documents); 149 | const search3 = findMultipleDocuments((value: any) => value?.nothing === null, documents); 150 | 151 | assertEquals(search1, [0, 1, 2, 3, 4, 5]); 152 | assertEquals(search2, []); 153 | assertEquals(search3, [0, 1, 2]); 154 | }); 155 | 156 | Deno.test(`${yellow('[core.ts]')} updateDocument (Basic)`, () => { 157 | const updated = updateDocument({ test: 1, test2: 'foo', test3: true, test4: null, test6: 123 }, { test: 42, test2: 'bar', test3: false, test4: 'notNull', test5: 'NewField', test6: undefined }); 158 | assertEquals(updated, { test: 42, test2: 'bar', test3: false, test4: 'notNull', test5: 'NewField' }); 159 | }); 160 | 161 | Deno.test(`${yellow('[core.ts]')} updateDocument (Partly)`, () => { 162 | const updated = updateDocument( 163 | { test: 42, test2: { value: 42 }, test3: ['foo', true, { value: 'bar' }] }, 164 | { test2: [1, 2, 3], test3: { foo: 'bar' } } 165 | ); 166 | assertEquals(updated, { test: 42, test2: [1, 2, 3], test3: { foo: 'bar' } }); 167 | }); 168 | 169 | Deno.test(`${yellow('[core.ts]')} updateDocument (No changes)`, () => { 170 | const updated = updateDocument( 171 | { test: 42, test2: { value: 42 }, test3: ['foo', true, { value: 'bar' }] }, 172 | {} 173 | ); 174 | assertEquals(updated, { test: 42, test2: { value: 42 }, test3: ['foo', true, { value: 'bar' }] }); 175 | }); 176 | 177 | Deno.test(`${yellow('[core.ts]')} updateDocument (Update function)`, () => { 178 | const updated = updateDocument( 179 | { test: 'foo', test2: { value: 42 }, test3: [1, 2, 3] }, 180 | (document: any) => { 181 | document.test += 'bar'; 182 | document.test2!.value = 0; 183 | document.test3!.push(4); 184 | return document; 185 | } 186 | ); 187 | 188 | assertEquals(updated, { test: 'foobar', test2: { value: 0 }, test3: [1, 2, 3, 4] }); 189 | }); 190 | 191 | Deno.test(`${yellow('[core.ts]')} updateDocument (Update field function)`, () => { 192 | const updated = updateDocument( 193 | { test: 'foo', test2: { value: 42 }, test3: [1, 2, 3] }, 194 | { 195 | test: (value: any) => value + 'bar', 196 | test2: (value: any) => { 197 | value.value = 0; 198 | return value; 199 | }, 200 | test3: (value: any) => { 201 | value.push(4); 202 | return value; 203 | }, 204 | } 205 | ); 206 | 207 | assertEquals(updated, { test: 'foobar', test2: { value: 0 }, test3: [1, 2, 3, 4] }); 208 | }); 209 | 210 | Deno.test(`${yellow('[core.ts]')} updateDocument (Empty object)`, () => { 211 | const updated = updateDocument( 212 | { test: true }, 213 | { test: undefined } 214 | ); 215 | assertEquals(updated, {}); 216 | }); 217 | 218 | Deno.test(`${yellow('[core.ts]')} updateDocument (Deletion)`, () => { 219 | const updated = updateDocument( 220 | { test: true }, 221 | () => null 222 | ); 223 | assertEquals(updated, {}); 224 | }); 225 | 226 | Deno.test(`${yellow('[core.ts]')} updateDocument (Immutability)`, () => { 227 | const array: any = [1, 2, 3, { field: 'value' }]; 228 | const document = { test: [0], foo: 'bar' }; 229 | 230 | const updated = updateDocument( 231 | document, 232 | { test: array } 233 | ); 234 | 235 | array[0] = 999; 236 | array[3].field = 'changed'; 237 | document.test[0] = 999; 238 | document.foo = 'baz'; 239 | 240 | assertEquals(updated, { test: [1, 2, 3, { field: 'value' }], foo: 'bar' }); 241 | }); 242 | 243 | Deno.test(`${yellow('[core.ts]')} matchValues (Primitives)`, () => { 244 | assertEquals(matchValues('foo', 'foo'), true); 245 | assertEquals(matchValues(42, 42), true); 246 | assertEquals(matchValues(true, true), true); 247 | assertEquals(matchValues(null, null), true); 248 | assertEquals(matchValues(undefined, undefined as any), true); 249 | }); 250 | 251 | Deno.test(`${yellow('[core.ts]')} matchValues (Advanced Valid)`, () => { 252 | assertEquals( 253 | matchValues((value: any) => value === 'foo', 'foo'), 254 | true 255 | ); 256 | assertEquals(matchValues(/foo/, 'fooBar'), true); 257 | assertEquals(matchValues([1, 2, 3], [1, 2, 3]), true); 258 | assertEquals(matchValues({ boolean: true }, { boolean: true }), true); 259 | }); 260 | 261 | Deno.test(`${yellow('[core.ts]')} matchValues (Advanced Invalid)`, () => { 262 | assertEquals(matchValues('foo', 10), false); 263 | assertEquals(matchValues(/bar/, true), false); 264 | assertEquals( 265 | matchValues((value: any) => false, true), 266 | false 267 | ); 268 | assertEquals(matchValues({ array: [1, 2, 3] }, { array: [1, 2, 3, 4] }), false); 269 | assertEquals(matchValues({ invalid: new Map() as any }, { invalid: {} }), false); 270 | assertEquals(matchValues({ array: [] }, { object: {} }), false); 271 | assertEquals(matchValues(new Map() as any, new Map() as any), false); 272 | }); 273 | 274 | 275 | Deno.test(`${yellow('[core.ts]')} parseDatabaseStorage`, () => { 276 | const result = parseDatabaseStorage('[{"foo":"bar"}, {}]'); 277 | assertEquals(result, [{foo: 'bar'}]); 278 | }); 279 | 280 | Deno.test(`${yellow('[core.ts]')} parseDatabaseStorage (Empty file)`, () => { 281 | const result = parseDatabaseStorage(''); 282 | assertEquals(result, []); 283 | }); 284 | 285 | Deno.test(`${yellow('[core.ts]')} parseDatabaseStorage (Not an Array)`, () => { 286 | assertThrows(() => parseDatabaseStorage('true'), undefined, 'should be an array of objects') 287 | }); 288 | 289 | Deno.test(`${yellow('[core.ts]')} parseDatabaseStorage (Invalid Array)`, () => { 290 | assertThrows(() => parseDatabaseStorage('[true]'), undefined, 'should contain only objects') 291 | }); 292 | -------------------------------------------------------------------------------- /tests/database_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from 'https://deno.land/std@0.102.0/testing/asserts.ts'; 2 | import { dirname, fromFileUrl } from 'https://deno.land/std@0.102.0/path/mod.ts'; 3 | import { copy, ensureDir, emptyDir } from 'https://deno.land/std@0.102.0/fs/mod.ts'; 4 | import { magenta } from 'https://deno.land/std@0.102.0/fmt/colors.ts'; 5 | import { delay } from 'https://deno.land/std@0.102.0/async/mod.ts'; 6 | 7 | import { Database } from '../lib/database.ts'; 8 | 9 | // Prepare enviroment 10 | const DIRNAME = dirname(fromFileUrl(import.meta.url)); 11 | const TEMP_PATH = DIRNAME + '/temp_database_tests_enviroment'; 12 | const ENVIROMENT_PATH = DIRNAME + '/enviroment'; 13 | 14 | await ensureDir(TEMP_PATH); 15 | await emptyDir(TEMP_PATH) 16 | await copy(ENVIROMENT_PATH, TEMP_PATH, { overwrite: true }); 17 | 18 | Deno.test({ 19 | name: `${magenta('[database.ts]')} Initialization`, 20 | sanitizeResources: false, 21 | sanitizeOps: false, 22 | 23 | async fn() { 24 | new Database(); 25 | new Database({}); 26 | new Database({ 27 | path: undefined, 28 | pretty: false, 29 | autoload: true, 30 | autosave: false, 31 | immutable: false, 32 | validator: () => { }, 33 | }); 34 | } 35 | }); 36 | 37 | Deno.test({ 38 | name: `${magenta('[database.ts]')} Pretty`, 39 | sanitizeResources: false, 40 | sanitizeOps: false, 41 | 42 | async fn() { 43 | const pretty = new Database({ path: TEMP_PATH + '/pretty_test.json', pretty: true }); 44 | const notPretty = new Database({ path: TEMP_PATH + '/not_pretty_test.json', pretty: false }); 45 | 46 | pretty.documents = [{ field: 0 }]; 47 | notPretty.documents = [{ field: 0 }]; 48 | 49 | pretty.save(); 50 | notPretty.save(); 51 | 52 | await delay(100); 53 | 54 | const prettyContent = await Deno.readTextFile(TEMP_PATH + '/pretty_test.json'); 55 | assertEquals(prettyContent.includes('\t'), true); 56 | 57 | const notPrettyContent = await Deno.readTextFile(TEMP_PATH + '/not_pretty_test.json'); 58 | assertEquals(notPrettyContent.includes('\t'), false); 59 | } 60 | }); 61 | 62 | Deno.test({ 63 | name: `${magenta('[database.ts]')} Return immutability`, 64 | sanitizeResources: false, 65 | sanitizeOps: false, 66 | 67 | async fn() { 68 | const immutable = new Database({ immutable: true }); 69 | 70 | const immutable_insertOne = await immutable.insertOne({ field: 0 }); 71 | immutable_insertOne.field = 1; 72 | 73 | const immutable_insertMany = await immutable.insertMany([{ field: 0 }]); 74 | immutable_insertMany[0].field = 1; 75 | 76 | const immutable_findOne = await immutable.findOne({ field: 0 }) as any; 77 | immutable_findOne.field = 1; 78 | 79 | const immutable_findMany = await immutable.findMany({ field: 0 }); 80 | immutable_findMany[0].field = 1; 81 | immutable_findMany[1].field = 1; 82 | 83 | const immutable_updateOne = await immutable.updateOne({ field: 0 }, { field: 0 }) as any; 84 | immutable_updateOne.field = 1; 85 | 86 | const immutable_updateMany = await immutable.updateMany({ field: () => true }, {}); 87 | immutable_updateMany[0].field = 1; 88 | immutable_updateMany[1].field = 1; 89 | immutable_updateMany.splice(0, 1); 90 | 91 | // Check if something changed 92 | assertEquals(immutable.documents[0].field, 0); 93 | assertEquals(immutable.documents[1].field, 0); 94 | 95 | ///////////////////////////////////////////////// 96 | 97 | const notImmutable = new Database({ immutable: false }); 98 | 99 | const notImmutable_insertOne = await notImmutable.insertOne({ field: 0 }); 100 | notImmutable_insertOne.field = 1; 101 | assertEquals(notImmutable.documents[0].field, 1); 102 | 103 | const notImmutable_insertMany = await notImmutable.insertMany([{ field: 0 }]); 104 | notImmutable_insertMany[0].field = 1; 105 | assertEquals(notImmutable.documents[1].field, 1); 106 | 107 | const notImmutable_findOne = await notImmutable.findOne({ field: 1 }) as any; 108 | notImmutable_findOne.field = 2; 109 | assertEquals(notImmutable.documents[0].field, 2); 110 | 111 | const notImmutable_findMany = await notImmutable.findMany({ field: 1 }); 112 | notImmutable_findMany[0].field = 2; 113 | assertEquals(notImmutable.documents[0].field, 2); 114 | assertEquals(notImmutable.documents[1].field, 2); 115 | 116 | const notImmutable_updateOne = await notImmutable.updateOne({ field: 2 }, { field: 2 }) as any; 117 | notImmutable_updateOne.field = 3; 118 | assertEquals(notImmutable.documents[0].field, 3); 119 | 120 | const notImmutable_updateMany = await notImmutable.updateMany({ field: () => true }, {}); 121 | notImmutable_updateMany[0].field = 4; 122 | notImmutable_updateMany[1].field = 4; 123 | assertEquals(notImmutable.documents[0].field, 4); 124 | assertEquals(notImmutable.documents[1].field, 4); 125 | 126 | } 127 | }); 128 | 129 | Deno.test({ 130 | name: `${magenta('[database.ts]')} Storage Immutability`, 131 | sanitizeResources: false, 132 | sanitizeOps: false, 133 | 134 | async fn() { 135 | const db = new Database({ immutable: false }); // Insert should always be immutable, without any exceptions 136 | 137 | const oneDocument: any = { field: [1, 2, { foo: ['bar'] }] }; 138 | await db.insertOne(oneDocument); 139 | oneDocument.field[2].foo[0] = 'baz'; 140 | 141 | const multipleDocuments: any = [{ field: [0] }, { field: [1] }]; 142 | await db.insertMany(multipleDocuments); 143 | multipleDocuments[0].field[0] = 999; 144 | multipleDocuments[1].field[0] = 999; 145 | multipleDocuments.push({ field: [999] }); 146 | 147 | const oneUpdate = [0]; 148 | await db.updateOne({ field: [0] }, { field: oneUpdate }) as any; 149 | oneUpdate[0] = 999; 150 | 151 | const multipleUpdate = [1]; 152 | await db.updateMany({ field: [1] }, { field: multipleUpdate }) as any; 153 | multipleUpdate[0] = 999; 154 | multipleUpdate.push(999); 155 | 156 | assertEquals(db.documents, [ 157 | { field: [1, 2, { foo: ['bar'] }] }, 158 | { field: [0] }, 159 | { field: [1] } 160 | ]); 161 | } 162 | }); 163 | 164 | Deno.test({ 165 | name: `${magenta('[database.ts]')} insertOne`, 166 | sanitizeResources: false, 167 | sanitizeOps: false, 168 | 169 | async fn() { 170 | const db = new Database({ path: TEMP_PATH + '/insertOne_test.json', pretty: false, batching: false }); 171 | 172 | const inserted = await db.insertOne({ foo: 'bar' }); 173 | assertEquals(db.documents, [{ foo: 'bar' }]); 174 | assertEquals(inserted, { foo: 'bar' }); 175 | 176 | const content = await Deno.readTextFile(TEMP_PATH + '/insertOne_test.json'); 177 | assertEquals(content, '[{"foo":"bar"}]'); 178 | } 179 | }); 180 | 181 | Deno.test({ 182 | name: `${magenta('[database.ts]')} insertOne (Empty)`, 183 | sanitizeResources: false, 184 | sanitizeOps: false, 185 | 186 | async fn() { 187 | const db = new Database({ path: TEMP_PATH + '/insertOne_empty_test.json', pretty: false, batching: false }); 188 | 189 | const inserted = await db.insertOne({}); 190 | assertEquals(db.documents, []); 191 | assertEquals(inserted, {}); 192 | 193 | const content = await Deno.readTextFile(TEMP_PATH + '/insertOne_empty_test.json'); 194 | assertEquals(content, '[]'); 195 | } 196 | }); 197 | 198 | Deno.test({ 199 | name: `${magenta('[database.ts]')} insertMany`, 200 | sanitizeResources: false, 201 | sanitizeOps: false, 202 | 203 | async fn() { 204 | const db = new Database({ path: TEMP_PATH + '/insertMany_test.json', pretty: false, batching: false }); 205 | 206 | const inserted = await db.insertMany([{ foo: 'bar' }, { bar: 'foo' }, {},]); 207 | assertEquals(db.documents, [{ foo: 'bar' }, { bar: 'foo' }]); 208 | assertEquals(inserted, [{ foo: 'bar' }, { bar: 'foo' }]); 209 | 210 | const content = await Deno.readTextFile(TEMP_PATH + '/insertMany_test.json'); 211 | assertEquals(content, '[{"foo":"bar"},{"bar":"foo"}]'); 212 | } 213 | }); 214 | 215 | Deno.test({ 216 | name: `${magenta('[database.ts]')} insertMany (Empty)`, 217 | sanitizeResources: false, 218 | sanitizeOps: false, 219 | 220 | async fn() { 221 | const db = new Database({ path: TEMP_PATH + '/insertMany_empty_test.json', pretty: false, batching: false }); 222 | 223 | const inserted = await db.insertMany([{}, {},]); 224 | assertEquals(db.documents, []); 225 | assertEquals(inserted, {}); 226 | 227 | const content = await Deno.readTextFile(TEMP_PATH + '/insertMany_empty_test.json'); 228 | assertEquals(content, '[]'); 229 | } 230 | }); 231 | 232 | Deno.test({ 233 | name: `${magenta('[database.ts]')} findOne`, 234 | sanitizeResources: false, 235 | sanitizeOps: false, 236 | 237 | async fn() { 238 | const db = new Database({ path: TEMP_PATH + '/findOne_test.json', pretty: false, batching: false }); 239 | 240 | const initialData = [ 241 | { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }, 242 | { id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }, 243 | { id: 3, text: 'three', boolean: true, empty: null, array: [3], object: {} }, 244 | ]; 245 | 246 | await db.insertMany(initialData); 247 | 248 | const found1 = await db.findOne({ id: 1 }); 249 | const found2 = await db.findOne({ id: 2, notDefined: undefined, object: {} }); 250 | const found3 = await db.findOne({ id: 3, text: /three/, boolean: (value) => value === true, empty: null, array: [3], object: {} }); 251 | const found4 = await db.findOne((value) => value.id === 1); 252 | const found5 = await db.findOne({}); 253 | const notFound1 = await db.findOne({ object: [] }); 254 | const notFound2 = await db.findOne((value) => value.id === 4); 255 | 256 | assertEquals(found1, { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }); 257 | assertEquals(found2, { id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }); 258 | assertEquals(found3, { id: 3, text: 'three', boolean: true, empty: null, array: [3], object: {} }); 259 | assertEquals(found4, { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }); 260 | assertEquals(found5, { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }); 261 | assertEquals(notFound1, null); 262 | assertEquals(notFound2, null); 263 | 264 | (found1 as any).id = 999; 265 | (found2 as any).id = 999; 266 | (found3 as any).id = 999; 267 | (found4 as any).array.push(999); 268 | (found5 as any).array.push(999); 269 | 270 | assertEquals(db.documents, initialData); 271 | } 272 | }); 273 | 274 | Deno.test({ 275 | name: `${magenta('[database.ts]')} findMany`, 276 | sanitizeResources: false, 277 | sanitizeOps: false, 278 | 279 | async fn() { 280 | const db = new Database({ path: TEMP_PATH + '/findMany_test.json', pretty: false, batching: false }); 281 | 282 | const initialData = [ 283 | { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }, 284 | { id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }, 285 | { id: 3, text: 'three', boolean: true, empty: null, array: [3], object: {} }, 286 | ]; 287 | 288 | await db.insertMany(initialData); 289 | 290 | const found1 = await db.findMany({ id: 1 }); 291 | const found2 = await db.findMany({ id: 2, notDefined: undefined, object: {} }); 292 | const found3 = await db.findMany({ boolean: (value) => value === true, object: {} }); 293 | const found4 = await db.findMany((value) => value.id === 1); 294 | const found5 = await db.findMany({}); 295 | const notFound1 = await db.findMany({ object: [] }); 296 | const notFound2 = await db.findMany((value) => value.id === 4); 297 | 298 | assertEquals(found1, [{ id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }]); 299 | assertEquals(found2, [{ id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }]); 300 | assertEquals(found3, initialData); 301 | assertEquals(found4, [{ id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }]); 302 | assertEquals(found5, initialData); 303 | assertEquals(notFound1, []); 304 | assertEquals(notFound2, []); 305 | 306 | (found1[0] as any).id = 999; 307 | (found2[0] as any).id = 999; 308 | (found3[0] as any).id = 999; 309 | (found4[0] as any).array.push(999); 310 | (found5[0] as any).array.push(999); 311 | 312 | assertEquals(db.documents, initialData); 313 | } 314 | }); 315 | 316 | Deno.test({ 317 | name: `${magenta('[database.ts]')} updateOne`, 318 | sanitizeResources: false, 319 | sanitizeOps: false, 320 | 321 | async fn() { 322 | const db = new Database({ path: TEMP_PATH + '/updateOne_test.json', pretty: false, batching: false }); 323 | 324 | const initialData = [ 325 | { id: 1, text: 'one', boolean: true, empty: null, array: [1], object: {} }, 326 | { id: 2, text: 'two', boolean: true, empty: null, array: [2], object: {} }, 327 | { id: 3, text: 'three', boolean: true, empty: null, array: [3], object: {} }, 328 | ]; 329 | 330 | await db.insertMany(initialData); 331 | 332 | const updated1 = await db.updateOne({ id: 1 }, { boolean: false }); 333 | const updated2 = await db.updateOne({ id: 2, object: {}, notDefined: undefined }, { object: { foo: 'bar' }, boolean: false }); 334 | const updated3 = await db.updateOne({ text: (value) => value === 'three' }, (doc) => { 335 | doc.id = 0; 336 | doc.text = 'zero'; 337 | doc.boolean = false; 338 | (doc.array as number[])[0] = 0; 339 | return doc; 340 | }); 341 | const updated4 = await db.updateOne((value) => value.id === 1, { array: [1, undefined as any], boolean: false }); 342 | const updated5 = await db.updateOne({ id: 0 }, { object: (value) => value, boolean: false }); 343 | 344 | const deleted1 = await db.updateOne({ id: 1 }, () => null); 345 | const deleted2 = await db.updateOne({ id: 2 }, () => ({ id: undefined as any })); 346 | 347 | assertEquals(updated1, { id: 1, text: 'one', boolean: false, empty: null, array: [1], object: {} }); 348 | assertEquals(updated2, { id: 2, text: 'two', boolean: false, empty: null, array: [2], object: { foo: 'bar' } }); 349 | assertEquals(updated3, { id: 0, text: 'zero', boolean: false, empty: null, array: [0], object: {} }); 350 | assertEquals(updated4, { id: 1, text: 'one', boolean: false, empty: null, array: [1, null], object: {} }); 351 | assertEquals(updated5, { id: 0, text: 'zero', boolean: false, empty: null, array: [0], object: { } }); 352 | assertEquals(deleted1, {}); 353 | assertEquals(deleted2, {}); 354 | 355 | (updated1 as any).id = 999; 356 | (updated2 as any).id = 999; 357 | (updated3 as any).id = 999; 358 | (updated4 as any).array.push(999); 359 | (updated5 as any).array.push(999); 360 | 361 | assertEquals(db.documents, [{ id: 0, text: 'zero', boolean: false, empty: null, array: [0], object: {} }]); 362 | } 363 | }); 364 | 365 | Deno.test({ 366 | name: `${magenta('[database.ts]')} count`, 367 | sanitizeResources: false, 368 | sanitizeOps: false, 369 | 370 | async fn() { 371 | const db = new Database({ pretty: false, batching: false }); 372 | 373 | const amount1 = await db.count(); 374 | await db.insertOne({ foo: 'bar' }); 375 | const amount2 = await db.count({}); 376 | const amount3 = await db.count({ foo: 'bar' }); 377 | const amount4 = await db.count({ foo: 'baz' }); 378 | 379 | assertEquals(amount1, 0); 380 | assertEquals(amount2, 1); 381 | assertEquals(amount3, 1); 382 | assertEquals(amount4, 0); 383 | } 384 | }); 385 | 386 | Deno.test({ 387 | name: `${magenta('[database.ts]')} drop`, 388 | sanitizeResources: false, 389 | sanitizeOps: false, 390 | 391 | async fn() { 392 | const db = new Database({ path: TEMP_PATH + '/drop_test.json', pretty: false, batching: false }); 393 | 394 | await db.insertMany([ 395 | { id: 1 }, 396 | { id: 2 }, 397 | { id: undefined as any }, 398 | {} 399 | ]); 400 | 401 | await db.drop(); 402 | assertEquals(db.documents, []); 403 | 404 | const content = await Deno.readTextFile(TEMP_PATH + '/drop_test.json'); 405 | assertEquals(content, '[]'); 406 | } 407 | }); 408 | 409 | // TODO: Finish testing 410 | 411 | // Remove temp files 412 | window.addEventListener('unload', () => { 413 | Deno.removeSync(TEMP_PATH, { recursive: true }); 414 | }); 415 | -------------------------------------------------------------------------------- /tests/enviroment/test_storage.json: -------------------------------------------------------------------------------- 1 | [ 2 | {}, 3 | { 4 | "username": "foo", 5 | "password": "bar", 6 | "numbers": [ 7 | 1, 8 | 2 9 | ] 10 | }, 11 | { 12 | "username": "foo", 13 | "password": "bar", 14 | "numbers": [ 15 | 3, 16 | 4 17 | ] 18 | }, 19 | { 20 | "username": "foo", 21 | "password": "bar", 22 | "numbers": [ 23 | 5, 24 | 6 25 | ] 26 | }, 27 | { 28 | "username": "foo", 29 | "password": "bar", 30 | "numbers": "invalid" 31 | }, 32 | {} 33 | ] 34 | -------------------------------------------------------------------------------- /tests/helpers_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.102.0/testing/asserts.ts'; 2 | import { blue } from 'https://deno.land/std@0.102.0/fmt/colors.ts'; 3 | 4 | import { 5 | moreThan, 6 | moreThanOrEqual, 7 | lessThan, 8 | lessThanOrEqual, 9 | between, 10 | betweenOrEqual, 11 | exists, 12 | type, 13 | includes, 14 | length, 15 | someElementMatch, 16 | everyElementMatch, 17 | and, 18 | or, 19 | not 20 | } from '../lib/helpers.ts'; 21 | 22 | Deno.test(`${blue('[helpers.ts]')} moreThan`, () => { 23 | const search = moreThan(5); 24 | assertEquals(search(6), true); 25 | assertEquals(search('foo'), false); 26 | }); 27 | 28 | Deno.test(`${blue('[helpers.ts]')} moreThanOrEqual`, () => { 29 | const search = moreThanOrEqual(5); 30 | assertEquals(search(5), true); 31 | assertEquals(search(6), true); 32 | assertEquals(search('foo'), false); 33 | }); 34 | 35 | Deno.test(`${blue('[helpers.ts]')} lessThan`, () => { 36 | const search = lessThan(5); 37 | assertEquals(search(4), true); 38 | assertEquals(search('foo'), false); 39 | }); 40 | 41 | Deno.test(`${blue('[helpers.ts]')} lessThanOrEqual`, () => { 42 | const search = lessThanOrEqual(5); 43 | assertEquals(search(5), true); 44 | assertEquals(search(4), true); 45 | assertEquals(search('foo'), false); 46 | }); 47 | 48 | Deno.test(`${blue('[helpers.ts]')} between`, () => { 49 | const search = between(5, 10); 50 | assertEquals(search(7), true); 51 | assertEquals(search(5), false); 52 | assertEquals(search('foo'), false); 53 | }); 54 | 55 | Deno.test(`${blue('[helpers.ts]')} betweenOrEqual`, () => { 56 | const search = betweenOrEqual(5, 10); 57 | assertEquals(search(7), true); 58 | assertEquals(search(5), true); 59 | assertEquals(search('foo'), false); 60 | }); 61 | 62 | Deno.test(`${blue('[helpers.ts]')} exists`, () => { 63 | const search = exists(); 64 | assertEquals(search(1), true); 65 | assertEquals(search({}), true); 66 | assertEquals(search(undefined), false); 67 | }); 68 | 69 | Deno.test(`${blue('[helpers.ts]')} type`, () => { 70 | const searchString = type('string'); 71 | assertEquals(searchString('foo'), true); 72 | assertEquals(searchString(0), false); 73 | 74 | const searchNumber = type('number'); 75 | assertEquals(searchNumber(1), true); 76 | assertEquals(searchNumber({}), false); 77 | 78 | const searchBoolean = type('boolean'); 79 | assertEquals(searchBoolean(true), true); 80 | assertEquals(searchBoolean({}), false); 81 | 82 | const searchArray = type('array'); 83 | assertEquals(searchArray([]), true); 84 | assertEquals(searchArray({}), false); 85 | 86 | const searchObject = type('object'); 87 | assertEquals(searchObject({}), true); 88 | assertEquals(searchObject([]), false); 89 | 90 | const searchNull = type('null'); 91 | assertEquals(searchNull(null), true); 92 | assertEquals(searchNull({}), false); 93 | }); 94 | 95 | Deno.test(`${blue('[helpers.ts]')} includes`, () => { 96 | const search = includes(5); 97 | assertEquals(search([1, 2, 5, 6]), true); 98 | assertEquals(search([5]), true); 99 | assertEquals(search({}), false); 100 | assertEquals(search([1, 2, 3]), false); 101 | }); 102 | 103 | Deno.test(`${blue('[helpers.ts]')} length`, () => { 104 | const search = length(0); 105 | assertEquals(search([]), true); 106 | assertEquals(search([0]), false); 107 | assertEquals(search({}), false); 108 | }); 109 | 110 | Deno.test(`${blue('[helpers.ts]')} someElementMatch`, () => { 111 | const search = someElementMatch('bar', /bar/g); 112 | assertEquals(search(['foo', 'bar', 'baz']), true); 113 | assertEquals(search(['foo', 'barbaz']), false); 114 | }); 115 | 116 | Deno.test(`${blue('[helpers.ts]')} everyElementMatch`, () => { 117 | const search = everyElementMatch('bar', (value) => typeof value === 'string'); 118 | assertEquals(search(['bar', 'bar', 'bar']), true); 119 | assertEquals(search(['bar', 'bar', { foo: 'bar'}]), false); 120 | }); 121 | 122 | Deno.test(`${blue('[helpers.ts]')} and`, () => { 123 | const search = and('bar', (value) => typeof value === 'string'); 124 | assertEquals(search('bar'), true); 125 | assertEquals(search('foobar'), false); 126 | }); 127 | 128 | Deno.test(`${blue('[helpers.ts]')} or`, () => { 129 | const search = or('bar', 'foo'); 130 | assertEquals(search('bar'), true); 131 | assertEquals(search('foo'), true); 132 | assertEquals(search('foobar'), false); 133 | }); 134 | 135 | Deno.test(`${blue('[helpers.ts]')} not`, () => { 136 | const search = not('bar'); 137 | assertEquals(search('foo'), true); 138 | assertEquals(search('bar'), false); 139 | }); 140 | -------------------------------------------------------------------------------- /tests/reader_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows, assertThrowsAsync } from 'https://deno.land/std@0.102.0/testing/asserts.ts'; 2 | import { copy, ensureDir, emptyDir, exists, existsSync } from 'https://deno.land/std@0.102.0/fs/mod.ts'; 3 | import { dirname, fromFileUrl } from 'https://deno.land/std@0.102.0/path/mod.ts'; 4 | import { cyan } from 'https://deno.land/std@0.102.0/fmt/colors.ts'; 5 | 6 | import { Reader } from '../lib/reader.ts'; 7 | 8 | // Prepare inviroment 9 | const DIRNAME = dirname(fromFileUrl(import.meta.url)); 10 | const TEMP_PATH = DIRNAME + '/temp_reader_tests_enviroment'; 11 | const ENVIROMENT_PATH = DIRNAME + '/enviroment'; 12 | 13 | await ensureDir(TEMP_PATH); 14 | await emptyDir(TEMP_PATH) 15 | await copy(ENVIROMENT_PATH, TEMP_PATH, { overwrite: true }); 16 | 17 | Deno.test({ 18 | name: `${cyan('[reader.ts]')} Read Sync (Basic)`, 19 | sanitizeResources: false, 20 | sanitizeOps: false, 21 | 22 | fn() { 23 | const content = Reader.readSync(TEMP_PATH + '/test_storage.json'); 24 | const originalContent = Deno.readTextFileSync(TEMP_PATH + '/test_storage.json'); 25 | 26 | assertEquals(content, originalContent); 27 | } 28 | }); 29 | 30 | Deno.test({ 31 | name: `${cyan('[reader.ts]')} Read Sync (Path doesn't exists)`, 32 | sanitizeResources: false, 33 | sanitizeOps: false, 34 | 35 | fn() { 36 | const content = Reader.readSync(TEMP_PATH + '/created/storage_sync.json'); 37 | assertEquals(content, '[]'); 38 | 39 | const fileExists = existsSync(TEMP_PATH + '/created/storage_sync.json'); 40 | assertEquals(fileExists, true); 41 | 42 | const fileContent = Deno.readTextFileSync(TEMP_PATH + '/created/storage_sync.json'); 43 | assertEquals(fileContent, '[]'); 44 | } 45 | }); 46 | 47 | Deno.test({ 48 | name: `${cyan('[reader.ts]')} Read Sync (Invalid path)`, 49 | sanitizeResources: false, 50 | sanitizeOps: false, 51 | 52 | fn() { 53 | assertThrows(() => Reader.readSync(TEMP_PATH)); // Its directory, not a file 54 | } 55 | }); 56 | 57 | Deno.test({ 58 | name: `${cyan('[reader.ts]')} Read (Basic)`, 59 | sanitizeResources: false, 60 | sanitizeOps: false, 61 | 62 | async fn() { 63 | const content = await Reader.read(TEMP_PATH + '/test_storage.json'); 64 | const originalContent = await Deno.readTextFile(TEMP_PATH + '/test_storage.json'); 65 | 66 | assertEquals(content, originalContent); 67 | } 68 | }); 69 | 70 | Deno.test({ 71 | name: `${cyan('[reader.ts]')} Read (Path doesn't exists)`, 72 | sanitizeResources: false, 73 | sanitizeOps: false, 74 | 75 | async fn() { 76 | const content = await Reader.read(TEMP_PATH + '/created/storage_async.json'); 77 | assertEquals(content, '[]'); 78 | 79 | const fileExists = await exists(TEMP_PATH + '/created/storage_async.json'); 80 | assertEquals(fileExists, true); 81 | 82 | const fileContent = await Deno.readTextFile(TEMP_PATH + '/created/storage_async.json'); 83 | assertEquals(fileContent, '[]'); 84 | } 85 | }); 86 | 87 | Deno.test({ 88 | name: `${cyan('[reader.ts]')} Read (Invalid path)`, 89 | sanitizeResources: false, 90 | sanitizeOps: false, 91 | 92 | async fn() { 93 | await assertThrowsAsync(async () => await Reader.read(TEMP_PATH)); // Its directory, not a file 94 | } 95 | }); 96 | 97 | // Remove temp files 98 | window.addEventListener('unload', () => { 99 | Deno.removeSync(TEMP_PATH, { recursive: true }); 100 | }); 101 | -------------------------------------------------------------------------------- /tests/utils_test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertEquals, assertNotEquals } from 'https://deno.land/std@0.102.0/testing/asserts.ts'; 2 | import { green } from 'https://deno.land/std@0.102.0/fmt/colors.ts'; 3 | 4 | import { 5 | cleanArray, 6 | numbersList, 7 | isObjectEmpty, 8 | getObjectLength, 9 | getPathFilename, 10 | getPathDirname, 11 | deepClone, 12 | deepCompare, 13 | prepareArray, 14 | prepareObject, 15 | isPrimitive, 16 | isString, 17 | isNumber, 18 | isBoolean, 19 | isUndefined, 20 | isNull, 21 | isFunction, 22 | isArray, 23 | isObject, 24 | isRegExp 25 | } from '../lib/utils.ts'; 26 | 27 | Deno.test(`${green('[utils.ts]')} cleanArray`, () => { 28 | const array = [1, 2, 3, 4, , 6, undefined, null]; 29 | delete array[5]; 30 | 31 | assertEquals(cleanArray(array), [1, 2, 3, 4, undefined, null]); 32 | }); 33 | 34 | Deno.test(`${green('[utils.ts]')} numbersList`, () => { 35 | assertEquals(numbersList(5), [0, 1, 2, 3, 4, 5]); 36 | assertEquals(numbersList(1), [0, 1]); 37 | assertEquals(numbersList(0), [0]); 38 | assertEquals(numbersList(-1), []); 39 | }); 40 | 41 | Deno.test(`${green('[utils.ts]')} isObjectEmpty`, () => { 42 | assertEquals(isObjectEmpty({}), true); 43 | assertEquals(isObjectEmpty({ key: undefined }), false); 44 | }); 45 | 46 | Deno.test(`${green('[utils.ts]')} getObjectLength`, () => { 47 | assertEquals(getObjectLength({}), 0); 48 | assertEquals(getObjectLength({ key: undefined }), 1); 49 | assertEquals(getObjectLength({ test1: 1, test2: 2, test3: 3 }), 3); 50 | }); 51 | 52 | Deno.test(`${green('[utils.ts]')} getPathFilename`, () => { 53 | assertEquals(getPathFilename('foo.json'), 'foo.json'); 54 | assertEquals(getPathFilename('./foo/bar.json'), 'bar.json'); 55 | assertEquals(getPathFilename('foo/bar/baz.json'), 'baz.json'); 56 | assertEquals(getPathFilename('/foo/bar.json'), 'bar.json'); 57 | assertEquals(getPathFilename('//foo//bar.json'), 'bar.json'); 58 | assertEquals(getPathFilename('\\foo\\bar.json'), 'bar.json'); 59 | }); 60 | 61 | Deno.test(`${green('[utils.ts]')} getPathDirname`, () => { 62 | assertEquals(getPathDirname('foo.json'), ''); 63 | assertEquals(getPathDirname('./foo/bar.json'), './foo'); 64 | assertEquals(getPathDirname('foo/bar/baz.json'), 'foo/bar'); 65 | assertEquals(getPathDirname('/foo/bar.json'), 'foo'); 66 | assertEquals(getPathDirname('//foo//bar.json'), 'foo'); 67 | assertEquals(getPathDirname('\\foo\\bar.json'), 'foo'); 68 | }); 69 | 70 | Deno.test(`${green('[utils.ts]')} deepClone (Primitives)`, () => { 71 | const number = 42; 72 | const string = 'foo'; 73 | const boolean = true; 74 | const undefinedValue = undefined; 75 | const nullValue = null; 76 | 77 | const numberClone = deepClone(number); 78 | const stringClone = deepClone(string); 79 | const booleanClone = deepClone(boolean); 80 | const undefinedClone = deepClone(undefinedValue); 81 | const nullClone = deepClone(nullValue); 82 | 83 | assertEquals(number, numberClone); 84 | assertEquals(string, stringClone); 85 | assertEquals(boolean, booleanClone); 86 | assertEquals(undefinedValue, undefinedClone); 87 | assertEquals(nullValue, nullClone); 88 | }); 89 | 90 | Deno.test(`${green('[utils.ts]')} deepClone (Objects & Arrays)`, () => { 91 | const array = [42, 'foo', true, null, undefined]; 92 | const object = { a: 42, b: 'foo', c: true, d: null, e: undefined }; 93 | 94 | const arrayClone = deepClone(array); 95 | const objectClone = deepClone(object); 96 | 97 | assertEquals(array, arrayClone); 98 | assertEquals(object, objectClone); 99 | assert(array !== arrayClone); 100 | assert(object !== objectClone); 101 | 102 | array[0] = 0; 103 | object.a = 0; 104 | 105 | assertNotEquals(array, arrayClone); 106 | assertNotEquals(object, objectClone); 107 | }); 108 | 109 | Deno.test(`${green('[utils.ts]')} deepClone (Mixed)`, () => { 110 | const object: any = { 111 | a: 1, 112 | b: 'bar', 113 | c: true, 114 | d: undefined, 115 | e: null, 116 | f: { test: null, value: undefined, x: [1, 2, 3], z: { foo: 'bar' } }, 117 | g: [1, true, 'baz', null, undefined, { test: 1 }, [1, 2, 3]], 118 | }; 119 | 120 | const objectClone = deepClone(object); 121 | 122 | assertEquals(objectClone, object); 123 | assert(objectClone !== object); 124 | 125 | object.a = 0; 126 | assert(object.a !== objectClone.a); 127 | 128 | object.f.x[0] = 0; 129 | assert(object.f.x[0] !== objectClone.f.x[0]); 130 | 131 | object.g = 0; 132 | assert(object.g !== objectClone.g); 133 | }); 134 | 135 | Deno.test(`${green('[utils.ts]')} deepCompare (Primitives)`, () => { 136 | const string = 'foo'; 137 | const boolean = true; 138 | const number = 42; 139 | const nullValue = null; 140 | const undefinedValue = undefined; 141 | 142 | assertEquals(deepCompare(string, 'bar'), false); 143 | assertEquals(deepCompare(boolean, false), false); 144 | assertEquals(deepCompare(number, 0), false); 145 | assertEquals(deepCompare(nullValue, undefined), false); 146 | assertEquals(deepCompare(undefinedValue, null), false); 147 | 148 | assertEquals(deepCompare(string, 'foo'), true); 149 | assertEquals(deepCompare(boolean, true), true); 150 | assertEquals(deepCompare(number, 42), true); 151 | assertEquals(deepCompare(nullValue, null), true); 152 | assertEquals(deepCompare(undefinedValue, undefined), true); 153 | }); 154 | 155 | Deno.test(`${green('[utils.ts]')} deepCompare (Objects & Arrays)`, () => { 156 | const array = [1, 'test', true, null, undefined]; 157 | const object = { a: 1, b: 'test', c: true, d: null, e: undefined }; 158 | 159 | assertEquals(deepCompare(array, array), true); 160 | assertEquals(deepCompare(object, object), true); 161 | 162 | assertEquals(deepCompare(array, [1, 'test', true, null]), false); 163 | assertEquals(deepCompare(object, { a: 1, b: 'test', c: true, d: null }), false); 164 | }); 165 | 166 | Deno.test(`${green('[utils.ts]')} prepareArray`, () => { 167 | const array = [ 168 | 1, 169 | 'bar', 170 | true, 171 | undefined, 172 | null, 173 | new Date(), 174 | { test: null, test2: undefined, test3: [1, 2, 3], test4: new Map() }, 175 | [null, undefined, { test: new Map(), test2: undefined }], 176 | ]; 177 | 178 | prepareArray(array); 179 | assertEquals(array, [1, 'bar', true, null, null, null, { test: null, test3: [1, 2, 3] }, [null, null, {}]]); 180 | }); 181 | 182 | Deno.test(`${green('[utils.ts]')} prepareObject`, () => { 183 | const object: any = { 184 | a: 1, 185 | b: 'foo', 186 | c: true, 187 | d: undefined, 188 | e: null, 189 | f: { value: undefined, value2: new Map() }, 190 | g: [1, 2, undefined, new Date()], 191 | }; 192 | 193 | prepareObject(object); 194 | assertEquals(object, { 195 | a: 1, 196 | b: 'foo', 197 | c: true, 198 | e: null, 199 | f: {}, 200 | g: [1, 2, null, null], 201 | }); 202 | }); 203 | 204 | Deno.test(`${green('[utils.ts]')} isPrimitive`, () => { 205 | assertEquals(isPrimitive('foo'), true); 206 | assertEquals(isPrimitive(42), true); 207 | assertEquals(isPrimitive(false), true); 208 | assertEquals(isPrimitive(null), true); 209 | assertEquals(isPrimitive({}), false); 210 | assertEquals(isPrimitive([]), false); 211 | assertEquals(isPrimitive(undefined), false); 212 | assertEquals(isPrimitive(/foo/), false); 213 | }); 214 | 215 | Deno.test(`${green('[utils.ts]')} isString`, () => { 216 | assertEquals(isString('foo'), true); 217 | assertEquals(isString(42), false); 218 | assertEquals(isString({}), false); 219 | }); 220 | 221 | Deno.test(`${green('[utils.ts]')} isNumber`, () => { 222 | assertEquals(isNumber(42), true); 223 | assertEquals(isNumber(NaN), false); 224 | assertEquals(isNumber({}), false); 225 | }); 226 | 227 | Deno.test(`${green('[utils.ts]')} isBoolean`, () => { 228 | assertEquals(isBoolean(false), true); 229 | assertEquals(isBoolean(0), false); 230 | assertEquals(isBoolean({}), false); 231 | }); 232 | 233 | Deno.test(`${green('[utils.ts]')} isUndefined`, () => { 234 | assertEquals(isUndefined(undefined), true); 235 | assertEquals(isUndefined(null), false); 236 | assertEquals(isUndefined({}), false); 237 | }); 238 | 239 | Deno.test(`${green('[utils.ts]')} isNull`, () => { 240 | assertEquals(isNull(null), true); 241 | assertEquals(isNull(undefined), false); 242 | assertEquals(isNull({}), false); 243 | }); 244 | 245 | Deno.test(`${green('[utils.ts]')} isFunction`, () => { 246 | assertEquals(isFunction(() => { }), true); 247 | assertEquals(isFunction(0), false); 248 | assertEquals(isFunction({}), false); 249 | }); 250 | 251 | Deno.test(`${green('[utils.ts]')} isArray`, () => { 252 | assertEquals(isArray([]), true); 253 | assertEquals(isArray(42), false); 254 | assertEquals(isArray({}), false); 255 | }); 256 | 257 | Deno.test(`${green('[utils.ts]')} isObject`, () => { 258 | assertEquals(isObject({}), true); 259 | assertEquals(isObject(new Date()), false); 260 | assertEquals(isObject([]), false); 261 | }); 262 | 263 | Deno.test(`${green('[utils.ts]')} isRegExp`, () => { 264 | assertEquals(isRegExp(/foo/), true); 265 | assertEquals(isRegExp(new Date()), false); 266 | assertEquals(isRegExp({}), false); 267 | }); 268 | -------------------------------------------------------------------------------- /tests/writer_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.102.0/testing/asserts.ts'; 2 | import { ensureDir, emptyDir } from 'https://deno.land/std@0.102.0/fs/mod.ts'; 3 | import { delay } from 'https://deno.land/std@0.102.0/async/mod.ts'; 4 | 5 | import { red } from 'https://deno.land/std@0.102.0/fmt/colors.ts'; 6 | import { dirname, fromFileUrl } from 'https://deno.land/std@0.102.0/path/mod.ts'; 7 | 8 | import { Writer } from '../lib/writer.ts'; 9 | 10 | // Prepare enviroment 11 | const DIRNAME = dirname(fromFileUrl(import.meta.url)); 12 | const TEMP_PATH = DIRNAME + '/temp_writer_tests_enviroment'; 13 | 14 | await ensureDir(TEMP_PATH); 15 | await emptyDir(TEMP_PATH); 16 | 17 | Deno.test({ 18 | name: `${red('[writer.ts]')} Write`, 19 | sanitizeResources: false, 20 | sanitizeOps: false, 21 | 22 | async fn() { 23 | const writer = new Writer(TEMP_PATH + '/test_storage_1.json'); 24 | 25 | await writer.write([{ foo: 'bar' }], false); 26 | 27 | const content = await Deno.readTextFile(TEMP_PATH + '/test_storage_1.json'); 28 | assertEquals(content, '[{"foo":"bar"}]'); 29 | } 30 | }); 31 | 32 | Deno.test({ 33 | name: `${red('[writer.ts]')} Batch write`, 34 | sanitizeResources: false, 35 | sanitizeOps: false, 36 | 37 | async fn() { 38 | const writer = new Writer(TEMP_PATH + '/test_storage_2.json'); 39 | 40 | writer.batchWrite([{ foo: 'old' }], false); 41 | writer.batchWrite([{ foo: 'new' }], false); 42 | 43 | await delay(100); 44 | 45 | const content = await Deno.readTextFile(TEMP_PATH + '/test_storage_2.json'); 46 | assertEquals(content, '[{"foo":"new"}]'); 47 | } 48 | }); 49 | 50 | // Remove temp files 51 | window.addEventListener('unload', () => { 52 | Deno.removeSync(TEMP_PATH, { recursive: true }); 53 | }); 54 | --------------------------------------------------------------------------------