diff --git a/CHANGELOG.md b/CHANGELOG.md index 653eaba..09a0a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,75 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -### [1.10.2] - 2023-21-07 +## [Unreleased] + +### Added + +- `Collection.values` method, which gets all distinct non-null values for a given key across a collection. + +### Changed + +- Refactored JavaScript part to be less verbose and reuse existing code better. +- Use JSDoc `{@link }` properties. +- Cleaned up and clarified README.md. +- Renamed `AllCriteria` to `AnyCriteria` to be more accurate. +- Replaced broken `NoMethods` type with a more generalized `RemoveMethods` type. +- Replaced `Writable` with more specific `Settable` and `Addable` types for set and add operations respectively. +- `Collection.select` now picks the correct return parameters directly instead of returning a partial object. + +### Fixed + +- Ran everything through a spelling checker. +- Method fields are no longer shown as valid in searches and selections. +- `Collection.editField` and `Collection.editFieldBulk` now return confirmations like the other write methods. +- `files.upload` and `files.delete` extract the Axios request and return `WriteConfirmation`s like all other methods. + +## [1.11.1] - 2024-02-12 + +### Fixed + +- Write methods being annotated as returning elements rather than confirmations +- Missing `Collection.select` return type. +- Make the JavaScript and TypeScript JSDoc entirely consistent. +- Fix file namespace being declared as an abstract class rather than a constant object. + +## [1.11.0] - 2023-12-17 + +### Changed + +- Deprecated `Collection.read_raw` and `Collection.write_raw` methods in favor of their camelCased counterparts. +- Changed type casing style to PascalCase everywhere. +- Use ES6 method notation everywhere. + +### Removed + +- `Raw` type in favor of `Record`. + +### Fixed + +- Broken Exception types +- Prettier not running on TypeScript files +- Nested keys not being typed properly +- Fix file namespace + +## [1.10.3] - 2023-11-01 + +### Added + +- Prettier + +### Fixed + +- Updated and cleaned up README.md +- Fixed types being placed under wrong namespace + +## [1.10.2] - 2023-07-21 ### Changed + - Updated README.md with working badges - Moved to pnpm for dependency version w/ tests ### Removed -- crypto module as it is now deprecated and a built-in package of node +- crypto module as it is now deprecated and a built-in node package diff --git a/README.md b/README.md index 1492088..2ed9e78 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ _Self hosted Firestore-like database with API endpoints based on micro bulk oper npm install --save firestorm-db ``` -## JavaScript Part +# JavaScript Part The JavaScript [index.js](./src/index.js) file is just an [Axios](https://www.npmjs.com/package/axios) wrapper of the library. @@ -58,16 +58,15 @@ userCollection A collection takes one required argument and one optional argument: - The name of the collection as a `String`. -- The method adder, which allows to inject methods to the get methods results. This would be a `Function` taking the element as an argument. +- The method adder, which lets you inject methods to query results. It's implemented similarly to [`Array.prototype.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map), taking an outputted element as an argument, modifying the element with methods and data inside a callback, and returning the modified element at the end. ```js const firestorm = require("firestorm-db"); -// returns a Collection instance const userCollection = firestorm.collection("users", (el) => { - el.hello = function () { - console.log(`${el.name} says hello!`); - }; + el.hello = () => console.log(`${el.name} says hello!`); + // return the modified element back with the injected method + return el; }); // if you have a 'users' table with a printable field named name @@ -81,15 +80,16 @@ Available methods for a collection: ### Read operations -| Name | Parameters | Description | -| ----------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| sha1() | none | Get the sha1 hash of the file. Can be used to see if same file content without downloading the file. | -| readRaw() | none | Returns the whole content of the JSON. ID values are injected for easier iteration, so this may be different to sha1(). | -| get(id) | id: `string \| number` | Get an element from the collection. | -| search(searchOptions, random) | searchOptions: `SearchOption[]` random?:`boolean \| number` | Search through the collection You can randomize the output order with random as true or a given seed. | -| searchKeys(keys) | keys: `string[] \| number[]` | Search specific keys through the collection. | -| select(selectOption) | selectOption: `{ field: string[] }` | Get only selected fields from the collection Essentially an upgraded version of readRaw. | -| random(max, seed, offset) | max?: `integer >= -1` seed?: `integer` offset?:`integer >= 0` | Reads random entries of collection. | +| Name | Parameters | Description | +| ----------------------------- | ------------------------------------------------------------| --------------------------------------------------------------------------------------------------------------------- | +| sha1() | none | Get the sha1 hash of the file. Can be used to see if same file content without downloading the file. | +| readRaw() | none | Returns the whole content of the JSON. ID values are injected for easier iteration, so this may be different to sha1. | +| get(id) | id: `string \| number` | Get an element from the collection. | +| search(searchOptions, random) | searchOptions: `SearchOption[]` random?:`boolean \| number` | Search through the collection You can randomize the output order with random as true or a given seed. | +| searchKeys(keys) | keys: `string[] \| number[]` | Search specific keys through the collection. | +| select(selectOption) | selectOption: `{ fields: string[] }` | Get only selected fields from the collection Essentially an upgraded version of readRaw. | +| values(valueOption) | valueOption: `{ field: string, flatten?: boolean }` | Get all distinct non-null values for a given key across a collection. | +| random(max, seed, offset) | max?: `number >= -1` seed?: `number` offset?:`number >= 0` | Reads random entries of collection. | The search method can take one or more options to filter entries in a collection. A search option takes a `field` with a `criteria` and compares it to a `value`. You can also use the boolean `ignoreCase` option for string values. @@ -97,32 +97,32 @@ Not all criteria are available depending the field type. There are more options ### All search options available -| Criteria | Types allowed | Description | -| ---------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------- | -| `'!='` | `boolean`, `number`, `string` | Searches if the entry field's value is different from yours | -| `'=='` | `boolean`, `number`, `string` | Searches if the entry field's value is equal to yours | -| `'>='` | `number`, `string` | Searches if the entry field's value is greater or equal than yours | -| `'<='` | `number`, `string` | Searches if the entry field's value is equal to than yours | -| `'>'` | `number`, `string` | Searches if the entry field's value is greater than yours | -| `'<'` | `number`, `string` | Searches if the entry field's value is lower than yours | -| `'in'` | `number`, `string` | Searches if the entry field's value is in the array of values you gave | -| `'includes'` | `string` | Searches if the entry field's value includes your substring | -| `'startsWith'` | `string` | Searches if the entry field's value starts with your substring | -| `'endsWith'` | `string` | Searches if the entry field's value ends with your substring | -| `'array-contains'` | `Array` | Searches if the entry field's array contains your value | -| `'array-contains-any'` | `Array` | Searches if the entry field's array ends contains your one value of more inside your values array | -| `'array-length-eq'` | `number` | Searches if the entry field's array size is equal to your value | -| `'array-length-df'` | `number` | Searches if the entry field's array size is different from your value | -| `'array-length-lt'` | `number` | Searches if the entry field's array size is lower than your value | -| `'array-length-gt'` | `number` | Searches if the entry field's array size is lower greater than your value | -| `'array-length-le'` | `number` | Searches if the entry field's array size is lower or equal to your value | -| `'array-length-ge'` | `number` | Searches if the entry field's array size is greater or equal to your value | +| Criteria | Types allowed | Description | +| ---------------------- | ----------------------------- | --------------------------------------------------------------------------------- | +| `'!='` | `boolean`, `number`, `string` | Entry field's value is different from yours | +| `'=='` | `boolean`, `number`, `string` | Entry field's value is equal to yours | +| `'>='` | `number`, `string` | Entry field's value is greater or equal than yours | +| `'<='` | `number`, `string` | Entry field's value is equal to than yours | +| `'>'` | `number`, `string` | Entry field's value is greater than yours | +| `'<'` | `number`, `string` | Entry field's value is lower than yours | +| `'in'` | `number`, `string` | Entry field's value is in the array of values you gave | +| `'includes'` | `string` | Entry field's value includes your substring | +| `'startsWith'` | `string` | Entry field's value starts with your substring | +| `'endsWith'` | `string` | Entry field's value ends with your substring | +| `'array-contains'` | `Array` | Entry field's array contains your value | +| `'array-contains-any'` | `Array` | Entry field's array ends contains your one value of more inside your values array | +| `'array-length-eq'` | `number` | Entry field's array size is equal to your value | +| `'array-length-df'` | `number` | Entry field's array size is different from your value | +| `'array-length-lt'` | `number` | Entry field's array size is lower than your value | +| `'array-length-gt'` | `number` | Entry field's array size is lower greater than your value | +| `'array-length-le'` | `number` | Entry field's array size is lower or equal to your value | +| `'array-length-ge'` | `number` | Entry field's array size is greater or equal to your value | ### Write operations | Name | Parameters | Description | | ----------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------- | -| writeRaw() | none | Set the entire JSON file contents **/!\\ Very dangerous /!\\** | +| writeRaw() | none | Set the entire JSON file contents **⚠️ Very dangerous! ⚠️** | | add(value) | value: `Object` | Adds one element with autoKey into the collection | | addBulk(values) | value: `Object[]` | Adds multiple elements with autoKey into the collection | | remove(key) | key: `string \| number` | Remove one element from the collection with the corresponding key | @@ -134,25 +134,25 @@ Not all criteria are available depending the field type. There are more options ### Edit field operations -Edit objects have an `id` to get the wanted element, a `field` they want to edit, an `operation` with what to do to this field, and a possible `value`. Here is a list of operations: +Edit objects have an `id` of the element, a `field` to edit, an `operation` with what to do to this field, and a possible `value`. Here is a list of operations: -| Operation | Value required | Types allowed | Description | -| -------------- | -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `set` | Yes | `any` | Sets a field to a given value | -| `remove` | No | `any` | Removes a field from the element | -| `append` | Yes | `string` | Appends string at the end of the string field | -| `invert` | No | `any` | Inverts tate of boolean field | -| `increment` | No | `number` | Adds a number to the field, default is 1 | -| `decrement` | No | `number` | Retrieves a number to the field, default is -1 | -| `array-push ` | Yes | `any` | Push an element to the end of an array field | -| `array-delete` | Yes | `integer` | Removes and element at a certain index in an array field, check [array_splice documentation](https://www.php.net/manual/function.array-splice) offset for more infos | -| `array-splice` | Yes | `[integer, integer]` | Removes certain elements, check [array_splice documentation](https://www.php.net/manual/function.array-splice) offset and length for more infos | +| Operation | Needs value | Types allowed | Description | +| -------------- | ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `set` | Yes | `any` | Sets a field to a given value. | +| `remove` | No | `any` | Removes a field from the element. | +| `append` | Yes | `string` | Appends a new string at the end of the string field. | +| `invert` | No | `any` | Inverts the state of a boolean field. | +| `increment` | No | `number` | Adds a number to the field, default is 1. | +| `decrement` | No | `number` | Removes a number from the field, default is 1. | +| `array-push ` | Yes | `any` | Push an element to the end of an array field. | +| `array-delete` | Yes | `number` | Removes an element at a certain index in an array field. Check the PHP [array_splice](https://www.php.net/manual/function.array-splice) offset for more info. | +| `array-splice` | Yes | `[number, number]` | Removes certain elements. Check the PHP [array_splice](https://www.php.net/manual/function.array-splice) offset and length for more info. |
-## PHP files +# PHP Part -The PHP files are the ones handling files, read and writes. They also handle GET and POST requests to manipulate the database. +The PHP files are the ones handling files, read and writes. They also handle `GET` and `POST` requests to manipulate the database. ## PHP setup @@ -187,7 +187,7 @@ $database_list[$tmp->fileName] = $tmp; The database will be stored in `/.json` and `autoKey` allows or forbids some write operations. -## Files feature +## Firestorm Files File API functions are detailed in the `files.php` PHP script. If you do not want to include this functionality, then just delete this file. @@ -198,7 +198,7 @@ You have to add 2 new configuration variables to your `config.php` file: $authorized_file_extension = array('.txt', '.png'); // subfolder of uploads location, must start with dirname($_SERVER['SCRIPT_FILENAME']) -// to force a subfolder of firestorm installation +// to force a subfolder of Firestorm installation $STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; ``` @@ -234,12 +234,8 @@ form.append("overwrite", "true"); // override optional argument (do not append t const uploadPromise = firestorm.files.upload(form); uploadPromise - .then(() => { - console.log("Upload successful"); - }) - .catch((err) => { - consoler.error(err); - }); + .then(() => console.log("Upload successful")) + .catch((err) => console.error(err)); ``` ## Get a file @@ -253,12 +249,8 @@ firestorm.address("ADDRESS_VALUE"); const getPromise = firestorm.files.get("/quote.txt"); getPromise - .then((fileContent) => { - console.log(fileContent); // 'but your kids are gonna love it. - }) - .catch((err) => { - console.error(err); - }); + .then((fileContent) => console.log(fileContent)) // 'but your kids are gonna love it. + .catch((err) => console.error(err)); ``` ## Delete a file @@ -273,12 +265,8 @@ firestorm.token("TOKEN_VALUE"); const deletePromise = firestorm.files.delete("/quote.txt"); deletePromise - .then(() => { - console.log("File successfully deleted"); - }) - .catch((err) => { - console.error(err); - }); + .then(() => console.log("File successfully deleted")) + .catch((err) => console.error(err)); ``` ## Memory warning @@ -290,7 +278,7 @@ Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 32360168 bytes) ``` -If you encounter a memory allocation issue, you have to allow more memory through this file `/etc/php/7.4/apache2/php.ini` with a bigger value here: +If you encounter a memory allocation issue, you have to allow more memory through `/etc/php/7.4/apache2/php.ini` with a bigger value: ``` memory_limit = 256M @@ -298,9 +286,9 @@ memory_limit = 256M ## API endpoints -All Firestorm methods correspond to an equivalent Axios request to the relevant PHP file. Read requests are GET requests and write requests are POST requests with provided JSON data. +All Firestorm methods correspond to an equivalent Axios request to the relevant PHP file. Read requests are `GET` requests and write requests are `POST` requests with provided JSON data. -You always have the same first keys and the one key per method: +The first keys in the request will always be the same: ```json { diff --git a/package.json b/package.json index 3d73e37..71c240e 100644 --- a/package.json +++ b/package.json @@ -42,15 +42,15 @@ "axios": "^1.6.7" }, "devDependencies": { - "chai": "^4.3.10", + "chai": "^4.4.1", "docdash": "^2.0.2", "form-data": "^4.0.0", "glob": "^10.3.10", "jsdoc": "^4.0.2", "jsdoc-to-markdown": "^8.0.1", - "mocha": "^10.2.0", + "mocha": "^10.3.0", "nyc": "^15.1.0", - "prettier": "^3.2.4", + "prettier": "^3.2.5", "recursive-copy": "^2.0.14", "typescript": "^5.3.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6787bc8..a421847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,8 +11,8 @@ dependencies: devDependencies: chai: - specifier: ^4.3.10 - version: 4.3.10 + specifier: ^4.4.1 + version: 4.4.1 docdash: specifier: ^2.0.2 version: 2.0.2 @@ -29,14 +29,14 @@ devDependencies: specifier: ^8.0.1 version: 8.0.1 mocha: - specifier: ^10.2.0 - version: 10.2.0 + specifier: ^10.3.0 + version: 10.3.0 nyc: specifier: ^15.1.0 version: 15.1.0 prettier: - specifier: ^3.2.4 - version: 3.2.4 + specifier: ^3.2.5 + version: 3.2.5 recursive-copy: specifier: ^2.0.14 version: 2.0.14 @@ -51,36 +51,36 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.22 dev: true - /@babel/code-frame@7.22.13: - resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.22.20 + '@babel/highlight': 7.23.4 chalk: 2.4.2 dev: true - /@babel/compat-data@7.23.2: - resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} engines: {node: '>=6.9.0'} dev: true - /@babel/core@7.23.2: - resolution: {integrity: sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==} + /@babel/core@7.23.9: + resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.22.13 - '@babel/generator': 7.23.0 - '@babel/helper-compilation-targets': 7.22.15 - '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.2) - '@babel/helpers': 7.23.2 - '@babel/parser': 7.23.0 - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) + '@babel/helpers': 7.23.9 + '@babel/parser': 7.23.9 + '@babel/template': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 convert-source-map: 2.0.0 debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -90,23 +90,23 @@ packages: - supports-color dev: true - /@babel/generator@7.23.0: - resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.9 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.22 jsesc: 2.5.2 dev: true - /@babel/helper-compilation-targets@7.22.15: - resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.23.2 - '@babel/helper-validator-option': 7.22.15 - browserslist: 4.22.1 + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.23.0 lru-cache: 5.1.1 semver: 6.3.1 dev: true @@ -120,31 +120,31 @@ packages: resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.22.15 - '@babel/types': 7.23.0 + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.9 dev: true /@babel/helper-module-imports@7.22.15: resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.9 dev: true - /@babel/helper-module-transforms@7.23.0(@babel/core@7.23.2): - resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.23.9 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-module-imports': 7.22.15 '@babel/helper-simple-access': 7.22.5 @@ -156,18 +156,18 @@ packages: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.9 dev: true /@babel/helper-split-export-declaration@7.22.6: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.9 dev: true - /@babel/helper-string-parser@7.22.5: - resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} engines: {node: '>=6.9.0'} dev: true @@ -176,24 +176,24 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-validator-option@7.22.15: - resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} dev: true - /@babel/helpers@7.23.2: - resolution: {integrity: sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==} + /@babel/helpers@7.23.9: + resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.2 - '@babel/types': 7.23.0 + '@babel/template': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 transitivePeerDependencies: - supports-color dev: true - /@babel/highlight@7.22.20: - resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-validator-identifier': 7.22.20 @@ -201,46 +201,46 @@ packages: js-tokens: 4.0.0 dev: true - /@babel/parser@7.23.0: - resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} + /@babel/parser@7.23.9: + resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.9 dev: true - /@babel/template@7.22.15: - resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + /@babel/template@7.23.9: + resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.22.13 - '@babel/parser': 7.23.0 - '@babel/types': 7.23.0 + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 dev: true - /@babel/traverse@7.23.2: - resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + /@babel/traverse@7.23.9: + resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.22.13 - '@babel/generator': 7.23.0 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.23.0 - '@babel/types': 7.23.0 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types@7.23.0: - resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + /@babel/types@7.23.9: + resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.22.5 + '@babel/helper-string-parser': 7.23.4 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 dev: true @@ -279,11 +279,11 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.22 dev: true - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} dev: true @@ -296,15 +296,15 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true - /@jridgewell/trace-mapping@0.3.20: - resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} dependencies: - '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@jsdoc/salty@0.2.5: - resolution: {integrity: sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==} + /@jsdoc/salty@0.2.7: + resolution: {integrity: sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==} engines: {node: '>=v12.0.0'} dependencies: lodash: 4.17.21 @@ -317,19 +317,19 @@ packages: dev: true optional: true - /@types/linkify-it@3.0.4: - resolution: {integrity: sha512-hPpIeeHb/2UuCw06kSNAOVWgehBLXEo0/fUs0mw3W2qhqX89PI2yvok83MnuctYGCPrabGIoi0fFso4DQ+sNUQ==} + /@types/linkify-it@3.0.5: + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} dev: true /@types/markdown-it@12.2.3: resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} dependencies: - '@types/linkify-it': 3.0.4 - '@types/mdurl': 1.0.4 + '@types/linkify-it': 3.0.5 + '@types/mdurl': 1.0.5 dev: true - /@types/mdurl@1.0.4: - resolution: {integrity: sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A==} + /@types/mdurl@1.0.5: + resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} dev: true /aggregate-error@3.1.0: @@ -524,15 +524,15 @@ packages: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} dev: true - /browserslist@4.22.1: - resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + /browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001559 - electron-to-chromium: 1.4.572 - node-releases: 2.0.13 - update-browserslist-db: 1.0.13(browserslist@4.22.1) + caniuse-lite: 1.0.30001588 + electron-to-chromium: 1.4.677 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.23.0) dev: true /cache-point@2.0.0: @@ -564,8 +564,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001559: - resolution: {integrity: sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==} + /caniuse-lite@1.0.30001588: + resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==} dev: true /catharsis@0.9.0: @@ -575,8 +575,8 @@ packages: lodash: 4.17.21 dev: true - /chai@4.3.10: - resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 @@ -821,15 +821,15 @@ packages: /docdash@2.0.2: resolution: {integrity: sha512-3SDDheh9ddrwjzf6dPFe1a16M6ftstqTNjik2+1fx46l24H9dD2osT2q9y+nBEC1wWz4GIqA48JmicOLQ0R8xA==} dependencies: - '@jsdoc/salty': 0.2.5 + '@jsdoc/salty': 0.2.7 dev: true /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true - /electron-to-chromium@1.4.572: - resolution: {integrity: sha512-RlFobl4D3ieetbnR+2EpxdzFl9h0RAJkPK3pfiwMug2nhBin2ZCsGIAJWdpNniLz43sgXam/CgipOmvTA+rUiA==} + /electron-to-chromium@1.4.677: + resolution: {integrity: sha512-erDa3CaDzwJOpyvfKhOiJjBVNnMM0qxHq47RheVVwsSQrgBA9ZSGV9kdaOfZDPXcHzhG7lBxhj6A7KvfLJBd6Q==} dev: true /emoji-regex@8.0.0: @@ -855,8 +855,8 @@ packages: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} dev: true - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} dev: true @@ -1026,8 +1026,8 @@ packages: path-scurry: 1.10.1 dev: true - /glob@7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -1037,15 +1037,15 @@ packages: path-is-absolute: 1.0.1 dev: true - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 5.0.1 once: 1.4.0 - path-is-absolute: 1.0.1 dev: true /globals@11.12.0: @@ -1175,8 +1175,8 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} dev: true @@ -1191,9 +1191,9 @@ packages: resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.23.9 '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -1205,7 +1205,7 @@ packages: dependencies: archy: 1.0.0 cross-spawn: 7.0.3 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 p-map: 3.0.0 rimraf: 3.0.2 uuid: 8.3.2 @@ -1215,7 +1215,7 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} dependencies: - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 dev: true @@ -1225,14 +1225,14 @@ packages: engines: {node: '>=10'} dependencies: debug: 4.3.4(supports-color@8.1.1) - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: - supports-color dev: true - /istanbul-reports@3.1.6: - resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} dependencies: html-escaper: 2.0.2 @@ -1318,8 +1318,8 @@ packages: engines: {node: '>=12.0.0'} hasBin: true dependencies: - '@babel/parser': 7.23.0 - '@jsdoc/salty': 0.2.5 + '@babel/parser': 7.23.9 + '@jsdoc/salty': 0.2.7 '@types/markdown-it': 12.2.3 bluebird: 3.7.2 catharsis: 0.9.0 @@ -1412,8 +1412,8 @@ packages: get-func-name: 2.0.2 dev: true - /lru-cache@10.0.1: - resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} dev: true @@ -1441,7 +1441,7 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.5.4 + semver: 7.6.0 dev: true /markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2): @@ -1541,8 +1541,8 @@ packages: hasBin: true dev: true - /mocha@10.2.0: - resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==} + /mocha@10.3.0: + resolution: {integrity: sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==} engines: {node: '>= 14.0.0'} hasBin: true dependencies: @@ -1553,13 +1553,12 @@ packages: diff: 5.0.0 escape-string-regexp: 4.0.0 find-up: 5.0.0 - glob: 7.2.0 + glob: 8.1.0 he: 1.2.0 js-yaml: 4.1.0 log-symbols: 4.1.0 minimatch: 5.0.1 ms: 2.1.3 - nanoid: 3.3.3 serialize-javascript: 6.0.0 strip-json-comments: 3.1.1 supports-color: 8.1.1 @@ -1577,12 +1576,6 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true - /nanoid@3.3.3: - resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true @@ -1594,8 +1587,8 @@ packages: process-on-spawn: 1.0.0 dev: true - /node-releases@2.0.13: - resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true /normalize-path@3.0.0: @@ -1618,13 +1611,13 @@ packages: foreground-child: 2.0.0 get-package-type: 0.1.0 glob: 7.2.3 - istanbul-lib-coverage: 3.2.0 + istanbul-lib-coverage: 3.2.2 istanbul-lib-hook: 3.0.0 istanbul-lib-instrument: 4.0.3 istanbul-lib-processinfo: 2.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.6 + istanbul-reports: 3.1.7 make-dir: 3.1.0 node-preload: 0.2.1 p-map: 3.0.0 @@ -1723,7 +1716,7 @@ packages: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 10.0.1 + lru-cache: 10.2.0 minipass: 7.0.4 dev: true @@ -1752,8 +1745,8 @@ packages: find-up: 4.1.0 dev: true - /prettier@3.2.4: - resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==} + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} engines: {node: '>=14'} hasBin: true dev: true @@ -1885,8 +1878,8 @@ packages: hasBin: true dev: true - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} engines: {node: '>=10'} hasBin: true dependencies: @@ -2145,14 +2138,14 @@ packages: resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} dev: true - /update-browserslist-db@1.0.13(browserslist@4.22.1): + /update-browserslist-db@1.0.13(browserslist@4.23.0): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.22.1 - escalade: 3.1.1 + browserslist: 4.23.0 + escalade: 3.1.2 picocolors: 1.0.0 dev: true @@ -2305,7 +2298,7 @@ packages: engines: {node: '>=10'} dependencies: cliui: 7.0.4 - escalade: 3.1.1 + escalade: 3.1.2 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 diff --git a/src/index.js b/src/index.js index e02c0cf..9c63f45 100644 --- a/src/index.js +++ b/src/index.js @@ -4,23 +4,29 @@ try { /** * @typedef {Object} SearchOption - * @property {string} field - The field you want to search in - * @property {"!=" | "==" | ">=" | "<=" | "<" | ">" | "in" | "includes" | "startsWith" | "endsWith" | "array-contains" | "array-contains-any" | "array-length-(eq|df|gt|lt|ge|le)" } criteria - Filter criteria - * @property {string | number | boolean | Array } value - The value you want to compare - * @property {boolean} ignoreCase - Ignore case on search string + * @property {string} field - The field to be searched for + * @property {"!=" | "==" | ">=" | "<=" | "<" | ">" | "in" | "includes" | "startsWith" | "endsWith" | "array-contains" | "array-contains-any" | "array-length-(eq|df|gt|lt|ge|le)"} criteria - Search criteria to filter results + * @property {string | number | boolean | Array} value - The value to be searched for + * @property {boolean} [ignoreCase] - Is it case sensitive? (default true) */ /** * @typedef {Object} EditObject - * @property {string | number } id - The affected element - * @property {string} field - The field you want to edit - * @property {"set" | "remove" | "append" | "increment" | "decrement" | "array-push" | "array-delete" | "array-splice"} operation - Wanted operation on field - * @property {string | number | boolean | Array} [value] - The value you want to compare + * @property {string | number} id - The affected element + * @property {string} field - The field to edit + * @property {"set" | "remove" | "append" | "increment" | "decrement" | "array-push" | "array-delete" | "array-splice"} operation - Operation for the field + * @property {string | number | boolean | Array} [value] - The value to write + */ + +/** + * @typedef {Object} ValueObject + * @property {string} field - Field to search + * @property {boolean} [flatten] - Flatten array fields? (default false) */ /** * @typedef {Object} SelectOption - * @property {Array} fields - Chosen fields to eventually return + * @property {Array} fields - Selected fields to be returned */ /** @@ -38,23 +44,21 @@ const ID_FIELD_NAME = "id"; const readAddress = () => { if (!_address) throw new Error("Firestorm address was not configured"); - return _address + "get.php"; }; + const writeAddress = () => { if (!_address) throw new Error("Firestorm address was not configured"); - return _address + "post.php"; }; + const fileAddress = () => { if (!_address) throw new Error("Firestorm address was not configured"); - return _address + "files.php"; }; const writeToken = () => { if (!_token) throw new Error("Firestorm token was not configured"); - return _token; }; @@ -70,15 +74,10 @@ const writeToken = () => { * @param {Promise} request The Axios concerned request */ const __extract_data = (request) => { - return new Promise((resolve, reject) => { - request - .then((res) => { - if ("data" in res) return resolve(res.data); - resolve(res); - }) - .catch((err) => { - reject(err); - }); + if (!(request instanceof Promise)) request = Promise.resolve(request); + return request.then((res) => { + if ("data" in res) return res.data; + return res; }); }; @@ -101,27 +100,21 @@ class Collection { } /** - * Add user methods to the returned data + * Add user methods to returned data * @private * @ignore * @param {AxiosPromise} req - Incoming request * @returns {Object | Object[]} */ __add_methods(req) { - return new Promise((resolve, reject) => { - req - .then((el) => { - if (Array.isArray(el)) { - return resolve(el.map((e) => this.addMethods(e))); - } - - el[Object.keys(el)[0]][ID_FIELD_NAME] = Object.keys(el)[0]; - el = el[Object.keys(el)[0]]; - - // else on the object itself - return resolve(this.addMethods(el)); - }) - .catch((err) => reject(err)); + if (!(req instanceof Promise)) req = Promise.resolve(req); + return req.then((el) => { + if (Array.isArray(el)) return el.map((el) => this.addMethods(el)); + el[Object.keys(el)[0]][ID_FIELD_NAME] = Object.keys(el)[0]; + el = el[Object.keys(el)[0]]; + + // else on the object itself + return this.addMethods(el); }); } @@ -158,13 +151,11 @@ class Collection { * @returns {Promise} Corresponding value */ get(id) { - return this.__add_methods( - this.__get_request({ - collection: this.collectionName, - command: "get", - id: id, - }), - ); + return this.__get_request({ + collection: this.collectionName, + command: "get", + id: id, + }).then((res) => this.__add_methods(res)); } /** @@ -208,7 +199,7 @@ class Collection { //TODO: add more strict value field warnings in JS and PHP }); - let params = { + const params = { collection: this.collectionName, command: "search", search: searchOptions, @@ -218,36 +209,22 @@ class Collection { if (random === true) { params.random = {}; } else { - let seed = parseInt(random); + const seed = parseInt(random); if (isNaN(seed)) return Promise.reject( new Error("random takes as parameter true, false or an integer value"), ); - params.random = { - seed: seed, - }; + params.random = { seed }; } } - return new Promise((resolve, reject) => { - let raw; - this.__get_request(params) - .then((res) => { - const arr = []; - - raw = res; - Object.keys(res).forEach((contribID) => { - const tmp = res[contribID]; - tmp[ID_FIELD_NAME] = contribID; - arr.push(tmp); - }); - - resolve(this.__add_methods(Promise.resolve(arr))); - }) - .catch((err) => { - err.raw = raw; - reject(err); - }); + return this.__get_request(params).then((res) => { + const arr = Object.entries(res).map(([id, value]) => { + value[ID_FIELD_NAME] = id; + return value; + }); + + return this.__add_methods(arr); }); } @@ -259,23 +236,17 @@ class Collection { searchKeys(keys) { if (!Array.isArray(keys)) return Promise.reject("Incorrect keys"); - return new Promise((resolve, reject) => { - this.__get_request({ - collection: this.collectionName, - command: "searchKeys", - search: keys, - }) - .then((res) => { - const arr = []; - Object.keys(res).forEach((contribID) => { - const tmp = res[contribID]; - tmp[ID_FIELD_NAME] = contribID; - arr.push(tmp); - }); - - resolve(this.__add_methods(Promise.resolve(arr))); - }) - .catch((err) => reject(err)); + return this.__get_request({ + collection: this.collectionName, + command: "searchKeys", + search: keys, + }).then((res) => { + const arr = Object.entries(res).map(([id, value]) => { + value[ID_FIELD_NAME] = id; + return value; + }); + + return this.__add_methods(arr); }); } @@ -284,26 +255,23 @@ class Collection { * @returns {Promise>} The entire collection */ readRaw() { - return new Promise((resolve, reject) => { - this.__get_request({ - collection: this.collectionName, - command: "read_raw", - }) - .then((data) => { - Object.keys(data).forEach((key) => { - data[key][ID_FIELD_NAME] = key; - this.addMethods(data[key]); - }); - - resolve(data); - }) - .catch(reject); + return this.__get_request({ + collection: this.collectionName, + command: "read_raw", + }).then((data) => { + // preserve as object + Object.keys(data).forEach((key) => { + data[key][ID_FIELD_NAME] = key; + this.addMethods(data[key]); + }); + + return data; }); } /** * Returns the whole content of the JSON - * @deprecated Use readRaw instead + * @deprecated Use {@link readRaw} instead * @returns {Promise>} The entire collection */ read_raw() { @@ -318,24 +286,41 @@ class Collection { */ select(selectOption) { if (!selectOption) selectOption = {}; - return new Promise((resolve, reject) => { - this.__get_request({ - collection: this.collectionName, - command: "select", - select: selectOption, - }) - .then((data) => { - Object.keys(data).forEach((key) => { - data[key][ID_FIELD_NAME] = key; - this.addMethods(data[key]); - }); - - resolve(data); - }) - .catch(reject); + return this.__get_request({ + collection: this.collectionName, + command: "select", + select: selectOption, + }).then((data) => { + Object.keys(data).forEach((key) => { + data[key][ID_FIELD_NAME] = key; + this.addMethods(data[key]); + }); + + return data; }); } + /** + * Get all distinct non-null values for a given key across a collection + * @param {ValueObject} valueOption - Value options + * @returns {Promise} Array of unique values + */ + values(valueOption) { + if (!valueOption) return Promise.reject("Value option must be provided"); + if (typeof valueOption.field !== "string") return Promise.reject("Field must be a string"); + if (valueOption.flatten !== undefined && typeof valueOption.flatten !== "boolean") + return Promise.reject("Flatten must be a boolean"); + + return this.__get_request({ + collection: this.collectionName, + command: "values", + values: valueOption, + }).then((data) => + // no ID_FIELD or method injection since no ids are returned + Object.values(data).filter((d) => d !== null), + ); + } + /** * Returns random max entries offsets with a given seed * @param {number} max - The maximum number of entries @@ -378,7 +363,7 @@ class Collection { this.addMethods(data[key]); }); - return Promise.resolve(data); + return data; }); } @@ -436,8 +421,8 @@ class Collection { /** * Set the entire JSON file contents + * @deprecated Use {@link writeRaw} instead * @param {Record} value - The value to write - * @deprecated Use writeRaw instead * @returns {Promise} Write confirmation */ write_raw(value) { @@ -450,21 +435,14 @@ class Collection { * @returns {Promise} The generated ID of the added element */ add(value) { - return new Promise((resolve, reject) => { - axios - .post(writeAddress(), this.__write_data("add", value)) - .then((res) => { - return this.__extract_data(Promise.resolve(res)); - }) - .then((res) => { - if (typeof res != "object" || !("id" in res) || typeof res.id != "string") - throw new Error("Incorrect result"); - resolve(res.id); - }) - .catch((err) => { - reject(err); - }); - }); + return axios + .post(writeAddress(), this.__write_data("add", value)) + .then((res) => this.__extract_data(res)) + .then((res) => { + if (typeof res != "object" || !("id" in res) || typeof res.id != "string") + return Promise.reject(new Error("Incorrect result")); + return res.id; + }); } /** @@ -473,13 +451,9 @@ class Collection { * @returns {Promise} The generated IDs of the added elements */ addBulk(values) { - return new Promise((resolve, reject) => { - this.__extract_data(axios.post(writeAddress(), this.__write_data("addBulk", values, true))) - .then((res) => { - resolve(res.ids); - }) - .catch(reject); - }); + return this.__extract_data( + axios.post(writeAddress(), this.__write_data("addBulk", values, true)), + ).then((res) => res.ids); } /** @@ -527,7 +501,7 @@ class Collection { /** * Edit one field of the collection * @param {EditObject} obj - The edit object - * @returns {Promise} The edited element + * @returns {Promise<{ success: boolean }>} Edit confirmation */ editField(obj) { const data = this.__write_data("editField", obj, null); @@ -537,7 +511,7 @@ class Collection { /** * Changes one field from an element in this collection * @param {EditObject[]} objArray The edit object array with operations - * @returns {Promise} The edited elements + * @returns {Promise<{ success: boolean[] }>} Edit confirmation */ editFieldBulk(objArray) { const data = this.__write_data("editFieldBulk", objArray, undefined); @@ -556,6 +530,7 @@ const firestorm = { */ address(newValue = undefined) { if (newValue === undefined) return readAddress(); + if (!newValue.endsWith("/")) newValue += "/"; if (newValue) _address = newValue; return _address; @@ -598,16 +573,17 @@ const firestorm = { ID_FIELD: ID_FIELD_NAME, /** - * Test child object with child namespace + * Firestorm file handler * @memberof firestorm * @type {Object} * @namespace firestorm.files */ files: { /** - * Get file back + * Get a file by its path * @memberof firestorm.files * @param {string} path - The file path wanted + * @returns {Promise} File contents */ get(path) { return __extract_data( @@ -620,33 +596,37 @@ const firestorm = { }, /** - * Uploads file + * Upload a file * @memberof firestorm.files * @param {FormData} form - The form data with path, filename, and file - * @returns {Promise} HTTP response + * @returns {Promise} Write confirmation */ upload(form) { form.append("token", firestorm.token()); - return axios.post(fileAddress(), form, { - headers: { - ...form.getHeaders(), - }, - }); + return __extract_data( + axios.post(fileAddress(), form, { + headers: { + ...form.getHeaders(), + }, + }), + ); }, /** - * Deletes a file given its path + * Deletes a file by path * @memberof firestorm.files * @param {string} path - The file path to delete - * @returns {Promise} HTTP response + * @returns {Promise} Write confirmation */ delete(path) { - return axios.delete(fileAddress(), { - data: { - path, - token: firestorm.token(), - }, - }); + return __extract_data( + axios.delete(fileAddress(), { + data: { + path, + token: firestorm.token(), + }, + }), + ); }, }, }; diff --git a/src/php/classes/HTTPException.php b/src/php/classes/HTTPException.php index 97c6600..fe58556 100644 --- a/src/php/classes/HTTPException.php +++ b/src/php/classes/HTTPException.php @@ -2,7 +2,7 @@ class HTTPException extends Exception { - // Redéfinissez l'exception ainsi le message n'est pas facultatif + // custom message public function __construct($message, $code = 400, Throwable $previous = null) { $type_message = gettype($message); @@ -13,13 +13,11 @@ public function __construct($message, $code = 400, Throwable $previous = null) { if($type_code != 'integer') throw new Exception("Incorrect code type for HTTPException constructor, expected string, got " . $type_code); - // traitement personnalisé que vous voulez réaliser ... - - // assurez-vous que tout a été assigné proprement + // assign everything parent::__construct($message, $code, $previous); } - // chaîne personnalisée représentant l'objet + // prettier representation public function __toString() { return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; } diff --git a/src/php/classes/JSONDatabase.php b/src/php/classes/JSONDatabase.php index c9143c0..22482e2 100644 --- a/src/php/classes/JSONDatabase.php +++ b/src/php/classes/JSONDatabase.php @@ -10,20 +10,20 @@ class JSONDatabase { public $folderPath = './files/'; public $fileName = 'db'; public $fileExt = '.json'; - + public $default = array(); - + public $autoKey = true; public $autoIncrement = true; - + public function fullPath() { return $this->folderPath . $this->fileName . $this->fileExt; } - + public function write_raw($content) { $content_type = gettype($content); $incorrect_types = array('integer', 'double', 'string', 'boolean'); - + // content must not be primitive if(in_array($content_type, $incorrect_types)) { throw new HTTPException('write_raw value must not be a ' . $content_type, 400); @@ -43,7 +43,7 @@ public function write_raw($content) { } // now we know we have an associative array - + // content must be objects foreach ($content as $key => $item) { // item must not be primitive @@ -54,20 +54,20 @@ public function write_raw($content) { throw new HTTPException('write_raw item with key' . $key . ' item must not be a ' . $item_type, 400); return 400; } - + // we accept assosiative array as items beacuse they may have an integer key } - + $content = stringifier($content); // fix empty raw content //be cause php parses {} as array(0) if($content === '[]') $content = '{}'; - + return file_put_contents($this->fullPath(), $content, LOCK_EX); } - + private function write($obj) { $obj['content'] = stringifier($obj['content'], 1); return FileAccess::write($obj); @@ -77,27 +77,27 @@ public function sha1() { $obj = $this->read_raw(); return sha1($obj['content']); } - + public function read_raw($waitLock = false) { return FileAccess::read($this->fullPath(), $waitLock, json_encode($this->default)); } - + public function read($waitLock = false) { $res = $this->read_raw($waitLock); $res['content'] = json_decode($res['content'], true); - + return $res; } - + public function get($key) { $obj = $this->read(); if(!$obj || array_key_exists('content', $obj) == false || array_key_exists(strval($key), $obj['content']) == false) return null; - + $res = array($key => $obj['content'][$key]); return $res; } - + public function set($key, $value) { if($key === null or $value === null) { /// === fixes the empty array == comparaison throw new HTTPException("Key or value are null", 400); @@ -108,46 +108,46 @@ public function set($key, $value) { throw new HTTPException('Incorrect key', 400); $value_var_type = gettype($value); - if($value_var_type == 'double' or $value_var_type == 'integer' or $value_var_type == 'string') + if($value_var_type == 'double' or $value_var_type == 'integer' or $value_var_type == 'string') throw new HTTPException('Invalid value type, got ' . $value_var_type . ', expected object', 400); - + if($value !== array() and !array_assoc($value)) throw new HTTPException('Value cannot be a sequential array', 400); - + $key = strval($key); - + // else set it at the corresponding value $obj = $this->read(true); $obj['content'][$key] = json_decode(json_encode($value), true); return $this->write($obj); } - + public function setBulk($keys, $values) { // we verify that our keys are in an array $key_var_type = gettype($keys); if($key_var_type != 'array') throw new Exception('Incorect keys type'); - + // else set it at the corresponding value $obj = $this->read(true); - + // decode and add all values $value_decoded = json_decode(json_encode($values), true); $keys_decoded = json_decode(json_encode($keys), true); for($i = 0; $i < count($value_decoded); $i++) { - + $key_var_type = gettype($keys_decoded[$i]); if($key_var_type != 'string' and $key_var_type != 'double' and $key_var_type != 'integer') throw new Exception('Incorrect key'); - + $key = strval($keys_decoded[$i]); - + $obj['content'][$key] = $value_decoded[$i]; } - + $this->write($obj); } - + private function newLastKey($arr) { if($this->autoIncrement) { $int_keys = array_filter(array_keys($arr), "is_int"); @@ -159,10 +159,10 @@ private function newLastKey($arr) { $last_key = uniqid(); } } - + return strval($last_key); } - + public function add($value) { // restricts types to objects only $value_type = gettype($value); @@ -171,18 +171,18 @@ public function add($value) { if($this->autoKey == false) throw new Exception('Autokey disabled'); - + // else set it at the corresponding value $obj = $this->read(true); - + $id = $this->newLastKey($obj['content']); $obj['content'][$id] = $value; - + $this->write($obj); - + return $id; } - + public function addBulk($values) { if($values !== array() and $values == NULL) throw new HTTPException('null-like value not accepted', 400); @@ -202,30 +202,30 @@ public function addBulk($values) { if($this->autoKey == false) throw new Exception('Autokey disabled'); - + // veriify that values is an array with number indices if(array_assoc($values)) throw new Exception('Wanted sequential array'); - + // else set it at the correspongding value $obj = $this->read(true); - + // decode and add all values $values_decoded = $values; $id_array = array(); foreach($values_decoded as $value_decoded) { $id = $this->newLastKey($obj['content']); - + $obj['content'][$id] = $value_decoded; - + array_push($id_array, $id); } - + $this->write($obj); - + return $id_array; } - + public function remove($key) { if(gettype($key) != 'string') throw new HTTPException("remove value must be a string", 400); @@ -234,11 +234,11 @@ public function remove($key) { unset($obj['content'][$key]); $this->write($obj); } - + public function removeBulk($keys) { if($keys !== array() and $keys == NULL) throw new HTTPException('null-like keys not accepted', 400); - + if(gettype($keys) !== 'array' or array_assoc($keys)) throw new HTTPException('keys must be an array', 400); @@ -251,59 +251,59 @@ public function removeBulk($keys) { } $obj = $this->read(true); - + // remove all keys foreach($keys as $key_decoded) { unset($obj['content'][$key_decoded]); } - + $this->write($obj); } - + public function search($conditions, $random = false) { $obj = $this->read(); - + $res = []; - + foreach(array_keys($obj['content']) as $key) { $el = $obj['content'][$key]; $el_root = $el; - + $add = true; - + $condition_index = 0; while($condition_index < count($conditions) and $add) { // get condition $condition = $conditions[$condition_index]; - + // get condition fields extracted $field = $condition['field']; $field_path = explode(".", $field); error_reporting(error_reporting() - E_NOTICE); $field_ind = 0; - + while($el != NULL && $field_ind + 1 < count($field_path)) { $el = $el[$field_path[$field_ind]]; $field_ind = $field_ind + 1; $field = $field_path[$field_ind]; } error_reporting(error_reporting() + E_NOTICE); - + if($el != NULL && array_key_exists($field, $el) && array_key_exists('criteria', $condition) && array_key_exists('value', $condition)) { $criteria = $condition['criteria']; $value = $condition['value']; - + // get field to compare $concernedField = $el[$field]; - + // get concerned field type $fieldType = gettype($concernedField); if($criteria == 'array-contains' || $criteria == 'array-contains-any') { $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; } - + switch($fieldType) { case 'boolean': switch($criteria) { @@ -430,11 +430,11 @@ public function search($conditions, $random = false) { } else { $add = false; } - + $condition_index++; $el = $el_root; } - + if($add) { $res[$key] = $el_root; } @@ -449,68 +449,68 @@ public function search($conditions, $random = false) { } $res = chooseRandom($res, $seed); } - + return $res; } - + public function searchKeys($searchedKeys) { $obj = $this->read(); - + $res = array(); if(gettype($searchedKeys) != 'array') return $res; - + foreach($searchedKeys as $key) { $key = strval($key); - + if(array_key_exists($key, $obj['content'])) { $res[$key] = $el = $obj['content'][$key]; } } - + return $res; } - + public function editField($editObj) { return $this->editFieldBulk(array($editObj))[0]; } - + // MANDATORY REFERENCE to edit directly: PHP 5+ private function __edit(&$obj, $editObj) { - + if(!is_object($editObj)) return false; // id required if(!check($editObj['id'])) return false; - + $id = $editObj['id']; - + // id string or integer if(gettype($id) != 'string' and gettype($id) != 'integer') return false; - + // object not found if(!array_key_exists($id, $obj['content']) || !check($obj['content'][$id])) return false; - + // field required if(!check($editObj['field'])) return false; - + $field = $editObj['field']; - + // field is a string if(gettype($field) != 'string') return false; - + // operation required if(!check($editObj['operation'])) return false; - + $operation = $editObj['operation']; - + $value = null; // return if operation has no value // set, append, array-push, array-delete, array-slice @@ -518,12 +518,12 @@ private function __edit(&$obj, $editObj) { return false; else $value = $editObj['value']; - + // field not found for other than set or push operation // for the remove operation it is still a success because at the end the field doesn't exist if(!isset($obj['content'][$id][$field]) and ($operation != 'set' and $operation != 'remove' and $operation != 'array-push')) return false; - + switch($operation) { case 'set': $obj['content'][$id][$field] = $value; @@ -535,14 +535,14 @@ private function __edit(&$obj, $editObj) { // check type string if(gettype($obj['content'][$id][$field]) != 'string' or gettype($value) != 'string') return false; - + $obj['content'][$id][$field] .= $value; return true; case 'invert': // check type boolean if(gettype($obj['content'][$id][$field]) != 'boolean') return false; - + $obj['content'][$id][$field] = !$obj['content'][$id][$field]; return true; case 'increment': @@ -550,9 +550,9 @@ private function __edit(&$obj, $editObj) { // check type number if(gettype($obj['content'][$id][$field]) != 'integer' and gettype($obj['content'][$id][$field]) != 'double') return false; - + $change = $operation == 'increment' ? +1 : -1; - + // check if value if(isset($editObj['value'])) { if(gettype($editObj['value']) == 'integer' or gettype($editObj['value']) == 'double') { // error here @@ -562,75 +562,75 @@ private function __edit(&$obj, $editObj) { return false; } } - + $obj['content'][$id][$field] += $change; return true; case 'array-push': // create it if not here if(!isset($obj['content'][$id][$field])) $obj['content'][$id][$field] = array(); - + // check if our field array if(gettype($obj['content'][$id][$field]) != 'array') return false; - + // our field must be a sequential array if(array_assoc($obj['content'][$id][$field])) return false; - + array_push($obj['content'][$id][$field], $value); - + return true; - + case 'array-delete': // check if our field array if(gettype($obj['content'][$id][$field]) != 'array') return false; - + // our field must be a sequential array if(array_assoc($obj['content'][$id][$field])) return false; - + // value must be integer if(gettype($value) != 'integer') return false; - + array_splice($obj['content'][$id][$field], $value, 1); - + return true; case 'array-splice': if(array_assoc($obj['content'][$id][$field])) return false; - + // value must be an array or to integers if(array_assoc($value) or count($value) != 2 or gettype($value[0]) != 'integer' or gettype($value[1]) != 'integer') return false; - + array_splice($obj['content'][$id][$field], $value[0], $value[1]); - + return true; default: break; } - - return false; + + return false; } - + public function editFieldBulk($objArray) { // need sequential array if(array_assoc($objArray)) return false; - + $arrayResult = array(); - + $fileObj = $this->read($this); - + foreach($objArray as &$editObj) { array_push($arrayResult, $this->__edit($fileObj, $editObj)); } - + $this->write($fileObj); - + return $arrayResult; } @@ -638,7 +638,7 @@ public function select($selectObj) { // check fields presence // fields is required, a array of strings $verif_fields = array_key_exists('fields', $selectObj); - if($verif_fields === false) throw new HTTPException('Missing require fields field'); + if($verif_fields === false) throw new HTTPException('Missing required fields field'); $verif_fields = gettype($selectObj['fields']) === 'array' && array_sequential($selectObj['fields']); if($verif_fields === false) throw new HTTPException('Incorrect fields type, expected an array'); @@ -646,7 +646,7 @@ public function select($selectObj) { $fields = $selectObj['fields']; $i = 0; $fields_count = count($fields); while($i < $fields_count && $verif_fields) { - $verif_fields = gettype($fields[$i]) === 'string'; + $verif_fields = gettype($fields[$i]) === 'string'; ++$i; } if(!$verif_fields) throw new HTTPException('fields field incorrect, expected an array of string'); @@ -665,6 +665,43 @@ public function select($selectObj) { return $result; } + public function values($valueObj) { + if (!array_key_exists('field', $valueObj)) + throw new HTTPException('Missing required field field'); + + if(!is_string($valueObj['field'])) + throw new HTTPException('Incorrect field type, expected a string'); + + if (array_key_exists('flatten', $valueObj)) { + if (!is_bool($valueObj['flatten'])) + throw new HTTPException('Incorrect flatten type, expected a boolean'); + $flatten = $valueObj['flatten']; + } else { + $flatten = false; + } + + $field = $valueObj['field']; + + $obj = $this->read(); + + $json = $obj['content']; + $result = []; + foreach ($json as $key => $value) { + // get correct field and skip existing primitive values (faster) + if (!array_key_exists($field, $value) || in_array($value, $result)) continue; + + // flatten array results if array field + if ($flatten === true && is_array($value[$field])) + $result = array_merge($result, $value[$field]); + else array_push($result, $value[$field]); + } + + // remove complex duplicates + $result = array_intersect_key($result, array_unique(array_map('serialize', $result))); + + return $result; + } + public function random($params) { return random($params, $this); } diff --git a/src/php/get.php b/src/php/get.php index 90429ec..022b9ff 100644 --- a/src/php/get.php +++ b/src/php/get.php @@ -50,7 +50,7 @@ if(!$command) http_error(400, 'No command provided'); -$commands_available = ['read_raw', 'get', 'search', 'searchKeys', 'select', 'random', 'sha1']; +$commands_available = ['read_raw', 'get', 'search', 'searchKeys', 'select', 'random', 'sha1', 'values']; // var_dump($command); // exit(); @@ -109,6 +109,13 @@ $result = $db->select($select); http_response(stringifier($result)); + case 'values': + $values = check_key_json('values', $inputJSON, false); + + if($values === false) http_error('400', 'No key provided'); + + $result = $db->values($values); + http_response(stringifier($result)); case 'random': $params = check_key_json('random', $inputJSON, false); if($params === false) http_error('400', 'No random object provided'); diff --git a/src/php/utils.php b/src/php/utils.php index 1a32370..3c84092 100644 --- a/src/php/utils.php +++ b/src/php/utils.php @@ -20,9 +20,9 @@ function sec($var) { function http_response($body, $code=200) { header('Content-Type: application/json'); http_response_code($code); - + echo $body; - + exit(); } @@ -47,32 +47,30 @@ function check_key_json($key, $arr, $parse = false) { return array_key_exists($key, $arr) ? ($parse ? sec($arr[$key]) : $arr[$key]) : false; } -function array_assoc(array $arr) -{ +function array_assoc(array $arr) { if (array() === $arr) return false; return array_keys($arr) !== range(0, count($arr) - 1); } -function array_sequential(array $arr) -{ +function array_sequential(array $arr) { return !array_assoc($arr); } -function stringifier($obj, $depth = 1) { +function stringifier($obj, $depth = 1) { if($depth == 0) { return json_encode($obj); } - + $res = "{"; - + $formed = array(); foreach(array_keys($obj) as $key) { array_push($formed, '"' . strval($key) . '":' . stringifier($obj[$key], $depth - 1)); } $res .= implode(",", $formed); - + $res .= "}"; - + return $res; } @@ -88,7 +86,7 @@ function cors() { if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) - header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); + header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}"); diff --git a/tests/house.json b/tests/house.json index bc89247..020d8fe 100644 --- a/tests/house.json +++ b/tests/house.json @@ -1,5 +1,5 @@ { - "livingroom": { + "living_room": { "name": "Living Room", "outdoor": false, "furniture": ["table", "chairs", "sofa", "vase", "curtains"] @@ -7,7 +7,7 @@ "kitchen": { "name": "Kitchen", "outdoor": false, - "furniture": ["table", "chairs", "frisge", "hoven", "freezer", "microwaves", "dishwasher"] + "furniture": ["table", "chairs", "fridge", "oven", "freezer", "microwaves", "dishwasher"] }, "bedroom": { "name": "Bedroom", diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 963ef97..8bc9f58 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -2,7 +2,7 @@ const chai = require("chai"); const FormData = require("form-data"); const { expect } = chai; -const firestorm = require("../src/index"); +const firestorm = require(".."); const crypto = require("crypto"); const path = require("path"); @@ -20,11 +20,11 @@ const DATABASE_FILE = path.join(__dirname, "base.json"); console.log("Testing at address " + ADDRESS + " with token " + TOKEN); -describe("Wrapper informations", () => { +describe("Wrapper information", () => { it("throws if no address yet", () => { expect(firestorm.address).to.throw(Error, "Firestorm address was not configured"); }); - it("binds good address", function () { + it("binds usable address", function () { firestorm.address(ADDRESS); const actual = firestorm.address(); @@ -38,7 +38,7 @@ describe("Wrapper informations", () => { done(); } }); - it("binds good token", function () { + it("binds usable token", function () { firestorm.token(TOKEN); const actual = firestorm.token(); @@ -47,6 +47,7 @@ describe("Wrapper informations", () => { }); let houseCollection; // = undefined +/** @type {firestorm.Collection} */ let base; // = undefined let content; let tmp; @@ -79,13 +80,7 @@ describe("File upload, download and delete", () => { }); }); it("finds an uploaded file and get it with same content", (done) => { - const timeoutPromise = function (timeout) { - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, timeout); - }); - }; + const timeoutPromise = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout)); let uploaded; const formData = new FormData(); formData.append("path", "/lyrics.txt"); @@ -98,13 +93,14 @@ describe("File upload, download and delete", () => { }) .then((res) => { expect(res).not.to.be.undefined; - expect(res.status).to.equals(200, "Upload failed"); + expect(res).to.deep.equal( + { message: "Written file successfully to /lyrics.txt" }, + "Message returned should match", + ); return timeoutPromise(200); }) - .then(() => { - return firestorm.files.get("/lyrics.txt"); - }) + .then(() => firestorm.files.get("/lyrics.txt")) .then((fileResult) => { const downloaded = Buffer.from(fileResult); expect(downloaded).to.deep.equal(uploaded); @@ -201,7 +197,7 @@ describe("GET operations", () => { firestorm .collection("unknown") .readRaw() - .then(() => done(new Error("Request should not full-fill"))) + .then(() => done(new Error("Request should not fulfill"))) .catch((err) => { if ("response" in err && err.response.status == 404) { done(); @@ -601,8 +597,37 @@ describe("GET operations", () => { }); }); + describe("values(valueOptions)", () => { + it("requires a field", (done) => { + base + .values() + .then((res) => { + done("Did not expect it to succeed"); + }) + .catch(() => done()); + }); + + it("doesn't require a flatten field", (done) => { + base + .values({ field: "name" }) + .then((res) => { + done(); + }) + .catch(() => done("Did not expect it to fail")); + }); + + it("needs a boolean flatten field if provided", (done) => { + base + .values({ field: "name", flatten: "this is not a boolean" }) + .then((res) => { + done("Did not expect it to succeed"); + }) + .catch(() => done()); + }); + }); + describe("random(max, seed, offset)", () => { - it("requires no parameters", (done) => { + it("doesn't require parameters", (done) => { base .random() .then((res) => { @@ -695,7 +720,7 @@ describe("PUT operations", () => { }); }); - describe("You must give him a correct value", () => { + describe("You must give a correct value", () => { const incorrect_bodies = [ undefined, null, @@ -713,7 +738,7 @@ describe("PUT operations", () => { base .writeRaw(body) .then((res) => { - done(new Error(`Should not full-fill returning ${JSON.stringify(res)}`)); + done(new Error(`Should not fulfill returning ${JSON.stringify(res)}`)); }) .catch((err) => { if (index < 2) { @@ -763,7 +788,7 @@ describe("PUT operations", () => { furniture: ["table", "chair", "flowerpot"], }) .then(() => { - done(new Error("This request should not full-fill")); + done(new Error("This request should not fulfill")); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -808,7 +833,7 @@ describe("PUT operations", () => { base .add(unco) .then((res) => { - done(new Error(`Should not full-fill with res ${res}`)); + done(new Error(`Should not fulfill with res ${res}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -833,7 +858,7 @@ describe("PUT operations", () => { ]; correct_values.forEach((co, index) => { - it(`${index === 0 ? "Empty object" : "Complex object"} should full-fill`, (done) => { + it(`${index === 0 ? "Empty object" : "Complex object"} should fulfill`, (done) => { base .add(co) .then(() => { @@ -848,7 +873,7 @@ describe("PUT operations", () => { }); describe("addBulk operations", () => { - it("must full-fill with empty array", (done) => { + it("must fulfill with empty array", (done) => { base .addBulk([]) .then((res) => { @@ -871,7 +896,7 @@ describe("PUT operations", () => { base .addBulk(unco) .then((res) => { - done(new Error(`Should not full-fill with res ${res}`)); + done(new Error(`Should not fulfill with res ${res}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -892,7 +917,7 @@ describe("PUT operations", () => { base .addBulk([unco]) .then((res) => { - done(new Error(`Should not full-fill with res ${res}`)); + done(new Error(`Should not fulfill with res ${res}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -979,7 +1004,7 @@ describe("PUT operations", () => { base .remove(unco) .then((res) => { - done(new Error(`Should not full-fill with value ${JSON.stringify(res)}`)); + done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -1031,7 +1056,7 @@ describe("PUT operations", () => { base .removeBulk([unco]) .then((res) => { - done(new Error(`Should not full-fill with value ${JSON.stringify(res)}`)); + done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) done(); @@ -1084,15 +1109,15 @@ describe("PUT operations", () => { it("Should not succeed with no parameters in the methods", (done) => { base .set() - .then(() => done(new Error("Should not full-fill"))) + .then(() => done(new Error("Should not fulfill"))) .catch(() => done()); }); - describe("0 values full-fill", () => { + describe("0 values fulfill", () => { const correct_values = ["0", 0, 0.0]; correct_values.forEach((unco) => { - it(`${JSON.stringify(unco)} value full-fills`, (done) => { + it(`${JSON.stringify(unco)} value fulfills`, (done) => { base .set(unco, tmp) .then((res) => { @@ -1114,7 +1139,7 @@ describe("PUT operations", () => { base .set(unco, tmp) .then((res) => { - done(new Error(`Should not full-fill with value ${JSON.stringify(res)}`)); + done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -1134,7 +1159,7 @@ describe("PUT operations", () => { base .set("1", unco) .then((res) => { - done(new Error(`Should not full-fill with value ${JSON.stringify(res)}`)); + done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -1181,7 +1206,7 @@ describe("PUT operations", () => { done(new Error(`Should return 400 not ${JSON.stringify(err)}`)); }); }); - it("full-fills with two empty arrays", (done) => { + it("fulfills with two empty arrays", (done) => { base .readRaw() .then((before) => { @@ -1224,7 +1249,7 @@ describe("PUT operations", () => { base .setBulk([unco], [tmp]) .then((res) => { - done(new Error(`Should not full-fill with value ${JSON.stringify(res)}`)); + done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -1268,7 +1293,7 @@ describe("PUT operations", () => { base .editField(unco) .then((res) => { - done(new Error("Should not full-fill with " + JSON.stringify(res))); + done(new Error("Should not fulfill with " + JSON.stringify(res))); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -1298,7 +1323,7 @@ describe("PUT operations", () => { base .editField(obj) .then((res) => { - done(new Error("Should not full-fill with " + JSON.stringify(res))); + done(new Error("Should not fulfill with " + JSON.stringify(res))); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -1321,7 +1346,7 @@ describe("PUT operations", () => { field: "name", }) .then((res) => { - done(new Error("Should not full-fill with " + JSON.stringify(res))); + done(new Error("Should not fulfill with " + JSON.stringify(res))); }) .catch((err) => { if ("response" in err && err.response.status == 400) { @@ -1342,7 +1367,7 @@ describe("PUT operations", () => { field: "name", }) .then((res) => { - done(new Error("Should not full-fill with " + JSON.stringify(res))); + done(new Error("Should not fulfill with " + JSON.stringify(res))); }) .catch((err) => { if ("response" in err && err.response.status == 400) { diff --git a/tests/ts-types-test.spec.ts b/tests/ts-types-test.spec.ts index dd24c86..3746419 100644 --- a/tests/ts-types-test.spec.ts +++ b/tests/ts-types-test.spec.ts @@ -1,33 +1,37 @@ -// VS-Code stress test for TypeScript types of Firestorm-db. -import * as firestorm from "../typings/index"; +// Stress test for firestorm-db TypeScript types. +import firestorm from ".."; /** - ** I. CREATE A COLLECTION - **/ + * I. CREATE A COLLECTION + */ // let's declare an interface for our collection interface User { name: string; } -// then we use that interface in our collection +// then we add that interface in our constructor firestorm.collection("users"); -// we can also declare a collection with methods, -// those methods should be implemented in the interface too +// we can also declare a collection with methods listed in the interface interface UserWithMethods extends User { getNameAsLowerCase: () => string; } -firestorm.collection("users", (col) => { +// where the method implementation goes in the addMethods +const usersWithMethods = firestorm.collection("users", (col) => { col.getNameAsLowerCase = (): string => col.name.toLowerCase(); - return col; }); +// we can use the methods in all results... +usersWithMethods.get("someKey").then((res) => res.getNameAsLowerCase()); +// ...but cannot write or search for method fields +usersWithMethods.select({ fields: ["name", "family"] }); // getNameAsLowerCase not allowed here + /** - ** II. SEARCH THROUGH COLLECTIONS - **/ + * II. SEARCH THROUGH COLLECTIONS + */ // 1. search through a collection interface User { @@ -43,7 +47,7 @@ const users = firestorm.collection("users"); // search all users that have the name 'john' (not case sensitive) users.search([{ field: "name", criteria: "==", value: "John", ignoreCase: true }]); -// search all users that have the name 'john' (case sensitive) +// search all users that have the name 'John' (case sensitive) users.search([{ field: "name", criteria: "==", value: "John" }]); users.search([{ field: "name", criteria: "==", value: "John", ignoreCase: false }]); @@ -77,10 +81,10 @@ firestorm.collection("families", (col) => { }); /** - ** III. EDIT FIELDS OF AN ITEM IN A COLLECTION - **/ + * III. METHODS WITH OBJECT PARAMETERS + */ -// editing 1 field +// editing one field users.editField({ id: "1291931", // the user you want to edit the field field: "age", @@ -103,3 +107,15 @@ users.editFieldBulk([ value: 69, }, ]); + +// selecting two fields those types and an ID field (always given) +users.select({ fields: ["age", "emails"] }); + +// select an array value with flatten mode will give a flattened type as a result +users.values({ field: "emails", flatten: true }); + +// flattening turned off gives an extra level of nesting, so the types reflect that +users.values({ field: "emails" }); + +// if not array type (can't really be flattened) it gets ignored +users.values({ field: "age", flatten: true }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 36c90e6..cd2efe4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,49 +1,45 @@ export type NumberCriteria = - | "==" /** test if the value is equal to the provided value */ - | "!=" /** test if the value is not equal to the provided value */ - | "<" /** test if the value is less than the provided value */ - | "<=" /** test if the value is less than or equal to the provided value */ - | ">" /** test if the value is greater than the provided value */ - | ">=" /** test if the value is greater than or equal to the provided value */ - | "in" /** test if the value is in the given array */; + | "==" /** Value is equal to the provided value */ + | "!=" /** Value is not equal to the provided value */ + | "<" /** Value is less than the provided value */ + | "<=" /** Value is less than or equal to the provided value */ + | ">" /** Value is greater than the provided value */ + | ">=" /** Value is greater than or equal to the provided value */ + | "in"; /** Value is in the given array */ export type StringCriteria = - | "==" /** test if the string value is equal to the provided value */ - | "!=" /** test if the string value is not equal to the provided value */ - | "<" /** test if the string value length is less than the provided value */ - | "<=" /** test if the string value length is less than or equal to the provided value */ - | ">" /** test if the string value length is greater than the provided value */ - | ">=" /** test if the string value length is greater than or equal to the provided value */ - | "in" /** test if the string value is in the given array */ - | "includes" /** test if the string value includes the provided value */ - | "contains" /** same as "includes" */ - | "startsWith" /** test if the string value starts with the provided value */ - | "endsWith" /** test if the string value ends with the provided value */; + | "==" /** String value is equal to the provided value */ + | "!=" /** String value is not equal to the provided value */ + | "<" /** String value length is less than the provided value */ + | "<=" /** String value length is less than or equal to the provided value */ + | ">" /** String value length is greater than the provided value */ + | ">=" /** String value length is greater than or equal to the provided value */ + | "in" /** String value is in the given array */ + | "includes" /** String value includes the provided value */ + | "contains" /** Same as "includes" */ + | "startsWith" /** String value starts with the provided value */ + | "endsWith"; /** String value ends with the provided value */ export type ArrayCriteria = - | "array-contains" /** test if the value is in the given array */ - | "array-contains-any" /** test if the any value of the array is in the given array */ - | "array-length-eq" /** test if the array length is equal to the provided value */ - | "array-length-df" /** test if the array length is different from the provided value */ - | "array-length-gt" /** test if the array length is greater than the provided value */ - | "array-length-lt" /** test if the array length is less than the provided value */ - | "array-length-ge" /** test if the array length is greater than or equal to the provided value */ - | "array-length-le" /** test if the array length is less than or equal to the provided value */; + | "array-contains" /** Value is in the given array */ + | "array-contains-any" /** Any value of the array is in the given array */ + | "array-length-eq" /** Array length is equal to the provided value */ + | "array-length-df" /** Array length is different from the provided value */ + | "array-length-gt" /** Array length is greater than the provided value */ + | "array-length-lt" /** Array length is less than the provided value */ + | "array-length-ge" /** Array length is greater than or equal to the provided value */ + | "array-length-le"; /** Array length is less than or equal to the provided value */ export type BooleanCriteria = - | "!=" /** test if the value is not equal to the provided value */ - | "==" /** test if the value is equal to the provided value */; + | "!=" /** Value is not equal to the provided value */ + | "=="; /** Value is equal to the provided value */ -export type AllCriteria = - | StringCriteria /** criteria applying to strings */ - | ArrayCriteria /** criteria applying to arrays */ - | BooleanCriteria /** criteria applying to boolean */ - | NumberCriteria /** criteria applying to numbers */; +export type AnyCriteria = StringCriteria | ArrayCriteria | BooleanCriteria | NumberCriteria; export type Criteria = T extends Function ? never : - | never /** methods are not allowed in the field (they are not saved in the collection JSON file) */ + | never /** Methods are not allowed in the field (they are not saved in the collection JSON file) */ | T extends Array ? ArrayCriteria : never | T extends string @@ -139,29 +135,46 @@ export type WriteConfirmation = { message: string }; export type SearchOption = { [K in keyof T]: { - /** the field to be searched for */ + /** The field to be searched for */ field: Path; - /** the criteria to be used to search for the field */ + /** Search criteria to filter results */ criteria: Criteria; - /** the value to be searched for */ + /** The value to be searched for */ value?: any; - /** is it case sensitive? (default true) */ + /** Is it case sensitive? (default true) */ ignoreCase?: boolean; }; }[keyof T]; -export interface SelectOption { - /** Chosen fields to eventually return */ - fields: Array; +export interface SelectOption { + /** Selected fields to be returned */ + fields: T; } -export interface CollectionMethods { - (collectionElement: Collection & T): Collection & T; +export interface ValueOption { + /** Field to search */ + field: K | "id"; + /** Flatten array fields? (default false) */ + flatten?: F; } -export type NoMethods = { - [K in keyof T]: T[K] extends Function ? K : never; -}[keyof T]; +/** Add methods to found elements */ +export type CollectionMethods = (collectionElement: T) => T; + +/** Remove methods from a type */ +export type RemoveMethods = Pick< + T, + { + [K in keyof T]: T[K] extends Function ? never : K; + }[keyof T] +>; + +/** ID field not known at add time */ +export type Addable = Omit, "id">; +/** ID field can be provided in request */ +export type Settable = Addable & { + id?: number | string; +}; export class Collection { /** @@ -192,7 +205,7 @@ export class Collection { * @returns The found elements */ public search( - options: SearchOption[], + options: SearchOption & { id: string | number }>[], random?: boolean | number, ): Promise; @@ -211,7 +224,7 @@ export class Collection { /** * Returns the whole content of the JSON - * @deprecated Use readRaw instead + * @deprecated Use {@link readRaw} instead * @returns The entire collection */ public read_raw(): Promise>; @@ -222,7 +235,18 @@ export class Collection { * @param option - The option you want to select * @returns Selected fields */ - public select(option: SelectOption): Promise>>; + public select>( + option: SelectOption, + ): Promise>>; + + /** + * Get all distinct non-null values for a given key across a collection + * @param option - Value options + * @returns Array of unique values + */ + public values, F extends boolean = false>( + option: ValueOption, + ): Promise ? (F extends true ? T[K] : T[K][]) : T[K][]>; /** * Get random max entries offset with a given seed @@ -238,29 +262,29 @@ export class Collection { * @param value - The value to write * @returns Write confirmation */ - public writeRaw(value: Record): Promise; + public writeRaw(value: Record>): Promise; /** * Set the entire JSON file contents - * @deprecated Use writeRaw instead + * @deprecated Use {@link writeRaw} instead * @param value - The value to write * @returns Write confirmation */ - public write_raw(value: Record): Promise; + public write_raw(value: Record>): Promise; /** * Automatically add a value to the JSON file * @param value - The value (without methods) to add * @returns The generated ID of the added element */ - public add(value: Writable): Promise; + public add(value: Addable): Promise; /** * Automatically add multiple values to the JSON file * @param values - The values (without methods) to add * @returns The generated IDs of the added elements */ - public addBulk(values: Writable[]): Promise; + public addBulk(values: Addable[]): Promise; /** * Remove an element from the collection by its ID @@ -282,7 +306,7 @@ export class Collection { * @param value - The value (without methods) you want to edit * @returns Write confirmation */ - public set(id: string | number, value: Writable): Promise; + public set(id: string | number, value: Settable): Promise; /** * Set multiple values in the collection by their IDs @@ -290,29 +314,26 @@ export class Collection { * @param values - The values (without methods) you want to edit * @returns Write confirmation */ - public setBulk(ids: string[] | number[], values: Writable[]): Promise; + public setBulk(ids: string[] | number[], values: Settable[]): Promise; /** * Edit one field of the collection * @param edit - The edit object - * @returns The edited element + * @returns Edit confirmation */ - public editField(edit: EditField): Promise; + public editField(edit: EditField>): Promise<{ success: boolean }>; /** * Change one field from multiple elements of the collection * @param edits - The edit objects - * @returns The edited elements + * @returns Edit confirmation */ - public editFieldBulk(edits: EditField[]): Promise; + public editFieldBulk(edits: EditField>[]): Promise<{ success: boolean[] }>; } /** Value for the id field when searching content */ export const ID_FIELD: string; -// don't need ID field when adding keys, and setting keys has a separate ID argument -type Writable = Omit>, "id">; - /** * Change the current Firestorm address * @param value - The new Firestorm address @@ -347,24 +368,25 @@ export function table(table: string): Promise>; */ export declare const files: { /** - * Get file back + * Get a file by its path * @param path - The file path wanted + * @returns File contents */ get(path: string): Promise; /** - * Upload file + * Upload a file * @param form - The form data with path, filename, and file - * @returns HTTP response + * @returns Write confirmation */ - upload(form: FormData): Promise; + upload(form: FormData): Promise; /** - * Deletes a file given its path + * Deletes a file by path * @param path - The file path to delete - * @returns HTTP response + * @returns Write confirmation */ - delete(path: string): Promise; + delete(path: string): Promise; }; /**