├── .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 |
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 |
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 | 
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 |
21 |
22 |
23 |
24 | ADD
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 |
--------------------------------------------------------------------------------